@mhosaic/feedback-cli 0.49.0 → 0.50.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 (48) hide show
  1. package/README.md +1 -1
  2. package/dist/bin.js +6 -7
  3. package/dist/{build-RCLWV2WL.js → build-AHXPNO76.js} +1 -2
  4. package/dist/{check-FOJPEE4Q.js → check-UZZFPZMA.js} +3 -4
  5. package/dist/{chunk-COJ75KUD.js → chunk-45QCNACT.js} +0 -1
  6. package/dist/{chunk-AZQSQJNQ.js → chunk-CFJPCJAQ.js} +0 -1
  7. package/dist/{chunk-SSLQOK2Z.js → chunk-GJEJSB2Q.js} +2 -3
  8. package/dist/{chunk-IHJPCMYF.js → chunk-KHQBWJ4A.js} +0 -1
  9. package/dist/config-75OC57EE.js +7 -0
  10. package/dist/{doctor-CTL34U4Y.js → doctor-Z4KZBFBN.js} +1 -2
  11. package/dist/{eject-VNJVOCQM.js → eject-QFPZ7Z3P.js} +1 -2
  12. package/dist/{generate-VFRQNPOI.js → generate-CLL5W7SX.js} +3 -4
  13. package/dist/{init-RS7BKALE.js → init-BK6TPKX6.js} +1 -2
  14. package/dist/{install-skill-BIBDYLKE.js → install-skill-FZ4YOIHX.js} +79 -10
  15. package/dist/{qa-YR4GCV6T.js → qa-APZ5CKN5.js} +9 -10
  16. package/dist/{sitemap-react-QTEAUKN5.js → sitemap-react-IF5PX2PM.js} +2 -3
  17. package/dist/{sitemap-vue-EJDQTCYM.js → sitemap-vue-LWK6Y4FE.js} +2 -3
  18. package/dist/{verify-LCXL3WOX.js → verify-24HTNRXV.js} +0 -1
  19. package/package.json +1 -1
  20. package/skills/integrate-feedback/SKILL.md +25 -91
  21. package/skills/integrate-feedback/references/consumer-install.md +3 -3
  22. package/skills/integrate-feedback/references/verify-install.md +21 -20
  23. package/dist/bin.js.map +0 -1
  24. package/dist/build-RCLWV2WL.js.map +0 -1
  25. package/dist/check-FOJPEE4Q.js.map +0 -1
  26. package/dist/chunk-AZQSQJNQ.js.map +0 -1
  27. package/dist/chunk-COJ75KUD.js.map +0 -1
  28. package/dist/chunk-IHJPCMYF.js.map +0 -1
  29. package/dist/chunk-SSLQOK2Z.js.map +0 -1
  30. package/dist/config-JE3QRZVH.js +0 -8
  31. package/dist/config-JE3QRZVH.js.map +0 -1
  32. package/dist/doctor-CTL34U4Y.js.map +0 -1
  33. package/dist/eject-VNJVOCQM.js.map +0 -1
  34. package/dist/generate-VFRQNPOI.js.map +0 -1
  35. package/dist/init-RS7BKALE.js.map +0 -1
  36. package/dist/install-skill-BIBDYLKE.js.map +0 -1
  37. package/dist/qa-YR4GCV6T.js.map +0 -1
  38. package/dist/sitemap-react-QTEAUKN5.js.map +0 -1
  39. package/dist/sitemap-vue-EJDQTCYM.js.map +0 -1
  40. package/dist/verify-LCXL3WOX.js.map +0 -1
  41. package/skills/chantier/SKILL.md +0 -141
  42. package/skills/feedback-close/SKILL.md +0 -66
  43. package/skills/feedback-fix/SKILL.md +0 -176
  44. package/skills/feedback-from-meeting/SKILL.md +0 -173
  45. package/skills/feedback-pull/SKILL.md +0 -74
  46. package/skills/feedback-watch-merges/SKILL.md +0 -68
  47. package/skills/integrate-feedback/references/operator-provision.md +0 -397
  48. package/skills/issue-pull/SKILL.md +0 -49
@@ -53,7 +53,7 @@ Before anything else, **confirm the cwd is actually the target.** This is the mo
53
53
 
54
54
  - Read `package.json`. If its `name` is `@mhosaic/feedback-tool-mhosaic`, `@mhosaic/feedback`, `@mhosaic/feedback-admin`, `@mhosaic/feedback-cli`, or contains the literal string `mhosaic/feedback`: **STOP**. The user is inside our own monorepo, not their host app. Tell them:
55
55
 
56
- > "Looks like we're inside `feedback-tool-mhosaic` itself. The consumer install writes to the current directory; running it here would install the widget into our own monorepo. Either pass `--cwd <target-app-path>` to the CLI, or close Claude and re-open it inside the target app's root, then re-run `/integrate-feedback` and pick 'Install (consumer)'."
56
+ > "Looks like we're inside `feedback-tool-mhosaic` itself. The consumer install writes to the current directory; running it here would install the widget into our own monorepo. Either pass `--cwd <target-app-path>` to the CLI, or close Claude and re-open it inside the target app's root, then re-run `/integrate-feedback`."
57
57
 
58
58
  Wait for the user to confirm they've moved before continuing.
59
59
 
@@ -207,7 +207,7 @@ Follow `references/verify-install.md` for the full end-to-end smoke test:
207
207
  2. Drive Chrome to the dev URL
208
208
  3. Screenshot — confirm the FAB renders (bottom-right corner)
209
209
  4. Click the FAB, fill the form with `[INTEGRATE-FEEDBACK SMOKE TEST]`, submit
210
- 5. Drive Chrome to `https://software-factory-3tbbu.ondigitalocean.app/c/<company-slug>/reports` and confirm the report landed
210
+ 5. If you have access to the Mhosaic console, drive Chrome to `https://software-factory-3tbbu.ondigitalocean.app/c/<company-slug>/reports` and confirm the report landed; otherwise the submit confirmation in the widget is the pass signal, and your Mhosaic contact confirms the report arrived (see `references/verify-install.md`)
211
211
 
212
212
  Both verify (step 8) and smoke test (step 9) write to the report stream — the operator can clean them up after a successful integration (admin SPA → /c/<company-slug>/reports → filter by description → bulk delete).
213
213
 
@@ -229,4 +229,4 @@ Encourage them to:
229
229
  - Tell the operator the smoke test passed
230
230
  - Delete the smoke-test reports from `/reports` in admin once it lands
231
231
 
232
- If a follow-up question comes up (e.g. "how do I add another origin?"), point them to the operator's `/integrate-feedback` provisioning mode (operator can re-run it with the same project slug to update origins) OR direct the operator to edit the project in the admin SPA at `/projects/<id>`.
232
+ If a follow-up question comes up (e.g. "how do I add another origin?"), tell them to ask their Mhosaic contact — provisioning (re-running with the same project slug to update origins, or editing the project in the admin SPA at `/projects/<id>`) is Mhosaic-internal, not something the consumer side does.
@@ -92,13 +92,12 @@ and surface the status code + response body.
92
92
 
93
93
  ---
94
94
 
95
- ## Step 6 — Confirm the report landed in admin
95
+ ## Step 6 — Confirm the report landed
96
96
 
97
- Drive Chrome to `https://software-factory-3tbbu.ondigitalocean.app/c/<company-slug>/reports`. You may need to log in if the operator's session expired.
97
+ Admin (`software-factory-3tbbu.ondigitalocean.app`) is @mhosaic.com-only SSO — most consumers won't have a login there. Branch on whether the user does:
98
98
 
99
- Filter by description containing `[INTEGRATE-FEEDBACK SMOKE TEST]`. The new report should appear at the top of the list.
99
+ **With admin access:** drive Chrome to `https://software-factory-3tbbu.ondigitalocean.app/c/<company-slug>/reports`. Filter by description containing `[INTEGRATE-FEEDBACK SMOKE TEST]`. The new report should appear at the top of the list. Click into it. Verify:
100
100
 
101
- Click into it. Verify:
102
101
  - Project = the project the operator just provisioned
103
102
  - Submitter = whatever identity was wired (anonymous-id, real user, etc.)
104
103
  - Page URL = the consumer's dev URL
@@ -106,6 +105,8 @@ Click into it. Verify:
106
105
 
107
106
  Screenshot this to give the user proof-of-life.
108
107
 
108
+ **Without admin access:** the pass signal is the `201`/`submitted (report id: …)` line from Step 1's `--with-test-report` run (and the success toast in Step 5). That's proof the report reached the backend — you can't see it land yourself. Tell the user to ask their Mhosaic contact to confirm it landed in admin, and don't claim this step "done" beyond that.
109
+
109
110
  ---
110
111
 
111
112
  ## Step 7 — (Optional cleanup) delete the smoke-test reports
@@ -121,22 +122,22 @@ Skip this step if the consumer wants to keep the smoke-test reports as a paper t
121
122
 
122
123
  ## Diagnostic table — when checks fail
123
124
 
124
- | Symptom | Likely cause | Fix |
125
- |---|---|---|
126
- | `✗ .env.local exists with both keys` | CLI init never ran, or ran in a different directory | Re-run `mhosaic-feedback init` from the project root |
127
- | `✗ API key starts with pk_proj_` | Operator pasted the wrong key (e.g. `sk_proj_…`, an MCP key) | Operator mints a *public* widget key (`kind: "public"`) in admin SPA |
128
- | `✗ Endpoint reachable over network` (network error) | DNS, firewall, offline, or wrong endpoint URL | Verify the URL in `.env.local`; try `curl -I <endpoint>/admin/login/` |
129
- | `✗ Endpoint reachable` (5xx) | Backend is down or mid-deploy | Wait 1–2 minutes; check DO App Platform deploy status |
130
- | `✗ CORS allows POST from <origin>` | Operator forgot to add this origin to `allowed_origins` | Operator opens admin SPA → Edit project → adds the dev URL (e.g. `http://localhost:5173`) |
131
- | `✗ POST /api/feedback/v1/reports/` returns 401 | Key invalid or revoked | Operator mints a new key |
132
- | `✗ POST /api/feedback/v1/reports/` returns 403 with "origin not allowed" | Same as CORS row above | Operator adds the origin |
133
- | FAB doesn't render in dev | `identify()` not called with a non-empty `id` | Wire identify per `identify-snippets.md`, OR for testing pass `{id: 'test-user'}` |
134
- | FAB renders but form submit fails with 403 | Origin not in `allowed_origins` (Vite dev URL differs from prod URL) | Operator adds the dev URL |
135
- | FAB renders but form submit fails with 400 | Serializer rejected the payload (description empty, env unknown, etc.) | Read DevTools network response — usually a clear field-level error |
136
- | Console error: `Failed to load module` for `@mhosaic/feedback` | Vite cache mismatch | Stop dev server, delete `node_modules/.vite`, restart |
137
- | Console error: `Unhandled CORS preflight failure` | Origin not allowlisted on the backend side | Operator adds the origin; consumer hard-reloads |
138
- | Widget submits anonymous reports despite identify() running | identify() called in a stale closure (effect dep array missing the user) | Re-read identify-snippets.md — every snippet has the right deps |
139
- | `verify --with-test-report` succeeds but the report doesn't appear in admin | Browser cache; report is filtered by default (e.g. `synthetic=true` hides it) | Refresh admin /reports; toggle "show synthetic" filter |
125
+ | Symptom | Likely cause | Fix |
126
+ | --------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
127
+ | `✗ .env.local exists with both keys` | CLI init never ran, or ran in a different directory | Re-run `mhosaic-feedback init` from the project root |
128
+ | `✗ API key starts with pk_proj_` | Operator pasted the wrong key (e.g. `sk_proj_…`, an MCP key) | Operator mints a _public_ widget key (`kind: "public"`) in admin SPA |
129
+ | `✗ Endpoint reachable over network` (network error) | DNS, firewall, offline, or wrong endpoint URL | Verify the URL in `.env.local`; try `curl -I <endpoint>/admin/login/` |
130
+ | `✗ Endpoint reachable` (5xx) | Backend is down or mid-deploy | Wait 1–2 minutes; check DO App Platform deploy status |
131
+ | `✗ CORS allows POST from <origin>` | Operator forgot to add this origin to `allowed_origins` | Operator opens admin SPA → Edit project → adds the dev URL (e.g. `http://localhost:5173`) |
132
+ | `✗ POST /api/feedback/v1/reports/` returns 401 | Key invalid or revoked | Operator mints a new key |
133
+ | `✗ POST /api/feedback/v1/reports/` returns 403 with "origin not allowed" | Same as CORS row above | Operator adds the origin |
134
+ | FAB doesn't render in dev | `identify()` not called with a non-empty `id` | Wire identify per `identify-snippets.md`, OR for testing pass `{id: 'test-user'}` |
135
+ | FAB renders but form submit fails with 403 | Origin not in `allowed_origins` (Vite dev URL differs from prod URL) | Operator adds the dev URL |
136
+ | FAB renders but form submit fails with 400 | Serializer rejected the payload (description empty, env unknown, etc.) | Read DevTools network response — usually a clear field-level error |
137
+ | Console error: `Failed to load module` for `@mhosaic/feedback` | Vite cache mismatch | Stop dev server, delete `node_modules/.vite`, restart |
138
+ | Console error: `Unhandled CORS preflight failure` | Origin not allowlisted on the backend side | Operator adds the origin; consumer hard-reloads |
139
+ | Widget submits anonymous reports despite identify() running | identify() called in a stale closure (effect dep array missing the user) | Re-read identify-snippets.md — every snippet has the right deps |
140
+ | `verify --with-test-report` succeeds but the report doesn't appear in admin | Browser cache; report is filtered by default (e.g. `synthetic=true` hides it) | Refresh admin /reports; toggle "show synthetic" filter |
140
141
 
141
142
  ---
142
143
 
package/dist/bin.js.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/bin.ts"],"sourcesContent":["async function main() {\n const [, , cmd, ...args] = process.argv\n if (!cmd || cmd === 'init') {\n const { runInit } = await import('./commands/init')\n return runInit(args)\n }\n if (cmd === 'doctor') {\n const { runDoctor } = await import('./commands/doctor')\n return runDoctor(args)\n }\n if (cmd === 'eject') {\n const { runEject } = await import('./commands/eject')\n return runEject(args)\n }\n if (cmd === 'verify') {\n const { runVerify } = await import('./commands/verify')\n return runVerify(args)\n }\n if (cmd === 'install-skill') {\n const { runInstallSkill } = await import('./commands/install-skill')\n return runInstallSkill(args)\n }\n if (cmd === 'qa') {\n const { runQa } = await import('./qa')\n return runQa(args)\n }\n if (cmd === '--version' || cmd === '-v') {\n process.stdout.write('0.14.1\\n')\n return\n }\n process.stderr.write(`Unknown command: ${cmd}\\n\\nUsage:\\n mhosaic-feedback init Set up the widget in this project\\n mhosaic-feedback doctor Check local setup\\n mhosaic-feedback verify Ping the backend with the configured key\\n mhosaic-feedback eject Strip widget integration from this project\\n mhosaic-feedback install-skill Copy the Claude Code skill to ~/.claude/skills\\n mhosaic-feedback qa Run QA-meter commands (sitemap|generate|build|check|refresh)\\n`)\n process.exitCode = 1\n}\n\nmain().catch((err) => {\n process.stderr.write(`${err instanceof Error ? err.message : String(err)}\\n`)\n process.exitCode = 1\n})\n"],"mappings":";;;AAAA,eAAe,OAAO;AACpB,QAAM,CAAC,EAAE,EAAE,KAAK,GAAG,IAAI,IAAI,QAAQ;AACnC,MAAI,CAAC,OAAO,QAAQ,QAAQ;AAC1B,UAAM,EAAE,QAAQ,IAAI,MAAM,OAAO,oBAAiB;AAClD,WAAO,QAAQ,IAAI;AAAA,EACrB;AACA,MAAI,QAAQ,UAAU;AACpB,UAAM,EAAE,UAAU,IAAI,MAAM,OAAO,sBAAmB;AACtD,WAAO,UAAU,IAAI;AAAA,EACvB;AACA,MAAI,QAAQ,SAAS;AACnB,UAAM,EAAE,SAAS,IAAI,MAAM,OAAO,qBAAkB;AACpD,WAAO,SAAS,IAAI;AAAA,EACtB;AACA,MAAI,QAAQ,UAAU;AACpB,UAAM,EAAE,UAAU,IAAI,MAAM,OAAO,sBAAmB;AACtD,WAAO,UAAU,IAAI;AAAA,EACvB;AACA,MAAI,QAAQ,iBAAiB;AAC3B,UAAM,EAAE,gBAAgB,IAAI,MAAM,OAAO,6BAA0B;AACnE,WAAO,gBAAgB,IAAI;AAAA,EAC7B;AACA,MAAI,QAAQ,MAAM;AAChB,UAAM,EAAE,MAAM,IAAI,MAAM,OAAO,kBAAM;AACrC,WAAO,MAAM,IAAI;AAAA,EACnB;AACA,MAAI,QAAQ,eAAe,QAAQ,MAAM;AACvC,YAAQ,OAAO,MAAM,UAAU;AAC/B;AAAA,EACF;AACA,UAAQ,OAAO,MAAM,oBAAoB,GAAG;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,CAAod;AAChgB,UAAQ,WAAW;AACrB;AAEA,KAAK,EAAE,MAAM,CAAC,QAAQ;AACpB,UAAQ,OAAO,MAAM,GAAG,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;AAAA,CAAI;AAC5E,UAAQ,WAAW;AACrB,CAAC;","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/qa/build.ts","../src/qa/color.ts","../src/qa/results.ts","../src/qa/load-suites.ts"],"sourcesContent":["/**\n * `qa build` — assemble the `qa-status.json` artifact from suites + sitemap +\n * Playwright results. HONEST by construction: lanes reflect only real evidence\n * (a scenario whose case is `covered` with a matching report entry gets that\n * run's lane; everything else is `never_run`), no client validations exist in\n * v1 (⇒ colour ceiling is `yellow`), and pages with no scenarios stay `gray`.\n *\n * Pipeline: load suites → per-pattern scenario seeds; DEDUP generated twins of\n * authored automation links (authored wins); merge automated/dev/client lanes;\n * derive scenario colour; build page-status entries from the sitemap (or a flat\n * list in degraded mode); roll the colours/counts up the sitemap subtree; and\n * compute the dual coverage/passing metric.\n *\n * The structural assembly helpers (`countsFor`, the rollup tree-walk, the dual\n * metric) are ported from the Vigilance donor `scripts/build-demo-status.mjs`\n * (every DEMO_/synthetic block discarded).\n */\n\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'\nimport { dirname } from 'node:path'\nimport { loadQaConfig, type QaConfig } from './config'\nimport { aggregateColor, deriveScenarioColor, isValidationStale } from './color'\nimport { readResults, type AutomatedLaneResult } from './results'\nimport { loadScenarioSeeds, type ScenarioSeed } from './load-suites'\nimport type {\n QaAutomatedLane,\n QaColor,\n QaCounts,\n QaHumanLane,\n QaMetric,\n QaPageStatus,\n QaScenario,\n QaScenarioPageRef,\n QaSitemapNode,\n QaStatusArtifact,\n} from './artifact-types'\nimport type { SitemapPage, SitemapPayload } from './sitemap-core'\n\nexport interface BuildOptions {\n /** App version stamped into the artifact (overrides cfg.appVersion). */\n appVersion?: string\n /** Injected for deterministic goldens; `runBuild` passes `new Date().toISOString()`. */\n generatedAt?: string\n}\n\nconst EMPTY_HUMAN: QaHumanLane = { validated: false, stale: false }\n/** Stable app_version fallback so the golden is reproducible when none is supplied. */\nconst FALLBACK_VERSION = '0.0.0'\n\n// ─────────────────────────────────────────────────── helpers ────────────────\n\n/** Slug-free stable node id for a pattern (ported from the donor `idFor`). */\nfunction idFor(pattern: string): string {\n return 'p' + pattern.replace(/[^a-zA-Z0-9]+/g, '-').replace(/-+$/, '')\n}\n\nfunction humanize(str: string): string {\n if (!str) return ''\n const cleaned = str.replace(/[:?()*]/g, '').replace(/[-_/]+/g, ' ').trim()\n return cleaned ? cleaned.charAt(0).toUpperCase() + cleaned.slice(1) : ''\n}\n\nfunction lastSegment(pattern: string): string {\n const parts = pattern.split('/').filter(Boolean)\n return parts[parts.length - 1] ?? ''\n}\n\n/** Human title for a sitemap page (ported from the donor `titleFor`). */\nfunction titleForPage(page: SitemapPage): string {\n return (\n page.title ||\n humanize(page.route_name ?? '') ||\n humanize(lastSegment(page.path_pattern)) ||\n page.path_pattern\n )\n}\n\nfunction emptyCounts(): QaCounts {\n return { scenarios: 0, passing: 0, dev_validated: 0, client_validated: 0 }\n}\n\n/** Counts over a scenario list (ported from the donor `countsFor`). */\nfunction countsFor(scenarios: QaScenario[]): QaCounts {\n return {\n scenarios: scenarios.length,\n passing: scenarios.filter((s) => s.automated.status === 'passing').length,\n dev_validated: scenarios.filter((s) => s.dev.validated).length,\n client_validated: scenarios.filter((s) => s.client.validated).length,\n }\n}\n\n// ─────────────────────────────────────── dev validations (optional) ─────────\n\ninterface DevValidationEvent {\n external_key?: string\n validated?: boolean\n rejected?: boolean\n by?: string\n at?: string\n app_version?: string\n note?: string\n}\n\n/**\n * Latest dev-validation event per `external_key`, from the optional\n * `devValidationsFile` (a flat array of events). Latest = greatest `at`.\n */\nfunction loadDevValidations(file: string | null): Map<string, DevValidationEvent> {\n const out = new Map<string, DevValidationEvent>()\n if (!file || !existsSync(file)) return out\n let parsed: unknown\n try {\n parsed = JSON.parse(readFileSync(file, 'utf8'))\n } catch (err) {\n throw new Error(`qa build: failed to read dev validations ${file}: ${err instanceof Error ? err.message : String(err)}`)\n }\n const events: DevValidationEvent[] = Array.isArray(parsed)\n ? (parsed as DevValidationEvent[])\n : Array.isArray((parsed as { events?: unknown }).events)\n ? ((parsed as { events: DevValidationEvent[] }).events)\n : []\n for (const ev of events) {\n if (!ev.external_key) continue\n const prev = out.get(ev.external_key)\n if (!prev || (ev.at ?? '') >= (prev.at ?? '')) out.set(ev.external_key, ev)\n }\n return out\n}\n\n// ─────────────────────────────────────────── scenario assembly ──────────────\n\n/**\n * Merge a seed's lanes into a finished `QaScenario`:\n * - automated: the report lane WHEN the case is `covered` AND a report entry\n * exists; else `never_run` (honesty — no invented runs);\n * - dev: latest event from the dev-validations file by `external_key`, with\n * derived staleness; else empty;\n * - client: always empty in v1.\n */\nfunction assembleScenario(\n seed: ScenarioSeed,\n results: Map<string, AutomatedLaneResult>,\n devEvents: Map<string, DevValidationEvent>,\n): QaScenario {\n let automated: QaAutomatedLane = { status: 'never_run' }\n if (seed.caseStatus === 'covered' && seed.spec_path && seed.test_title) {\n const lane = results.get(`${seed.spec_path}::${seed.test_title}`)\n if (lane) {\n automated = {\n status: lane.status,\n ...(lane.at ? { at: lane.at } : {}),\n ...(lane.provenance ? { provenance: lane.provenance } : {}),\n }\n }\n }\n\n let dev: QaHumanLane = { ...EMPTY_HUMAN }\n const ev = devEvents.get(seed.external_key)\n if (ev) {\n const stale = ev.validated\n ? isValidationStale({\n ...(ev.at ? { validatedAt: ev.at } : {}),\n automatedStatus: automated.status,\n ...(automated.at ? { automatedAt: automated.at } : {}),\n })\n : false\n dev = {\n validated: ev.validated === true,\n stale,\n ...(ev.rejected ? { rejected: true } : {}),\n ...(ev.by ? { by: ev.by } : {}),\n ...(ev.at ? { at: ev.at } : {}),\n ...(ev.app_version ? { app_version: ev.app_version } : {}),\n ...(ev.note ? { note: ev.note } : {}),\n }\n }\n\n const client: QaHumanLane = { ...EMPTY_HUMAN }\n const color = deriveScenarioColor(automated, dev, client)\n\n return {\n id: seed.id,\n external_key: seed.external_key,\n title: seed.title,\n color,\n source: seed.source,\n ...(seed.spec_path ? { spec_path: seed.spec_path } : {}),\n pages: seed.pages,\n group: seed.group,\n ...(seed.priority ? { priority: seed.priority } : {}),\n ...(seed.caseStatus ? { caseStatus: seed.caseStatus } : {}),\n ...(seed.grounding ? { grounding: seed.grounding } : {}),\n automated,\n dev,\n client,\n }\n}\n\n/**\n * DEDUP (Step 3): an authored case whose `(spec_path, test_title)` an automation\n * link names suppresses the GENERATED twin with the same key. Authored wins (it\n * carries human metadata). Returns the seeds to keep, per pattern unchanged.\n */\nfunction dedupSeeds(byPattern: Map<string, ScenarioSeed[]>): Map<string, ScenarioSeed[]> {\n // Collect every authored dedup key across all patterns.\n const authoredKeys = new Set<string>()\n for (const seeds of byPattern.values()) {\n for (const seed of seeds) {\n if (seed.origin === 'authored' && seed.dedupKey) authoredKeys.add(seed.dedupKey)\n }\n }\n const out = new Map<string, ScenarioSeed[]>()\n for (const [pattern, seeds] of byPattern) {\n const kept = seeds.filter(\n (seed) => !(seed.origin === 'generated' && seed.dedupKey && authoredKeys.has(seed.dedupKey)),\n )\n out.set(pattern, kept)\n }\n return out\n}\n\n/** Primary-scenario colours of a page (the page colour aggregates only these). */\nfunction primaryColors(scenarios: QaScenario[], pattern: string): QaColor[] {\n return scenarios\n .filter((s) => (s.pages ?? []).some((ref: QaScenarioPageRef) => ref.pattern === pattern && ref.role === 'primary'))\n .map((s) => s.color)\n}\n\n// ──────────────────────────────────────────────── artifact build ────────────\n\n/** Read & validate the sitemap payload from `file`. */\nfunction loadSitemap(file: string): SitemapPayload {\n let parsed: unknown\n try {\n parsed = JSON.parse(readFileSync(file, 'utf8'))\n } catch (err) {\n throw new Error(`qa build: failed to read sitemap ${file}: ${err instanceof Error ? err.message : String(err)}`)\n }\n if (typeof parsed !== 'object' || parsed === null || !Array.isArray((parsed as SitemapPayload).pages)) {\n throw new Error(`qa build: sitemap ${file} is malformed (expected { format, pages: [] })`)\n }\n return parsed as SitemapPayload\n}\n\n/**\n * Assemble the full `QaStatusArtifact`. Pure given its inputs + `opts` (no\n * non-deterministic `Date` on the tested path — `generatedAt` is injectable).\n */\nexport function buildArtifact(cfg: QaConfig, opts: BuildOptions = {}): QaStatusArtifact {\n const appVersion = opts.appVersion ?? cfg.appVersion ?? FALLBACK_VERSION\n const generatedAt = opts.generatedAt ?? FALLBACK_VERSION\n\n // 1. Seeds + dedup.\n const { byPattern: rawByPattern } = loadScenarioSeeds(cfg.suitesDir)\n const byPattern = dedupSeeds(rawByPattern)\n\n // 2. Run + dev evidence.\n const results = cfg.resultsFile && existsSync(cfg.resultsFile) ? readResults(cfg.resultsFile) : new Map()\n const devEvents = loadDevValidations(cfg.devValidationsFile)\n\n // 3. Assemble scenarios per pattern.\n const scenariosByPattern = new Map<string, QaScenario[]>()\n for (const [pattern, seeds] of byPattern) {\n scenariosByPattern.set(\n pattern,\n seeds.map((seed) => assembleScenario(seed, results, devEvents)),\n )\n }\n\n if (!cfg.sitemap) {\n return buildDegraded(scenariosByPattern, cfg, appVersion, generatedAt)\n }\n return buildWithSitemap(loadSitemap(cfg.sitemap.file), scenariosByPattern, cfg, appVersion, generatedAt)\n}\n\n/** A page-status entry for one pattern from its scenarios. */\nfunction pageStatusFor(\n pattern: string,\n title: string,\n scenarios: QaScenario[],\n): QaPageStatus {\n return {\n page: { id: idFor(pattern), path_pattern: pattern, title },\n color: aggregateColor(primaryColors(scenarios, pattern)),\n counts: countsFor(scenarios),\n scenarios,\n }\n}\n\nfunction pct(num: number, den: number): number {\n return den === 0 ? 0 : Math.round((100 * num) / den)\n}\n\nfunction metricFor(pageStatuses: QaPageStatus[], navigableTotal: number): QaMetric {\n const pagesWithScenarios = pageStatuses.filter((s) => s.counts.scenarios > 0).length\n const scenariosTotal = pageStatuses.reduce((n, s) => n + s.counts.scenarios, 0)\n const scenariosPassing = pageStatuses.reduce((n, s) => n + s.counts.passing, 0)\n return {\n pages_with_scenarios_pct: pct(pagesWithScenarios, navigableTotal),\n scenarios_passing_pct: pct(scenariosPassing, scenariosTotal),\n }\n}\n\n/**\n * Sitemap mode: page-status per navigable page node, plus the sitemap emitted as\n * a FOREST — one node per input page, `parent_id` = the id of the node whose\n * `path_pattern` is this page's input `parent` (or `null` for top-level pages).\n * NO synthetic root is invented: the real sitemap is a forest of top-level roots\n * (e.g. /login, /admin, /ingredients…), and the renderer's `buildTree` already\n * iterates every `parent_id === null` node as a root. Each node's rollup is over\n * its OWN subtree, so no node spans the whole app — correct for a forest.\n */\nfunction buildWithSitemap(\n sitemap: SitemapPayload,\n scenariosByPattern: Map<string, QaScenario[]>,\n cfg: QaConfig,\n appVersion: string,\n generatedAt: string,\n): QaStatusArtifact {\n const allPages = sitemap.pages\n const navigable = allPages.filter((p) => !p.structural)\n const titleByPattern = new Map(allPages.map((p) => [p.path_pattern, titleForPage(p)]))\n\n // Page-status for every navigable sitemap page (scenarios or none → gray).\n const pageStatusByPattern = new Map<string, QaPageStatus>()\n for (const page of navigable) {\n const scenarios = scenariosByPattern.get(page.path_pattern) ?? []\n pageStatusByPattern.set(\n page.path_pattern,\n pageStatusFor(page.path_pattern, titleByPattern.get(page.path_pattern)!, scenarios),\n )\n }\n\n // Patterns with scenarios but NO sitemap node — keep them so coverage isn't\n // lost (the design says `check` reports these). They get a page entry with a\n // null sitemap node id but are NOT rolled into the tree.\n const orphanPages: QaPageStatus[] = []\n for (const [pattern, scenarios] of scenariosByPattern) {\n if (!pageStatusByPattern.has(pattern)) {\n orphanPages.push(pageStatusFor(pattern, humanize(lastSegment(pattern)) || pattern, scenarios))\n }\n }\n\n const pages: QaPageStatus[] = [...pageStatusByPattern.values(), ...orphanPages]\n\n // ── Sitemap nodes with bottom-up rollups ──────────────────────────────────\n const childrenByParent = new Map<string | null, SitemapPage[]>()\n for (const p of allPages) {\n const list = childrenByParent.get(p.parent)\n if (list) list.push(p)\n else childrenByParent.set(p.parent, [p])\n }\n const subtree = (pattern: string): string[] => {\n const out = [pattern]\n for (const child of childrenByParent.get(pattern) ?? []) out.push(...subtree(child.path_pattern))\n return out\n }\n\n // Per-node rollup over the node's OWN subtree (real, non-structural pages only).\n const rollupOver = (statuses: QaPageStatus[]) => ({\n pages_total: statuses.length,\n pages_green: statuses.filter((s) => s.color === 'green').length,\n scenarios_total: statuses.reduce((n, s) => n + s.counts.scenarios, 0),\n scenarios_green: statuses.reduce((n, s) => n + s.scenarios.filter((x) => x.color === 'green').length, 0),\n })\n\n const nodes: QaSitemapNode[] = allPages.map((page) => {\n const status = pageStatusByPattern.get(page.path_pattern)\n const ownCounts = status ? status.counts : emptyCounts()\n // Subtree = self + descendants; keep only real (navigable) pages for rollups.\n const descStatuses = subtree(page.path_pattern)\n .map((pat) => pageStatusByPattern.get(pat))\n .filter((s): s is QaPageStatus => Boolean(s))\n return {\n id: idFor(page.path_pattern),\n path_pattern: page.path_pattern,\n title: titleByPattern.get(page.path_pattern)!,\n // Map the input `parent` (a path_pattern) → that node's id; null stays a root.\n parent_id: page.parent ? idFor(page.parent) : null,\n order: page.order,\n ...(page.structural ? { structural: true } : {}),\n color: aggregateColor(descStatuses.map((s) => s.color)),\n counts: ownCounts,\n rollup: rollupOver(descStatuses),\n }\n })\n\n // Global dual metric over ALL pages app-wide (independent of any sitemap node).\n const metric = metricFor(pages, navigable.length + orphanPages.length)\n\n return {\n format: 1,\n environment: cfg.environment,\n app_version: appVersion,\n generated_at: generatedAt,\n pages,\n sitemap: nodes,\n metric,\n }\n}\n\n/**\n * Degraded mode (no sitemap): a FLAT page list synthesized from the patterns\n * that have scenarios. Every sitemap node has `parent_id: null` (no tree). One\n * node per pattern, rolled up over itself only.\n */\nfunction buildDegraded(\n scenariosByPattern: Map<string, QaScenario[]>,\n cfg: QaConfig,\n appVersion: string,\n generatedAt: string,\n): QaStatusArtifact {\n const patterns = [...scenariosByPattern.keys()].sort((a, b) => a.localeCompare(b))\n const pages: QaPageStatus[] = patterns.map((pattern) =>\n pageStatusFor(pattern, humanize(lastSegment(pattern)) || pattern, scenariosByPattern.get(pattern)!),\n )\n\n const sitemap: QaSitemapNode[] = pages.map((ps, i) => ({\n id: ps.page!.id,\n path_pattern: ps.page!.path_pattern,\n title: ps.page!.title,\n parent_id: null,\n order: i,\n color: ps.color,\n counts: ps.counts,\n rollup: {\n pages_total: 1,\n pages_green: ps.color === 'green' ? 1 : 0,\n scenarios_total: ps.counts.scenarios,\n scenarios_green: ps.scenarios.filter((x) => x.color === 'green').length,\n },\n }))\n\n return {\n format: 1,\n environment: cfg.environment,\n app_version: appVersion,\n generated_at: generatedAt,\n pages,\n sitemap,\n metric: metricFor(pages, pages.length),\n }\n}\n\n// ─────────────────────────────────────────────────── CLI entry ──────────────\n\n/** Parse `--version <v>` from the arg list. */\nfunction parseVersion(args: string[]): string | undefined {\n const i = args.indexOf('--version')\n if (i >= 0 && args[i + 1]) return args[i + 1]\n return undefined\n}\n\nexport async function runBuild(args: string[]): Promise<void> {\n const cfg = loadQaConfig(process.cwd())\n const appVersion = parseVersion(args)\n let artifact: QaStatusArtifact\n try {\n artifact = buildArtifact(cfg, {\n ...(appVersion ? { appVersion } : {}),\n generatedAt: new Date().toISOString(),\n })\n } catch (err) {\n process.stderr.write(`${err instanceof Error ? err.message : String(err)}\\n`)\n process.exitCode = 1\n return\n }\n\n mkdirSync(dirname(cfg.outFile), { recursive: true })\n writeFileSync(cfg.outFile, `${JSON.stringify(artifact, null, 2)}\\n`, 'utf8')\n\n process.stderr.write(\n `qa build → ${cfg.outFile}\\n` +\n ` ${artifact.metric.pages_with_scenarios_pct}% pages with scenarios · ` +\n `${artifact.metric.scenarios_passing_pct}% scenarios passing\\n`,\n )\n}\n","/**\n * QA-Meter colour algebra — CLI-LOCAL MIRROR of the core package's\n * `packages/core/src/qa-meter/color.ts`. Lifted verbatim (it is tiny and\n * dependency-free) so the CLI stays self-contained: the build derives the SAME\n * colours the renderer would, from a single set of algebra rules.\n *\n * If the rules ever change, update BOTH copies — they are intentionally\n * structurally identical (doc 03 §9.2/§9.3/§9.4).\n */\n\nexport type QaColor = 'green' | 'yellow' | 'red' | 'gray'\nexport type QaAutomatedStatus = 'passing' | 'failing' | 'flaky' | 'never_run'\n\n/** Minimal shape of the automated lane the colour needs. */\nexport interface AutomatedLaneLike {\n status: QaAutomatedStatus\n}\n\n/** Minimal shape of a human lane (dev or client). */\nexport interface HumanLaneLike {\n validated: boolean\n rejected?: boolean\n /** Derived (never stored): a validation contradicted by a more-recent failing run. */\n stale?: boolean\n}\n\n/**\n * Colour of a scenario from its lane trinity (doc 03 §9.3):\n * - `red` : automated run failing OR latest human event `rejected`;\n * - `gray` : never run AND no human validation;\n * - `green` : automated `passing` AND dev validated AND client validated, nothing stale;\n * - `yellow`: everything else (incl. \"passes, awaiting validation\").\n *\n * In the temporary meter the client lane is always empty ⇒ `green` is\n * unreachable: the ceiling colour is `yellow`, by design (honesty §12).\n */\nexport function deriveScenarioColor(\n automated: AutomatedLaneLike,\n dev: HumanLaneLike,\n client: HumanLaneLike,\n): QaColor {\n const rejected = dev.rejected === true || client.rejected === true\n if (automated.status === 'failing' || rejected) {\n return 'red'\n }\n const everRun = automated.status !== 'never_run'\n const anyHuman = dev.validated || client.validated\n if (!everRun && !anyHuman) {\n return 'gray'\n }\n const fresh = dev.stale !== true && client.stale !== true\n if (automated.status === 'passing' && dev.validated && client.validated && fresh) {\n return 'green'\n }\n return 'yellow'\n}\n\n/**\n * Aggregate colour of a set: a page from its PRIMARY scenarios, or a sitemap node\n * from its subtree pages. Precedence (doc 03 §9.3): one red dominates; else all\n * green ⇒ green; empty or all gray ⇒ gray; else yellow.\n */\nexport function aggregateColor(colors: ReadonlyArray<QaColor>): QaColor {\n if (colors.length === 0) {\n return 'gray'\n }\n if (colors.includes('red')) {\n return 'red'\n }\n if (colors.every((c) => c === 'green')) {\n return 'green'\n }\n if (colors.every((c) => c === 'gray')) {\n return 'gray'\n }\n return 'yellow'\n}\n\n/**\n * Lenient staleness (§9.4) + light re-break (§9.2): a human validation is\n * \"contradicted\" when a FAILING automated run is more recent than it. We do NOT\n * stale on a mere version bump while automated passes. Timestamps = ISO 8601\n * (lexicographically comparable).\n */\nexport function isValidationStale(opts: {\n validatedAt?: string\n automatedStatus: QaAutomatedStatus\n automatedAt?: string\n}): boolean {\n if (!opts.validatedAt) {\n return false\n }\n return opts.automatedStatus === 'failing' && !!opts.automatedAt && opts.automatedAt > opts.validatedAt\n}\n","/**\n * `qa build` — read a Playwright JSON report into per-(spec, test) automated\n * lanes. HONEST: a lane reflects only what the report says. A test absent from\n * the report yields NO lane (the build maps that to `never_run`).\n *\n * The report nests: `report.suites[]` may contain `.suites[]`; each suite has\n * `.specs[]`; each spec has `.file`/`.title`/`.tests[]`; each test has\n * `.results[]` with `.status`/`.retry`/`.startTime`. We key on the spec `file`\n * EXACTLY as it appears in the report (e2eDir-relative) + the test title.\n */\n\nimport { readFileSync } from 'node:fs'\nimport type { QaAutomatedStatus } from './color'\n\n/** The automated lane derived for one (spec, title) pair from the report. */\nexport interface AutomatedLaneResult {\n status: QaAutomatedStatus\n /** Last result's `startTime` (ISO 8601), if any. */\n at?: string\n /** Provenance summary for the tooltip — e.g. \"laptop · clone · retried\". */\n provenance?: string\n}\n\ninterface RawResult {\n status?: string\n retry?: number\n startTime?: string\n}\ninterface RawTest {\n results?: RawResult[]\n}\ninterface RawSpec {\n file?: string\n title?: string\n tests?: RawTest[]\n}\ninterface RawSuite {\n file?: string\n specs?: RawSpec[]\n suites?: RawSuite[]\n}\ninterface RawReport {\n suites?: RawSuite[]\n}\n\n/** Provenance summary from runner env vars + whether the test was retried. */\nfunction provenanceFor(retried: boolean): string | undefined {\n const parts: string[] = []\n const runner = process.env.QA_RUNNER\n const database = process.env.QA_DATABASE\n const authMode = process.env.QA_AUTH_MODE\n if (runner) parts.push(runner)\n if (database) parts.push(database)\n if (authMode) parts.push(authMode)\n if (retried) parts.push('retried')\n return parts.length > 0 ? parts.join(' · ') : undefined\n}\n\n/**\n * Build the automated lane for one spec test from its `results[]`. The LAST\n * result is authoritative (Playwright appends retries): passed → `passing`,\n * unless it took a retry to get there → `flaky`; otherwise `failing`.\n */\nfunction laneFor(test: RawTest): AutomatedLaneResult {\n const results = test.results ?? []\n if (results.length === 0) {\n return { status: 'never_run' }\n }\n const last = results[results.length - 1]!\n const at = last.startTime\n const retried = (last.retry ?? 0) > 0 || results.length > 1\n let status: QaAutomatedStatus\n if (last.status === 'passed') {\n status = retried ? 'flaky' : 'passing'\n } else {\n status = 'failing'\n }\n const provenance = provenanceFor(retried)\n return { status, ...(at ? { at } : {}), ...(provenance ? { provenance } : {}) }\n}\n\n/**\n * Read a Playwright JSON report into a Map keyed `\"{specFile}::{testTitle}\"`.\n * Walks nested suites recursively. The spec `file` is used verbatim.\n */\nexport function readResults(file: string): Map<string, AutomatedLaneResult> {\n const lanes = new Map<string, AutomatedLaneResult>()\n let report: RawReport\n try {\n report = JSON.parse(readFileSync(file, 'utf8')) as RawReport\n } catch (err) {\n throw new Error(`qa build: failed to read Playwright report ${file}: ${err instanceof Error ? err.message : String(err)}`)\n }\n\n const walk = (suite: RawSuite): void => {\n for (const spec of suite.specs ?? []) {\n const specFile = spec.file ?? suite.file\n if (!specFile || !spec.title) continue\n for (const test of spec.tests ?? []) {\n lanes.set(`${specFile}::${spec.title}`, laneFor(test))\n }\n }\n for (const child of suite.suites ?? []) walk(child)\n }\n for (const suite of report.suites ?? []) walk(suite)\n\n return lanes\n}\n","/**\n * `qa build` — load REAL suites (`suitesDir/`) and project them into per-pattern\n * QA-Meter scenario SEEDS (one instance per case × applicable page). Ported from\n * the Vigilance donor `scripts/load-suites.mjs` to TypeScript against the shared\n * `suite-format.ts` types.\n *\n * Sources discovered (no hard-coded list — drop a file in, it is ingested):\n * - module suites: `suitesDir/modules/<section>/<section>-test-suite.yaml`\n * - generated suites: `suitesDir/generated/*.yaml` (treated as module suites)\n * - cross-module suites: `*-test-suite.yaml` at the root of `suitesDir`.\n *\n * `page_sets` and `applies_to` are expanded; `traverses` is resolved; each (case\n * × applicable page) becomes one seed attached to that page's pattern — the\n * coverage \"numerator\". HONESTY: we do NOT fabricate a run result here. A seed\n * carries the case metadata + automation LINK; the build merges the actual run\n * lane (or `never_run`) and computes colour.\n */\n\nimport { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'\nimport { join } from 'node:path'\nimport { parse } from 'yaml'\nimport type { SuiteDoc, SuiteTestCase, SuitePage } from './suite-format'\nimport type { QaCaseStatus, QaGrounding, QaPriority, QaScenarioPageRef } from './artifact-types'\n\n/**\n * A per-pattern scenario SEED: everything the build needs to assemble a\n * `QaScenario` EXCEPT the run-derived lanes and the colour.\n */\nexport interface ScenarioSeed {\n id: string\n external_key: string\n title: string\n source: 'reporter' | 'manual'\n /** Spec path that covers this case (for automation-lane lookup), if any. */\n spec_path?: string\n /** The spec's `test()` title used for the automation-lane lookup, if any. */\n test_title?: string\n group: { key: string; label: string }\n priority?: QaPriority\n caseStatus?: QaCaseStatus\n grounding?: QaGrounding\n /** This seed's pages: the current pattern primary + traversed refs. */\n pages: QaScenarioPageRef[]\n /** The pattern this seed is attached to (its primary page). */\n pattern: string\n /** `authored` | `generated` — origin of the donor suite (for dedup precedence). */\n origin: 'authored' | 'generated'\n /** Dedup key: `{spec_path}::{test_title}` when both are present, else undefined. */\n dedupKey?: string\n}\n\n/** Module suite, parsed into the few fields the projection needs. */\ninterface ModuleSuite {\n section: string\n pages: Record<string, SuitePage>\n pageSets: Record<string, string[]>\n labels: Record<string, string>\n cases: SuiteTestCase[]\n origin: 'authored' | 'generated'\n}\n\n/** Auto-discover module suites under `suitesDir/modules/<section>/`. */\nfunction discoverModuleSuites(suitesDir: string): string[] {\n const modulesDir = join(suitesDir, 'modules')\n if (!existsSync(modulesDir)) return []\n const out: string[] = []\n for (const name of readdirSync(modulesDir).sort()) {\n const sub = join(modulesDir, name)\n if (!statSync(sub).isDirectory()) continue\n const file = readdirSync(sub).find((f) => f.endsWith('-test-suite.yaml'))\n if (file) out.push(join(sub, file))\n }\n return out\n}\n\n/** Auto-discover generated suites under `suitesDir/generated/`. */\nfunction discoverGeneratedSuites(suitesDir: string): string[] {\n const genDir = join(suitesDir, 'generated')\n if (!existsSync(genDir)) return []\n return readdirSync(genDir)\n .filter((f) => f.endsWith('.yaml') || f.endsWith('.yml'))\n .sort()\n .map((f) => join(genDir, f))\n}\n\n/** Auto-discover cross-module suites at the root of `suitesDir`. */\nfunction discoverCrossSuites(suitesDir: string): string[] {\n if (!existsSync(suitesDir)) return []\n return readdirSync(suitesDir)\n .filter((f) => f.endsWith('-test-suite.yaml'))\n .sort()\n .map((f) => join(suitesDir, f))\n}\n\n/** Parse `# ---- Suite X — label (detail) ---- ` headers → { X: 'label' }. */\nfunction parseSuiteLabels(rawText: string): Record<string, string> {\n const labels: Record<string, string> = {}\n const re = /^\\s*#\\s*-+\\s*Suite\\s+(\\S+)\\s*[—–-]\\s*(.+?)\\s*-*\\s*$/gm\n let m: RegExpExecArray | null\n while ((m = re.exec(rawText)) !== null) {\n labels[m[1]!] = m[2]!.replace(/\\s*\\([^)]*\\)\\s*$/, '').trim()\n }\n return labels\n}\n\n/** Resolve `applies_to` (page_set name | list of route keys) to a list of route keys. */\nfunction expandRoutes(value: string[] | string | undefined, pageSets: Record<string, string[]>): string[] {\n if (!value) return []\n if (typeof value === 'string') return pageSets[value] ?? [value]\n return value.flatMap((entry) => (pageSets[entry] ? pageSets[entry]! : [entry]))\n}\n\n/** A case is `covered` iff its declared status is `covered`. */\nfunction caseStatusOf(testCase: SuiteTestCase): QaCaseStatus {\n const s = testCase.status\n if (s === 'covered' || s === 'manual' || s === 'blocked' || s === 'open-question') return s\n // Generated cases default to `covered`; authored cases without status → manual.\n return 'manual'\n}\n\n/**\n * Load every discoverable suite into per-pattern scenario seeds.\n * @returns `{ byPattern, coveredPatterns }` — seeds keyed by primary pattern, and\n * the set of patterns that have at least one `covered` case.\n */\nexport function loadScenarioSeeds(suitesDir: string): {\n byPattern: Map<string, ScenarioSeed[]>\n coveredPatterns: Set<string>\n} {\n const modules: Record<string, ModuleSuite> = {}\n const byPattern = new Map<string, ScenarioSeed[]>()\n const coveredPatterns = new Set<string>()\n\n const push = (pattern: string, seed: ScenarioSeed): void => {\n if (!pattern) return\n const list = byPattern.get(pattern)\n if (list) list.push(seed)\n else byPattern.set(pattern, [seed])\n }\n\n // ── 1. Module suites (authored) + generated suites ─────────────────────────\n const moduleFiles: Array<{ path: string; origin: 'authored' | 'generated' }> = [\n ...discoverModuleSuites(suitesDir).map((path) => ({ path, origin: 'authored' as const })),\n ...discoverGeneratedSuites(suitesDir).map((path) => ({ path, origin: 'generated' as const })),\n ]\n\n for (const { path, origin } of moduleFiles) {\n const raw = readFileSync(path, 'utf8')\n const doc = parse(raw) as SuiteDoc | undefined\n if (!doc || !doc.meta) continue\n const section = doc.meta.section ?? path\n // A generated suite for an already-authored section MERGES into it (the\n // authored registry/labels win); cases accumulate so dedup can run later.\n const existing = modules[section]\n if (existing) {\n existing.cases.push(...(doc.test_cases ?? []))\n // Authored origin dominates if mixed.\n if (origin === 'authored') existing.origin = 'authored'\n } else {\n modules[section] = {\n section,\n pages: doc.pages ?? {},\n pageSets: doc.page_sets ?? {},\n labels: parseSuiteLabels(raw),\n cases: [...(doc.test_cases ?? [])],\n origin,\n }\n }\n // Merge page registry: a generated suite may register pages the authored one\n // lacks (e.g. /widgets/:widgetId/edit) — keep authored entries on conflict.\n if (existing) {\n for (const [key, page] of Object.entries(doc.pages ?? {})) {\n if (!(key in existing.pages)) existing.pages[key] = page\n }\n for (const [key, set] of Object.entries(doc.page_sets ?? {})) {\n if (!(key in existing.pageSets)) existing.pageSets[key] = set\n }\n }\n }\n\n // Track per-case origin so we can stamp seeds. A case's origin is the origin\n // of the suite it came from; but after the merge above we lost that. Re-derive\n // by re-reading: simpler — re-walk each file independently for seed emission.\n for (const { path, origin } of moduleFiles) {\n const raw = readFileSync(path, 'utf8')\n const doc = parse(raw) as SuiteDoc | undefined\n if (!doc || !doc.meta) continue\n const section = doc.meta.section ?? path\n const mod = modules[section]!\n const labels = mod.labels\n const localPages = { ...mod.pages, ...(doc.pages ?? {}) }\n const localPageSets = { ...mod.pageSets, ...(doc.page_sets ?? {}) }\n\n for (const testCase of doc.test_cases ?? []) {\n const routes = expandRoutes(testCase.applies_to, localPageSets)\n const traversed = (testCase.traverses ?? []).map((t) => t.pattern).filter(Boolean)\n const groupKey = testCase.suite ?? '?'\n const label = labels[groupKey] ?? `Suite ${groupKey}`\n const status = caseStatusOf(testCase)\n const specFallback = routes.length > 0 ? localPages[routes[0]!]?.spec : undefined\n const specPath = testCase.automation?.spec ?? specFallback\n const testTitle = testCase.automation?.test_title ?? localPages[routes[0]!]?.test_title\n const dedupKey = specPath && testTitle ? `${specPath}::${testTitle}` : undefined\n // An authored case linked to a Playwright test DISPLAYS the canonical test\n // title (the verbatim `test()` name) — it represents the SAME scenario as\n // its generated twin, which dedup will suppress. The rich human metadata\n // (priority, grounding, steps…) still comes from the authored case.\n const displayTitle =\n origin === 'authored' && testCase.automation?.type === 'playwright' && testCase.automation?.test_title\n ? testCase.automation.test_title\n : testCase.title\n\n for (const route of routes) {\n const page = localPages[route]\n const pattern = page?.pattern\n if (!pattern) continue\n if (status === 'covered') coveredPatterns.add(pattern)\n push(pattern, {\n id: `s-${section}-${testCase.id}-${route}`,\n external_key: testCase.external_key ?? `${section}#${testCase.id}`,\n title: displayTitle,\n source: testCase.automation?.type === 'playwright' ? 'reporter' : 'manual',\n ...(specPath ? { spec_path: specPath } : {}),\n ...(testTitle ? { test_title: testTitle } : {}),\n group: { key: groupKey, label },\n ...(testCase.priority ? { priority: testCase.priority as QaPriority } : {}),\n caseStatus: status,\n ...(testCase.grounding ? { grounding: testCase.grounding as QaGrounding } : {}),\n pages: [\n { pattern, role: 'primary' },\n ...traversed.map((p) => ({ pattern: p, role: 'traversed' as const })),\n ],\n pattern,\n origin,\n ...(dedupKey ? { dedupKey } : {}),\n })\n }\n }\n }\n\n // ── 2. Cross-module suites (TC-X… cases, attached to BOTH modules' pages) ───\n for (const crossPath of discoverCrossSuites(suitesDir)) {\n const raw = readFileSync(crossPath, 'utf8')\n const doc = parse(raw) as\n | (SuiteDoc & { meta?: { kind?: string; pair?: string[] }; test_cases?: CrossCase[] })\n | undefined\n if (!doc || doc.meta?.kind !== 'cross-module') continue\n const pair = doc.meta?.pair ?? []\n const crossLabel = pair.length ? `Inter-modules (${pair.join(' × ')})` : 'Inter-modules'\n for (const testCase of doc.test_cases ?? []) {\n const refs: QaScenarioPageRef[] = []\n for (const touch of testCase.touches ?? []) {\n const mod = modules[touch.module]\n if (!mod) continue\n const routes = touch.route ? [touch.route] : expandRoutes(touch.page_set, mod.pageSets)\n for (const route of routes) {\n const pattern = mod.pages[route]?.pattern\n if (pattern) refs.push({ pattern, role: touch.role === 'origin' ? 'primary' : 'traversed' })\n }\n }\n const patterns = [...new Set(refs.map((r) => r.pattern))]\n const status = caseStatusOf(testCase as unknown as SuiteTestCase)\n for (const pattern of patterns) {\n push(pattern, {\n id: `s-cross-${testCase.id}-${pattern}`,\n external_key: `cross#${testCase.id}`,\n title: testCase.title,\n source: 'manual',\n group: { key: 'X', label: crossLabel },\n ...(testCase.priority ? { priority: testCase.priority as QaPriority } : {}),\n caseStatus: status,\n ...(testCase.grounding ? { grounding: testCase.grounding as QaGrounding } : {}),\n pages: [{ pattern, role: 'primary' }, ...refs.filter((r) => r.pattern !== pattern)],\n pattern,\n origin: 'authored',\n })\n }\n }\n }\n\n return { byPattern, coveredPatterns }\n}\n\n/** A cross-module case (`touches[]` instead of `applies_to`). */\ninterface CrossCase {\n id: string\n title: string\n status?: string\n priority?: string\n grounding?: string\n touches?: Array<{ module: string; route?: string; page_set?: string; role?: string }>\n}\n"],"mappings":"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","names":["existsSync","readFileSync","readFileSync","existsSync","readFileSync"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/qa/check.ts"],"sourcesContent":["/**\n * `qa check` — the CI tripwire for the QA-Meter inputs (design spec §7.4).\n *\n * Validates that the hand-authored suites, the generated suites, the frozen\n * sitemap and the dev-validations ledger are mutually CONSISTENT and have not\n * DRIFTED. Five rules (§7.4):\n *\n * 1. pattern-not-in-sitemap — every referenced page pattern exists in the sitemap.\n * 2. generated-stale — on-disk generated suites match a fresh in-memory run.\n * 3. unmappable-test (+ zero-case / dynamic-title) — nothing vanishes silently.\n * 4. dev-validation-append-only — the dev-validations ledger is append-only vs HEAD.\n * 5. dedup-collision — no two AUTHORED cases share a dedup key.\n *\n * Pure rule helpers (`checkPatternsInSitemap`, `checkDedupCollisions`, …) are\n * exported so they can be unit-tested with synthetic inputs.\n */\nimport { execFileSync } from 'node:child_process'\nimport { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'\nimport { dirname, join, relative } from 'node:path'\nimport { parse } from 'yaml'\n\nimport { loadQaConfig, type QaConfig } from './config'\nimport { generateSuites, serializeSuite } from './generate'\nimport type { SuiteDoc } from './suite-format'\n\n/** A single check outcome. */\nexport interface Finding {\n rule: string\n level: 'error' | 'warning'\n message: string\n}\n\n/** A suite document paired with its on-disk origin. */\ninterface LoadedSuite {\n doc: SuiteDoc\n origin: 'authored' | 'generated'\n /** Human label for messages (the file or section). */\n label: string\n}\n\n// ───────────────────────────────────────────── suite discovery (read-only) ──\n\n/** Read + parse a `*-test-suite.yaml` into a `SuiteDoc`, or null if unusable. */\nfunction parseSuiteDoc(path: string): SuiteDoc | null {\n try {\n const doc = parse(readFileSync(path, 'utf8')) as SuiteDoc | undefined\n if (!doc || !doc.meta) return null\n return doc\n } catch {\n return null\n }\n}\n\n/**\n * Load every authored module suite (`suitesDir/modules/<section>/`) and every\n * generated suite (`suitesDir/generated/`). Mirrors `load-suites` discovery but\n * keeps the raw `SuiteDoc` + origin (the check needs page registries and\n * per-case automation keys, which the seed projection flattens away).\n */\nfunction loadSuiteDocs(suitesDir: string): LoadedSuite[] {\n const out: LoadedSuite[] = []\n\n const modulesDir = join(suitesDir, 'modules')\n if (existsSync(modulesDir)) {\n for (const name of readdirSync(modulesDir).sort()) {\n const sub = join(modulesDir, name)\n if (!statSync(sub).isDirectory()) continue\n const file = readdirSync(sub).find((f) => f.endsWith('-test-suite.yaml'))\n if (!file) continue\n const doc = parseSuiteDoc(join(sub, file))\n if (doc) out.push({ doc, origin: 'authored', label: doc.meta.section ?? name })\n }\n }\n\n const genDir = join(suitesDir, 'generated')\n if (existsSync(genDir)) {\n for (const f of readdirSync(genDir).filter((x) => x.endsWith('.yaml') || x.endsWith('.yml')).sort()) {\n const doc = parseSuiteDoc(join(genDir, f))\n if (doc) out.push({ doc, origin: 'generated', label: doc.meta.section ?? f })\n }\n }\n\n // Cross-module suites at the root reference module pages, not their own — they\n // don't introduce new patterns to validate here, so they are skipped.\n return out\n}\n\n/** Every (pattern, suiteLabel) pair referenced by a suite's pages registry. */\nfunction referencedPatterns(suite: LoadedSuite): Array<{ pattern: string; suite: string }> {\n const out: Array<{ pattern: string; suite: string }> = []\n for (const page of Object.values(suite.doc.pages ?? {})) {\n if (page?.pattern) out.push({ pattern: page.pattern, suite: suite.label })\n }\n for (const tc of suite.doc.test_cases ?? []) {\n for (const t of tc.traverses ?? []) {\n if (t?.pattern) out.push({ pattern: t.pattern, suite: suite.label })\n }\n }\n return out\n}\n\n// ─────────────────────────────────────────────────── rule 1: sitemap ────────\n\n/**\n * RULE 1 — every page pattern referenced by a loaded suite must be a known\n * `path_pattern` in the sitemap. Exported for unit testing with synthetic docs.\n */\nexport function checkPatternsInSitemap(\n suites: Array<SuiteDoc | LoadedSuite>,\n sitemapPatterns: Set<string>,\n): Finding[] {\n const findings: Finding[] = []\n const seen = new Set<string>()\n for (const s of suites) {\n const loaded: LoadedSuite =\n 'doc' in s ? s : { doc: s, origin: s.meta.origin ?? 'authored', label: s.meta.section ?? 'suite' }\n for (const { pattern, suite } of referencedPatterns(loaded)) {\n if (sitemapPatterns.has(pattern)) continue\n const dedup = `${suite}::${pattern}`\n if (seen.has(dedup)) continue\n seen.add(dedup)\n findings.push({\n rule: 'pattern-not-in-sitemap',\n level: 'error',\n message: `pattern ${pattern} (suite ${suite}) is not present in the sitemap`,\n })\n }\n }\n return findings\n}\n\n/** Load the configured sitemap's `path_pattern` set, or null when absent. */\nfunction loadSitemapPatterns(cfg: QaConfig): Set<string> | null {\n if (!cfg.sitemap || !cfg.sitemap.file || !existsSync(cfg.sitemap.file)) return null\n try {\n const parsed = JSON.parse(readFileSync(cfg.sitemap.file, 'utf8')) as {\n pages?: Array<{ path_pattern?: string }>\n }\n const set = new Set<string>()\n for (const p of parsed.pages ?? []) {\n if (typeof p.path_pattern === 'string') set.add(p.path_pattern)\n }\n return set\n } catch {\n return null\n }\n}\n\n// ─────────────────────────────────────────────── rule 2: generated-stale ────\n\n/**\n * RULE 2 — re-generate suites in memory and compare each against the on-disk\n * `generated/<section>-test-suite.yaml`. Any difference, missing or extra file\n * means the committed generated suites are stale.\n */\nfunction checkGeneratedStale(cfg: QaConfig): Finding[] {\n const findings: Finding[] = []\n const genDir = join(cfg.suitesDir, 'generated')\n const { suites } = generateSuites(cfg)\n\n const expectedFiles = new Map<string, string>()\n for (const [section, doc] of suites) {\n expectedFiles.set(`${section}-test-suite.yaml`, serializeSuite(doc))\n }\n\n if (!existsSync(genDir)) {\n if (expectedFiles.size > 0) {\n findings.push({\n rule: 'generated-stale',\n level: 'error',\n message:\n 'generated suites out of date — `generated/` is missing; run `mhosaic-feedback qa generate`',\n })\n }\n return findings\n }\n\n // Compare expected files against disk.\n for (const [name, content] of expectedFiles) {\n const path = join(genDir, name)\n if (!existsSync(path)) {\n findings.push({\n rule: 'generated-stale',\n level: 'error',\n message: `generated suites out of date — ${name} is missing; run \\`mhosaic-feedback qa generate\\``,\n })\n continue\n }\n if (readFileSync(path, 'utf8') !== content) {\n findings.push({\n rule: 'generated-stale',\n level: 'error',\n message: `generated suites out of date — ${name} differs; run \\`mhosaic-feedback qa generate\\``,\n })\n }\n }\n\n // Extra generated files that the generator no longer produces are also stale.\n const onDisk = readdirSync(genDir).filter((f) => f.endsWith('-test-suite.yaml'))\n for (const name of onDisk) {\n if (!expectedFiles.has(name)) {\n findings.push({\n rule: 'generated-stale',\n level: 'error',\n message: `generated suites out of date — ${name} is no longer generated; run \\`mhosaic-feedback qa generate\\``,\n })\n }\n }\n\n return findings\n}\n\n// ─────────────────────────────────────────────── rule 5: dedup-collision ────\n\n/**\n * RULE 5 — two AUTHORED cases that resolve to the SAME dedup key\n * `(automation.spec ?? pages[firstRoute].spec) :: automation.test_title` would\n * make dedup ambiguous. Generated-vs-authored matches are NORMAL dedup (not a\n * collision) — only authored-vs-authored same-key is flagged. Exported for unit\n * testing.\n */\nexport function checkDedupCollisions(suites: Array<SuiteDoc | LoadedSuite>): Finding[] {\n const findings: Finding[] = []\n const seen = new Map<string, string>() // key → first case description\n\n for (const s of suites) {\n const loaded: LoadedSuite =\n 'doc' in s ? s : { doc: s, origin: s.meta.origin ?? 'authored', label: s.meta.section ?? 'suite' }\n if (loaded.origin !== 'authored') continue\n\n const pages = loaded.doc.pages ?? {}\n for (const tc of loaded.doc.test_cases ?? []) {\n const firstRoute = tc.applies_to?.[0]\n const spec = tc.automation?.spec ?? (firstRoute ? pages[firstRoute]?.spec : undefined)\n const title = tc.automation?.test_title\n if (!spec || !title) continue // no dedup key → cannot collide\n const key = `${spec}::${title}`\n const here = `${loaded.label}/${tc.id}`\n const prior = seen.get(key)\n if (prior) {\n findings.push({\n rule: 'dedup-collision',\n level: 'error',\n message: `duplicate authored dedup key ${key} — cases ${prior} and ${here} collide`,\n })\n } else {\n seen.set(key, here)\n }\n }\n }\n return findings\n}\n\n// ─────────────────────────────────────── rule 4: dev-validation append-only ─\n\n/** Identity of a dev-validation entry (best-effort): its `external_key`/`id`, else JSON. */\nfunction entryIdentity(entry: unknown): string {\n if (entry && typeof entry === 'object') {\n const o = entry as Record<string, unknown>\n for (const k of ['external_key', 'externalKey', 'id', 'key']) {\n if (typeof o[k] === 'string') return `${k}:${o[k]}`\n }\n }\n return JSON.stringify(entry)\n}\n\n/**\n * RULE 4 — the dev-validations file (a JSON array) must be append-only relative\n * to HEAD: every entry present at HEAD must still be present AND unchanged in the\n * working copy. SKIPS (no error) when the file or git is absent, or the file is\n * newly added (not at HEAD).\n */\nfunction checkDevValidationAppendOnly(cfg: QaConfig): Finding[] {\n const file = cfg.devValidationsFile\n if (!file || !existsSync(file)) {\n return [\n {\n rule: 'dev-validation-append-only',\n level: 'warning',\n message: 'skipped — no dev-validations file configured/present',\n },\n ]\n }\n\n // Locate the repo root (git toplevel) for `git show HEAD:<rel>`.\n let repoRoot: string\n try {\n repoRoot = execFileSync('git', ['rev-parse', '--show-toplevel'], {\n cwd: dirname(file),\n encoding: 'utf8',\n }).trim()\n } catch {\n return [\n {\n rule: 'dev-validation-append-only',\n level: 'warning',\n message: 'skipped — not a git repository',\n },\n ]\n }\n\n const rel = relative(repoRoot, file).split('\\\\').join('/')\n\n let headText: string\n try {\n headText = execFileSync('git', ['show', `HEAD:${rel}`], { cwd: repoRoot, encoding: 'utf8' })\n } catch {\n // File not at HEAD = newly added = fine (append-only trivially holds).\n return [\n {\n rule: 'dev-validation-append-only',\n level: 'warning',\n message: 'skipped — dev-validations file is new (not at HEAD)',\n },\n ]\n }\n\n let headArr: unknown[]\n let workArr: unknown[]\n try {\n headArr = JSON.parse(headText)\n workArr = JSON.parse(readFileSync(file, 'utf8'))\n } catch {\n return [\n {\n rule: 'dev-validation-append-only',\n level: 'warning',\n message: 'skipped — dev-validations file is not valid JSON at HEAD or in working copy',\n },\n ]\n }\n if (!Array.isArray(headArr) || !Array.isArray(workArr)) {\n return [\n {\n rule: 'dev-validation-append-only',\n level: 'warning',\n message: 'skipped — dev-validations file is not a JSON array',\n },\n ]\n }\n\n // Build an identity → serialized-entry map of the working copy. Every HEAD\n // entry must still be present (by identity) AND byte-identical (no mutation).\n const work = new Map<string, string>()\n for (const e of workArr) work.set(entryIdentity(e), JSON.stringify(e))\n\n const findings: Finding[] = []\n for (const headEntry of headArr) {\n const id = entryIdentity(headEntry)\n const headSer = JSON.stringify(headEntry)\n if (!work.has(id)) {\n findings.push({\n rule: 'dev-validation-append-only',\n level: 'error',\n message: `dev-validation entry removed (not append-only): ${id}`,\n })\n } else if (work.get(id) !== headSer) {\n findings.push({\n rule: 'dev-validation-append-only',\n level: 'error',\n message: `dev-validation entry mutated (not append-only): ${id}`,\n })\n }\n }\n return findings\n}\n\n// ──────────────────────────────────────────────────── orchestration ─────────\n\n/**\n * A finding is INFORMATIONAL (a SKIP note, not a real warning) when a rule could\n * not run — e.g. no sitemap configured, no dev-validations ledger. These must\n * never fail the build, even under `--strict`.\n */\nexport function isInformational(f: Finding): boolean {\n return f.level === 'warning' && f.message.startsWith('skipped')\n}\n\n/** Run all five §7.4 rules and return every finding. */\nexport function runChecks(cfg: QaConfig, opts: { strict?: boolean } = {}): Finding[] {\n const findings: Finding[] = []\n const warnLevel: Finding['level'] = opts.strict ? 'error' : 'warning'\n\n const suites = loadSuiteDocs(cfg.suitesDir)\n\n // Rule 1 — pattern-not-in-sitemap.\n const sitemapPatterns = loadSitemapPatterns(cfg)\n if (sitemapPatterns === null) {\n findings.push({\n rule: 'pattern-not-in-sitemap',\n level: 'warning',\n message: 'skipped — no sitemap configured/present (meter runs in degraded flat mode)',\n })\n } else {\n findings.push(...checkPatternsInSitemap(suites, sitemapPatterns))\n }\n\n // Rule 2 — generated-stale.\n findings.push(...checkGeneratedStale(cfg))\n\n // Rule 3 — unmappable / zero-case / dynamic-title (warning, or error under --strict).\n const { unmappable, zeroCaseSpecs, dynamicTitles } = generateSuites(cfg)\n for (const entry of unmappable) {\n findings.push({ rule: 'unmappable-test', level: warnLevel, message: `unmappable — ${entry}` })\n }\n for (const spec of zeroCaseSpecs) {\n findings.push({ rule: 'zero-case-spec', level: warnLevel, message: `zero-case spec — ${spec}` })\n }\n for (const entry of dynamicTitles) {\n findings.push({ rule: 'dynamic-title-spec', level: warnLevel, message: `dynamic-title — ${entry}` })\n }\n\n // Rule 4 — dev-validation append-only.\n findings.push(...checkDevValidationAppendOnly(cfg))\n\n // Rule 5 — dedup-collision.\n findings.push(...checkDedupCollisions(suites))\n\n return findings\n}\n\n// ─────────────────────────────────────────────────────── CLI entry ──────────\n\n/** Lazily load `kleur` for colored output (mirrors `commands/doctor.ts`). */\nasync function colors(): Promise<{\n red: (s: string) => string\n yellow: (s: string) => string\n green: (s: string) => string\n gray: (s: string) => string\n bold: (s: string) => string\n}> {\n const k = (await import('kleur')).default\n return {\n red: (s) => k.red(s),\n yellow: (s) => k.yellow(s),\n green: (s) => k.green(s),\n gray: (s) => k.gray(s),\n bold: (s) => k.bold(s),\n }\n}\n\nexport async function runCheck(args: string[]): Promise<void> {\n const strict = args.includes('--strict')\n const cfg = loadQaConfig(process.cwd())\n const findings = runChecks(cfg, { strict })\n const c = await colors()\n\n if (findings.length === 0) {\n process.stdout.write(`${c.green('✓')} qa check: all checks passed\\n`)\n return\n }\n\n // Group by rule for a clean, scannable report.\n const byRule = new Map<string, Finding[]>()\n for (const f of findings) {\n const list = byRule.get(f.rule)\n if (list) list.push(f)\n else byRule.set(f.rule, [f])\n }\n\n let hasError = false\n let hasActionableWarning = false\n for (const rule of [...byRule.keys()].sort((a, b) => a.localeCompare(b))) {\n process.stdout.write(`${c.bold(rule)}\\n`)\n for (const f of byRule.get(rule)!) {\n if (f.level === 'error') hasError = true\n // Informational skip notes (no sitemap, no ledger, …) never fail the build.\n else if (!isInformational(f)) hasActionableWarning = true\n const icon = f.level === 'error' ? c.red('✗') : c.yellow('!')\n process.stdout.write(` ${icon} ${f.message}\\n`)\n }\n }\n\n // Exit 1 on any error, or any actionable warning under --strict.\n if (hasError || (strict && hasActionableWarning)) {\n process.exitCode = 1\n }\n}\n"],"mappings":"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","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/qa/sitemap-core.ts"],"sourcesContent":["/**\n * Logique pure de construction du sitemap (Track A3).\n *\n * Transforme l'arbre de routes Vue (`router.options.routes` / la constante\n * `routes`) en le payload de sync doc 03 §3.1 — agnostique de la manière dont\n * les routes ont été chargées (Vite runtime, ou lien statique des modules).\n * Aucune dépendance runtime : se teste à la table (vitest) et se réutilise tel\n * quel quand l'export alimentera le CLI plateforme `mhosaic-feedback sitemap sync`.\n */\n\n/** Forme minimale d'un `RouteRecordRaw` dont on a besoin (structurellement compatible). */\nexport interface RawRoute {\n path?: string;\n name?: string | symbol;\n redirect?: unknown;\n children?: ReadonlyArray<RawRoute>;\n}\n\n/** Un nœud de page émis (doc 03 §3.1). */\nexport interface SitemapPage {\n path_pattern: string;\n route_name?: string;\n title?: string;\n parent: string | null;\n order: number;\n structural?: boolean;\n}\n\nexport interface SitemapPayload {\n format: 1;\n pages: SitemapPage[];\n}\n\n/** Normalisation canonique : `/` initial, pas de `/` final (sauf racine), slashes dédoublés. */\nexport function normalizePattern(pattern: string | undefined | null): string {\n if (!pattern) {\n return '/';\n }\n let normalized = pattern.startsWith('/') ? pattern : `/${pattern}`;\n normalized = normalized.replace(/\\/{2,}/g, '/');\n if (normalized.length > 1) {\n normalized = normalized.replace(/\\/+$/, '');\n }\n return normalized.length > 0 ? normalized : '/';\n}\n\n/** Joint un pattern parent absolu et un segment enfant (le segment peut être absolu). */\nfunction joinPattern(parentAbs: string, childPath: string): string {\n if (!childPath) {\n return parentAbs || '/';\n }\n if (childPath.startsWith('/')) {\n return normalizePattern(childPath);\n }\n const base = parentAbs === '/' ? '' : parentAbs;\n return normalizePattern(`${base}/${childPath}`);\n}\n\n/** Une route catch-all (`/:pathMatch(.*)*`) n'est pas une page. */\nfunction isCatchAll(path: string | undefined): boolean {\n return !!path && /\\(\\.\\*\\)/.test(path);\n}\n\nexport interface BuildOptions {\n /** Résout un titre humain depuis le nom de route et/ou le pattern. */\n titleFor?: (routeName: string | undefined, pattern: string) => string | undefined;\n}\n\n/**\n * Construit le payload de sitemap.\n *\n * Règles (doc 03 §3.1) :\n * - records redirect-only (redirect sans enfants) → sautés (pur alias) ;\n * - catch-all → sauté ;\n * - segments intermédiaires sans `name` → émis comme nœuds `structural` (arbre sans orphelins) ;\n * - un enfant à `path: ''` partage le pattern du parent : on FUSIONNE (le nom/titre concret\n * l'emporte sur le wrapper structurel — c'est le patron Vue « segment + enfant nommé sans path »).\n */\nexport function buildSitemap(routes: ReadonlyArray<RawRoute>, options: BuildOptions = {}): SitemapPayload {\n const titleFor = options.titleFor ?? (() => undefined);\n const pages: SitemapPage[] = [];\n const byPattern = new Map<string, SitemapPage>();\n\n const visit = (records: ReadonlyArray<RawRoute>, parentAbs: string, parentEmitted: string | null): void => {\n records.forEach((record, index) => {\n const hasChildren = Array.isArray(record.children) && record.children.length > 0;\n const isRedirect = record.redirect != null;\n if (isRedirect && !hasChildren) {\n return; // alias de navigation pur\n }\n if (isCatchAll(record.path)) {\n return;\n }\n\n const abs = joinPattern(parentAbs, record.path ?? '');\n const routeName = typeof record.name === 'string' ? record.name : undefined;\n\n const existing = byPattern.get(abs);\n if (existing) {\n // Fusion : le nœud nommé concret écrase le wrapper structurel au même pattern.\n if (routeName && !existing.route_name) {\n existing.route_name = routeName;\n if (!existing.title) {\n const mergedTitle = titleFor(routeName, abs);\n if (mergedTitle) {\n existing.title = mergedTitle;\n }\n }\n delete existing.structural;\n }\n } else {\n const title = titleFor(routeName, abs) ?? undefined;\n const page: SitemapPage = { path_pattern: abs, parent: parentEmitted, order: index };\n if (routeName) {\n page.route_name = routeName;\n } else {\n page.structural = true;\n }\n if (title) {\n page.title = title;\n }\n byPattern.set(abs, page);\n pages.push(page);\n }\n\n if (hasChildren) {\n visit(record.children as ReadonlyArray<RawRoute>, abs, abs);\n }\n });\n };\n\n visit(routes, '', null);\n return { format: 1, pages };\n}\n\n/** Forme minimale d'un item de menu (`src/menu.ts`). */\nexport interface MenuLike {\n label?: string;\n to?: string | { name?: string };\n subItems?: ReadonlyArray<MenuLike>;\n megaMenu?: ReadonlyArray<MenuLike>;\n}\n\nfunction resolveMessage(messages: unknown, dottedKey: string): string | undefined {\n let current: unknown = messages;\n for (const part of dottedKey.split('.')) {\n if (current == null || typeof current !== 'object' || !(part in (current as Record<string, unknown>))) {\n return undefined;\n }\n current = (current as Record<string, unknown>)[part];\n }\n return typeof current === 'string' ? current : undefined;\n}\n\n/**\n * Construit un résolveur de titres depuis le méga-menu (`menu.ts`, qui mappe les\n * routes → clés `pages.*`) et les messages (`fr.json`). Ne remplit que ce qu'il\n * trouve ; les pages sans entrée de menu restent sans titre (curées plus tard,\n * comme côté plateforme — doc 03 §1.2 : « le sync ne remplit que les titres vides »).\n */\nexport function buildTitleResolver(menuItems: ReadonlyArray<MenuLike>, messages: unknown): (routeName: string | undefined, pattern: string) => string | undefined {\n const labelByName = new Map<string, string>();\n const labelByPattern = new Map<string, string>();\n\n const walk = (items: ReadonlyArray<MenuLike>): void => {\n for (const item of items) {\n if (item.label && item.to) {\n if (typeof item.to === 'string') {\n labelByPattern.set(normalizePattern(item.to), item.label);\n } else if (item.to.name) {\n labelByName.set(item.to.name, item.label);\n }\n }\n if (item.subItems) {\n walk(item.subItems);\n }\n if (item.megaMenu) {\n walk(item.megaMenu);\n }\n }\n };\n walk(menuItems);\n\n return (routeName, pattern) => {\n const key = (routeName && labelByName.get(routeName)) || labelByPattern.get(normalizePattern(pattern));\n return key ? resolveMessage(messages, key) : undefined;\n };\n}\n"],"mappings":";;;AAkCO,SAAS,iBAAiB,SAA4C;AAC3E,MAAI,CAAC,SAAS;AACZ,WAAO;AAAA,EACT;AACA,MAAI,aAAa,QAAQ,WAAW,GAAG,IAAI,UAAU,IAAI,OAAO;AAChE,eAAa,WAAW,QAAQ,WAAW,GAAG;AAC9C,MAAI,WAAW,SAAS,GAAG;AACzB,iBAAa,WAAW,QAAQ,QAAQ,EAAE;AAAA,EAC5C;AACA,SAAO,WAAW,SAAS,IAAI,aAAa;AAC9C;AAGA,SAAS,YAAY,WAAmB,WAA2B;AACjE,MAAI,CAAC,WAAW;AACd,WAAO,aAAa;AAAA,EACtB;AACA,MAAI,UAAU,WAAW,GAAG,GAAG;AAC7B,WAAO,iBAAiB,SAAS;AAAA,EACnC;AACA,QAAM,OAAO,cAAc,MAAM,KAAK;AACtC,SAAO,iBAAiB,GAAG,IAAI,IAAI,SAAS,EAAE;AAChD;AAGA,SAAS,WAAW,MAAmC;AACrD,SAAO,CAAC,CAAC,QAAQ,WAAW,KAAK,IAAI;AACvC;AAiBO,SAAS,aAAa,QAAiC,UAAwB,CAAC,GAAmB;AACxG,QAAM,WAAW,QAAQ,aAAa,MAAM;AAC5C,QAAM,QAAuB,CAAC;AAC9B,QAAM,YAAY,oBAAI,IAAyB;AAE/C,QAAM,QAAQ,CAAC,SAAkC,WAAmB,kBAAuC;AACzG,YAAQ,QAAQ,CAAC,QAAQ,UAAU;AACjC,YAAM,cAAc,MAAM,QAAQ,OAAO,QAAQ,KAAK,OAAO,SAAS,SAAS;AAC/E,YAAM,aAAa,OAAO,YAAY;AACtC,UAAI,cAAc,CAAC,aAAa;AAC9B;AAAA,MACF;AACA,UAAI,WAAW,OAAO,IAAI,GAAG;AAC3B;AAAA,MACF;AAEA,YAAM,MAAM,YAAY,WAAW,OAAO,QAAQ,EAAE;AACpD,YAAM,YAAY,OAAO,OAAO,SAAS,WAAW,OAAO,OAAO;AAElE,YAAM,WAAW,UAAU,IAAI,GAAG;AAClC,UAAI,UAAU;AAEZ,YAAI,aAAa,CAAC,SAAS,YAAY;AACrC,mBAAS,aAAa;AACtB,cAAI,CAAC,SAAS,OAAO;AACnB,kBAAM,cAAc,SAAS,WAAW,GAAG;AAC3C,gBAAI,aAAa;AACf,uBAAS,QAAQ;AAAA,YACnB;AAAA,UACF;AACA,iBAAO,SAAS;AAAA,QAClB;AAAA,MACF,OAAO;AACL,cAAM,QAAQ,SAAS,WAAW,GAAG,KAAK;AAC1C,cAAM,OAAoB,EAAE,cAAc,KAAK,QAAQ,eAAe,OAAO,MAAM;AACnF,YAAI,WAAW;AACb,eAAK,aAAa;AAAA,QACpB,OAAO;AACL,eAAK,aAAa;AAAA,QACpB;AACA,YAAI,OAAO;AACT,eAAK,QAAQ;AAAA,QACf;AACA,kBAAU,IAAI,KAAK,IAAI;AACvB,cAAM,KAAK,IAAI;AAAA,MACjB;AAEA,UAAI,aAAa;AACf,cAAM,OAAO,UAAqC,KAAK,GAAG;AAAA,MAC5D;AAAA,IACF,CAAC;AAAA,EACH;AAEA,QAAM,QAAQ,IAAI,IAAI;AACtB,SAAO,EAAE,QAAQ,GAAG,MAAM;AAC5B;","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/detect.ts"],"sourcesContent":["import { readFile } from 'node:fs/promises'\nimport { join } from 'node:path'\n\nexport type Framework =\n | { kind: 'vite-react'; entry: string }\n | { kind: 'nextjs'; entry: string }\n | { kind: 'remix'; entry: string }\n | { kind: 'cra'; entry: string }\n | { kind: 'plain'; entry: null }\n | { kind: 'unknown'; entry: null }\n\nexport interface PackageShape {\n dependencies?: Record<string, string>\n devDependencies?: Record<string, string>\n}\n\nexport function detectFromPackage(pkg: PackageShape): Framework {\n const deps = { ...(pkg.dependencies ?? {}), ...(pkg.devDependencies ?? {}) }\n if (deps.next) return { kind: 'nextjs', entry: 'app/layout.tsx' }\n if (deps['@remix-run/react']) return { kind: 'remix', entry: 'app/root.tsx' }\n if (deps['react-scripts']) return { kind: 'cra', entry: 'src/index.tsx' }\n if (deps.vite && (deps.react || deps['@vitejs/plugin-react'])) return { kind: 'vite-react', entry: 'src/main.tsx' }\n return { kind: 'unknown', entry: null }\n}\n\nexport async function detectFramework(cwd: string): Promise<Framework> {\n try {\n const raw = await readFile(join(cwd, 'package.json'), 'utf8')\n return detectFromPackage(JSON.parse(raw))\n } catch {\n return { kind: 'plain', entry: null }\n }\n}\n"],"mappings":";;;AAAA,SAAS,gBAAgB;AACzB,SAAS,YAAY;AAed,SAAS,kBAAkB,KAA8B;AAC9D,QAAM,OAAO,EAAE,GAAI,IAAI,gBAAgB,CAAC,GAAI,GAAI,IAAI,mBAAmB,CAAC,EAAG;AAC3E,MAAI,KAAK,KAAM,QAAO,EAAE,MAAM,UAAU,OAAO,iBAAiB;AAChE,MAAI,KAAK,kBAAkB,EAAG,QAAO,EAAE,MAAM,SAAS,OAAO,eAAe;AAC5E,MAAI,KAAK,eAAe,EAAG,QAAO,EAAE,MAAM,OAAO,OAAO,gBAAgB;AACxE,MAAI,KAAK,SAAS,KAAK,SAAS,KAAK,sBAAsB,GAAI,QAAO,EAAE,MAAM,cAAc,OAAO,eAAe;AAClH,SAAO,EAAE,MAAM,WAAW,OAAO,KAAK;AACxC;AAEA,eAAsB,gBAAgB,KAAiC;AACrE,MAAI;AACF,UAAM,MAAM,MAAM,SAAS,KAAK,KAAK,cAAc,GAAG,MAAM;AAC5D,WAAO,kBAAkB,KAAK,MAAM,GAAG,CAAC;AAAA,EAC1C,QAAQ;AACN,WAAO,EAAE,MAAM,SAAS,OAAO,KAAK;AAAA,EACtC;AACF;","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/qa/config.ts"],"sourcesContent":["import { existsSync, readFileSync } from 'node:fs'\nimport { isAbsolute, join, resolve } from 'node:path'\n\n/**\n * Optional sitemap block of the qa-meter config. When absent the meter runs in\n * degraded flat mode (no route rollups). Path fields are absolutized against\n * the config file's directory; `framework` is an adapter name (no path).\n */\nexport interface QaSitemapConfig {\n /** Where the sitemap.json lives (output of `qa sitemap`, or hand-provided). Absolute. */\n file: string\n /** Adapter name for `qa sitemap` (e.g. 'vue-router'); omitted if hand-provided. */\n framework?: string\n /** Directory holding the router declarations. Absolute. */\n routerDir?: string\n /** Optional locale JSON for human titles. Absolute. */\n titlesFrom?: string\n /**\n * Optional import-alias map for the adapter (e.g. `{ '@': 'src' }`). KEYS are\n * kept verbatim; VALUES are absolutized against the config dir (like other\n * path fields) so `@/router/x` resolves to a real file. Default: none.\n */\n alias?: Record<string, string>\n /**\n * react-router adapter only: the file holding the route tree (e.g.\n * `frontend/src/App.tsx` or `frontend/src/router.tsx`). Absolute.\n */\n entry?: string\n /** react-router adapter only: force a reader instead of auto-detecting. */\n routerStyle?: 'jsx' | 'data'\n}\n\n/**\n * Resolved qa-meter config. All filesystem paths are ABSOLUTE (resolved against\n * the directory containing qa-meter.config.json) except `testsGlob` and\n * `fixturesFile`/`pageObjectsDir`, which stay relative to `e2eDir` — they are\n * consumed downstream by joining onto `e2eDir`, so they are kept as-given.\n */\nexport interface QaConfig {\n /** Playwright e2e root. Absolute. */\n e2eDir: string\n /** Suites root ('authored' + 'generated/'). Absolute. */\n suitesDir: string\n /** Where the assembled artifact is written. Absolute. */\n outFile: string\n /** Glob for spec files, relative to e2eDir (kept as a glob string). */\n testsGlob: string\n /** Fixtures file, relative to e2eDir (kept as-given). */\n fixturesFile: string\n /** Page-objects dir, relative to e2eDir (kept as-given). */\n pageObjectsDir: string\n /** Annotation prefix honored in spec headers. */\n annotationPrefix: string\n /** Environment stamped into the artifact. */\n environment: string\n /** App version string, or null (resolved from --version flag downstream). */\n appVersion: string | null\n /** Optional Playwright results file. Absolute, or null. */\n resultsFile: string | null\n /** Optional dev-validations file. Absolute, or null. */\n devValidationsFile: string | null\n /** Optional sitemap block, or null when absent. */\n sitemap: QaSitemapConfig | null\n}\n\nconst CONFIG_NAME = 'qa-meter.config.json'\n\n/** Absolutize `p` against `dir` (already-absolute paths pass through). */\nfunction abs(dir: string, p: string): string {\n return isAbsolute(p) ? p : resolve(join(dir, p))\n}\n\n/** Read a required string field; throw a field-named error if missing/wrong-type. */\nfunction reqString(raw: Record<string, unknown>, field: string): string {\n const v = raw[field]\n if (v === undefined || v === null) throw new Error(`${field} is required in ${CONFIG_NAME}`)\n if (typeof v !== 'string') throw new Error(`${field} must be a string in ${CONFIG_NAME}`)\n return v\n}\n\n/** Read an optional string field with a default; throw a field-named error on wrong type. */\nfunction optString(raw: Record<string, unknown>, field: string, fallback: string): string {\n const v = raw[field]\n if (v === undefined || v === null) return fallback\n if (typeof v !== 'string') throw new Error(`${field} must be a string in ${CONFIG_NAME}`)\n return v\n}\n\n/** Read an optional path field; absolutize if present, else null. Throws on wrong type. */\nfunction optPath(raw: Record<string, unknown>, field: string, dir: string): string | null {\n const v = raw[field]\n if (v === undefined || v === null) return null\n if (typeof v !== 'string') throw new Error(`${field} must be a string in ${CONFIG_NAME}`)\n return abs(dir, v)\n}\n\n/**\n * Read an optional alias map field. KEYS pass through verbatim; VALUES are\n * absolutized against `dir` (like other path fields) so `@` → `<dir>/src`\n * becomes an absolute base for the adapter. Returns null if absent. Throws a\n * field-named error on wrong shape.\n */\nfunction optAlias(\n raw: Record<string, unknown>,\n field: string,\n dir: string,\n): Record<string, string> | null {\n const v = raw[field]\n if (v === undefined || v === null) return null\n if (typeof v !== 'object' || Array.isArray(v)) {\n throw new Error(`${field} must be an object map in ${CONFIG_NAME}`)\n }\n const out: Record<string, string> = {}\n for (const [key, val] of Object.entries(v as Record<string, unknown>)) {\n if (typeof val !== 'string') {\n throw new Error(`${field}.${key} must be a string in ${CONFIG_NAME}`)\n }\n out[key] = abs(dir, val)\n }\n return out\n}\n\n/** Read an optional `routerStyle` ('jsx' | 'data'); null if absent. Throws on bad value. */\nfunction optRouterStyle(raw: Record<string, unknown>): 'jsx' | 'data' | null {\n const v = raw.routerStyle\n if (v === undefined || v === null) return null\n if (v !== 'jsx' && v !== 'data') {\n throw new Error(`sitemap.routerStyle must be \"jsx\" or \"data\" in ${CONFIG_NAME}`)\n }\n return v\n}\n\n/**\n * Load and validate `qa-meter.config.json` from `cwd`. All paths are resolved\n * against `cwd` (the directory containing the config file). Validation is\n * hand-rolled (no schema lib) to match the CLI's zero-dependency posture.\n */\nexport function loadQaConfig(cwd: string): QaConfig {\n const file = join(cwd, CONFIG_NAME)\n if (!existsSync(file)) {\n throw new Error(`Missing ${CONFIG_NAME} in ${cwd}. Create one — see the qa-meter design spec §7.1.`)\n }\n\n let parsed: unknown\n try {\n parsed = JSON.parse(readFileSync(file, 'utf8'))\n } catch (err) {\n throw new Error(`Failed to parse ${CONFIG_NAME}: ${err instanceof Error ? err.message : String(err)}`)\n }\n if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {\n throw new Error(`${CONFIG_NAME} must contain a JSON object`)\n }\n const raw = parsed as Record<string, unknown>\n\n // Type-check every present-but-wrong-type string field up front, so a bad\n // field surfaces a field-named error regardless of whether some *other*\n // required field happens to be missing.\n for (const field of [\n 'e2eDir',\n 'suitesDir',\n 'outFile',\n 'testsGlob',\n 'fixturesFile',\n 'pageObjectsDir',\n 'annotationPrefix',\n 'environment',\n 'resultsFile',\n 'devValidationsFile',\n ]) {\n const v = raw[field]\n if (v !== undefined && v !== null && typeof v !== 'string') {\n throw new Error(`${field} must be a string in ${CONFIG_NAME}`)\n }\n }\n\n let sitemap: QaSitemapConfig | null = null\n if (raw.sitemap !== undefined && raw.sitemap !== null) {\n if (typeof raw.sitemap !== 'object' || Array.isArray(raw.sitemap)) {\n throw new Error(`sitemap must be an object in ${CONFIG_NAME}`)\n }\n const s = raw.sitemap as Record<string, unknown>\n const routerDir = optPath(s, 'routerDir', cwd)\n const titlesFrom = optPath(s, 'titlesFrom', cwd)\n const alias = optAlias(s, 'alias', cwd)\n const entry = optPath(s, 'entry', cwd)\n const routerStyle = optRouterStyle(s)\n sitemap = {\n file: abs(cwd, reqString(s, 'file')),\n ...(s.framework !== undefined && s.framework !== null\n ? { framework: optString(s, 'framework', '') }\n : {}),\n ...(routerDir !== null ? { routerDir } : {}),\n ...(titlesFrom !== null ? { titlesFrom } : {}),\n ...(alias !== null ? { alias } : {}),\n ...(entry !== null ? { entry } : {}),\n ...(routerStyle !== null ? { routerStyle } : {}),\n }\n }\n\n return {\n e2eDir: abs(cwd, reqString(raw, 'e2eDir')),\n suitesDir: abs(cwd, reqString(raw, 'suitesDir')),\n outFile: abs(cwd, reqString(raw, 'outFile')),\n // testsGlob, fixturesFile, pageObjectsDir are kept relative to e2eDir (not absolutized).\n testsGlob: optString(raw, 'testsGlob', 'tests/**/*.spec.ts'),\n fixturesFile: optString(raw, 'fixturesFile', 'helpers/fixtures.ts'),\n pageObjectsDir: optString(raw, 'pageObjectsDir', 'pages'),\n annotationPrefix: optString(raw, 'annotationPrefix', '@mhosaic'),\n environment: optString(raw, 'environment', 'staging'),\n appVersion:\n raw.appVersion === undefined || raw.appVersion === null\n ? null\n : typeof raw.appVersion === 'string'\n ? raw.appVersion\n : (() => {\n throw new Error(`appVersion must be a string or null in ${CONFIG_NAME}`)\n })(),\n resultsFile: optPath(raw, 'resultsFile', cwd),\n devValidationsFile: optPath(raw, 'devValidationsFile', cwd),\n sitemap,\n }\n}\n"],"mappings":"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","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/qa/generate.ts"],"sourcesContent":["/**\n * `qa generate` — static analysis of Playwright specs into generated suite YAML.\n *\n * Parses (never executes) the fixtures file, the referenced page-objects, and\n * every spec via the TypeScript compiler API, then emits one generated suite per\n * test directory: one `test_case` per `test()`, pages inferred from the\n * page-object fixtures each test destructures (or overridden by a\n * `<prefix>:pages` header). Output is fully deterministic — every collection is\n * sorted lexicographically so two runs serialize identically.\n */\nimport { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'\nimport { dirname, join, relative, resolve } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport ts from 'typescript'\nimport { stringify } from 'yaml'\nimport { loadQaConfig, type QaConfig } from './config'\nimport { normalizePattern } from './sitemap-core'\nimport type { GeneratedSuiteDoc, SuitePage, SuiteTestCase } from './suite-format'\n\nconst HEADER_COMMENT =\n '# GENERATED by qa-meter — do not hand-edit. Regenerate with: mhosaic-feedback qa generate'\n\n// ───────────────────────────────────────────────────────── helpers ──────────\n\n/** Read the CLI package version (stamped into `meta.generated_by`). */\nfunction cliVersion(): string {\n const here = dirname(fileURLToPath(import.meta.url))\n // src/qa/generate.ts → ../../package.json ; dist/* → ../package.json. Walk up.\n for (let dir = here, i = 0; i < 5; i++, dir = dirname(dir)) {\n const candidate = join(dir, 'package.json')\n if (existsSync(candidate)) {\n try {\n const pkg = JSON.parse(readFileSync(candidate, 'utf8')) as { name?: string; version?: string }\n if (pkg.name === '@mhosaic/feedback-cli' && typeof pkg.version === 'string') return pkg.version\n } catch {\n // keep walking\n }\n }\n }\n return '0.0.0'\n}\n\n/** Slug a title: lowercase, strip accents, non-alphanumerics → `-`, collapse/trim. */\nfunction slug(input: string): string {\n return input\n .normalize('NFD')\n .replace(/[̀-ͯ]/g, '')\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '')\n}\n\n/** Stable registry key for a route pattern (e.g. `/widgets/:widgetId/edit` → `widgets-widgetId-edit`). */\nfunction patternKey(pattern: string): string {\n const key = pattern\n .replace(/[:*]/g, '')\n .split('/')\n .filter(Boolean)\n .map((seg) => slug(seg))\n .filter(Boolean)\n .join('-')\n return key.length > 0 ? key : 'root'\n}\n\n/** Read & parse a TS source file into an AST (no Program, no type-checking). */\nfunction parseFile(file: string): ts.SourceFile {\n const text = readFileSync(file, 'utf8')\n return ts.createSourceFile(file, text, ts.ScriptTarget.Latest, /*setParentNodes*/ true, ts.ScriptKind.TS)\n}\n\n/** Resolve a relative module specifier to an on-disk `.ts` file path. */\nfunction resolveImport(fromFile: string, spec: string): string | null {\n if (!spec.startsWith('.')) return null\n const base = resolve(dirname(fromFile), spec)\n for (const candidate of [`${base}.ts`, join(base, 'index.ts')]) {\n if (existsSync(candidate)) return candidate\n }\n return null\n}\n\n/** Map an imported identifier name → its source file, from a file's import declarations. */\nfunction importMap(sf: ts.SourceFile): Map<string, string> {\n const map = new Map<string, string>()\n for (const stmt of sf.statements) {\n if (!ts.isImportDeclaration(stmt) || !ts.isStringLiteral(stmt.moduleSpecifier)) continue\n const target = resolveImport(sf.fileName, stmt.moduleSpecifier.text)\n if (!target) continue\n const bindings = stmt.importClause?.namedBindings\n if (bindings && ts.isNamedImports(bindings)) {\n for (const el of bindings.elements) map.set(el.name.text, target)\n }\n if (stmt.importClause?.name) map.set(stmt.importClause.name.text, target)\n }\n return map\n}\n\n// ─────────────────────────────────────────────── step 1: fixtures map ───────\n\n/** `fixtureName → page-object class name` from the `base.extend({...})` call. */\nfunction fixtureClasses(sf: ts.SourceFile): Map<string, string> {\n const out = new Map<string, string>()\n\n /** Is this a `…extend(...)` / `…extend<...>(...)` call? */\n const isExtendCall = (node: ts.Node): node is ts.CallExpression =>\n ts.isCallExpression(node) &&\n ts.isPropertyAccessExpression(node.expression) &&\n node.expression.name.text === 'extend'\n\n let objLit: ts.ObjectLiteralExpression | undefined\n const findExtend = (node: ts.Node): void => {\n if (objLit) return\n if (isExtendCall(node)) {\n const arg = node.arguments[0]\n if (arg && ts.isObjectLiteralExpression(arg)) objLit = arg\n }\n ts.forEachChild(node, findExtend)\n }\n findExtend(sf)\n if (!objLit) return out\n\n for (const prop of objLit.properties) {\n // A fixture property is `async name({ … }, use) { … }` (method) or `name: async (…) => …`.\n let name: string | undefined\n let body: ts.Node | undefined\n if (ts.isMethodDeclaration(prop) && (ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name))) {\n name = prop.name.text\n body = prop.body\n } else if (\n ts.isPropertyAssignment(prop) &&\n (ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name)) &&\n (ts.isArrowFunction(prop.initializer) || ts.isFunctionExpression(prop.initializer))\n ) {\n name = prop.name.text\n body = prop.initializer.body\n }\n if (!name || !body) continue\n\n // Find `use(new <Class>(...))` inside the body.\n let className: string | undefined\n const findUse = (node: ts.Node): void => {\n if (className) return\n if (\n ts.isCallExpression(node) &&\n ts.isIdentifier(node.expression) &&\n node.expression.text === 'use' &&\n node.arguments[0] &&\n ts.isNewExpression(node.arguments[0]) &&\n ts.isIdentifier(node.arguments[0].expression)\n ) {\n className = node.arguments[0].expression.text\n }\n ts.forEachChild(node, findUse)\n }\n findUse(body)\n if (className) out.set(name, className)\n }\n return out\n}\n\n// ──────────────────────────────────── step 2: page-object goto patterns ─────\n\n/** Extract the route pattern from a page-object class's `goto` method, or null. */\nfunction gotoPattern(sf: ts.SourceFile, className: string): string | null {\n let cls: ts.ClassDeclaration | undefined\n for (const stmt of sf.statements) {\n if (ts.isClassDeclaration(stmt) && stmt.name?.text === className) cls = stmt\n }\n if (!cls) return null\n\n let method: ts.MethodDeclaration | undefined\n for (const member of cls.members) {\n if (ts.isMethodDeclaration(member) && ts.isIdentifier(member.name) && member.name.text === 'goto') {\n method = member\n }\n }\n if (!method?.body) return null\n\n // Parameter names, in order, to resolve `${expr}` spans that aren't bare identifiers.\n const params = method.parameters\n .map((p) => (ts.isIdentifier(p.name) ? p.name.text : undefined))\n .filter((n): n is string => !!n)\n\n // Find `this.page.goto(<arg>)`.\n let arg: ts.Expression | undefined\n const findGoto = (node: ts.Node): void => {\n if (arg) return\n if (\n ts.isCallExpression(node) &&\n ts.isPropertyAccessExpression(node.expression) &&\n node.expression.name.text === 'goto' &&\n node.arguments.length > 0\n ) {\n arg = node.arguments[0]\n }\n ts.forEachChild(node, findGoto)\n }\n findGoto(method.body)\n if (!arg) return null\n\n if (ts.isStringLiteralLike(arg)) return normalizePattern(arg.text)\n\n if (ts.isTemplateExpression(arg)) {\n let raw = arg.head.text\n arg.templateSpans.forEach((span, i) => {\n let name: string\n if (ts.isIdentifier(span.expression)) {\n name = span.expression.text\n } else {\n name = params[i] ?? 'param'\n }\n raw += `:${name}${span.literal.text}`\n })\n return normalizePattern(raw)\n }\n return null\n}\n\n// ─────────────────────────────────────────────────── step 3: specs ──────────\n\ninterface SpecCase {\n title: string\n /** destructured fixture property keys, in source order */\n fixtures: string[]\n /** literal `page.goto('/x')` patterns found in the body */\n gotos: string[]\n}\n\ninterface HeaderAnnotations {\n pages?: string[]\n scenario?: string\n}\n\n/** Parse the leading comment block for `<prefix>:pages` / `<prefix>:scenario` (+ legacy fallback). */\nfunction parseHeader(text: string, prefix: string): HeaderAnnotations {\n const out: HeaderAnnotations = {}\n const lines = text.split(/\\r?\\n/)\n const comments: string[] = []\n for (const line of lines) {\n const trimmed = line.trim()\n if (trimmed === '') continue\n if (trimmed.startsWith('//')) {\n comments.push(trimmed.replace(/^\\/\\/\\s?/, ''))\n continue\n }\n if (trimmed.startsWith('/*') || trimmed.startsWith('*')) {\n comments.push(trimmed.replace(/^\\/\\*+\\s?/, '').replace(/^\\*+\\s?/, '').replace(/\\*+\\/\\s*$/, ''))\n continue\n }\n break // first non-comment, non-blank line ends the header\n }\n\n const escaped = prefix.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n const read = (pfx: string): void => {\n for (const c of comments) {\n const pagesMatch = c.match(new RegExp(`${pfx}:pages\\\\s+(.+)$`))\n if (pagesMatch?.[1] && out.pages === undefined) {\n out.pages = pagesMatch[1].split(',').map((p) => p.trim()).filter(Boolean)\n }\n const scenarioMatch = c.match(new RegExp(`${pfx}:scenario\\\\s+(\\\\S+)`))\n if (scenarioMatch?.[1] && out.scenario === undefined) out.scenario = scenarioMatch[1]\n }\n }\n read(escaped)\n // legacy read-only fallback when the configured prefix form is absent\n read('@testgen')\n return out\n}\n\n/** Collect destructured property KEYS from a `test()` arrow's `({ a, b: alias })` param. */\nfunction destructuredKeys(fn: ts.ArrowFunction | ts.FunctionExpression): string[] {\n const param = fn.parameters[0]\n if (!param || !ts.isObjectBindingPattern(param.name)) return []\n const keys: string[] = []\n for (const el of param.name.elements) {\n // propertyName is the source key when aliased (`b: alias`); else `name` is the key.\n const keyNode = el.propertyName ?? el.name\n if (ts.isIdentifier(keyNode)) keys.push(keyNode.text)\n else if (ts.isStringLiteral(keyNode)) keys.push(keyNode.text)\n }\n return keys\n}\n\n/** Find literal `page.goto('/x')` string patterns inside a test body. */\nfunction literalGotos(body: ts.Node): string[] {\n const out: string[] = []\n const visit = (node: ts.Node): void => {\n if (\n ts.isCallExpression(node) &&\n ts.isPropertyAccessExpression(node.expression) &&\n node.expression.name.text === 'goto' &&\n node.arguments[0] &&\n ts.isStringLiteralLike(node.arguments[0])\n ) {\n out.push(normalizePattern(node.arguments[0].text))\n }\n ts.forEachChild(node, visit)\n }\n visit(body)\n return out\n}\n\n/**\n * Is `node` a test-defining call — `test(...)`, `test.only(...)`, or\n * `test.skip(...)` — i.e. one that REGISTERS a single test? Excludes structural\n * helpers (`test.describe`, `test.beforeEach`, `test.step`, …) which do not.\n */\nfunction isTestCall(node: ts.CallExpression): boolean {\n const callee = node.expression\n if (ts.isIdentifier(callee)) return callee.text === 'test'\n if (\n ts.isPropertyAccessExpression(callee) &&\n ts.isIdentifier(callee.expression) &&\n callee.expression.text === 'test'\n ) {\n return callee.name.text === 'only' || callee.name.text === 'skip' || callee.name.text === 'fixme'\n }\n return false\n}\n\n/** Outcome of scanning a spec AST: emittable cases + reports of dynamic-title tests. */\ninterface SpecScan {\n cases: SpecCase[]\n /** Approx descriptions of `test()` calls we found but couldn't emit (non-static title). */\n dynamicTitles: string[]\n}\n\n/**\n * Walk the WHOLE spec AST and collect every `test()`/`test.only()`/`test.skip()`\n * call (so tests inside `test.describe(() => …)` blocks, loops, or other wrappers\n * are found — not just direct top-level statements). A test with a non-static\n * title (template literal or identifier) cannot yield a stable external_key /\n * test_title that matches a Playwright result, so it is NOT emitted but is\n * REPORTED via `dynamicTitles` — silent loss is never acceptable.\n */\nfunction specCases(sf: ts.SourceFile): SpecScan {\n const cases: SpecCase[] = []\n const dynamicTitles: string[] = []\n\n const visit = (node: ts.Node): void => {\n if (ts.isCallExpression(node) && isTestCall(node)) {\n const titleArg = node.arguments[0]\n const fnArg = node.arguments[1]\n if (titleArg && ts.isStringLiteralLike(titleArg)) {\n if (fnArg && (ts.isArrowFunction(fnArg) || ts.isFunctionExpression(fnArg))) {\n const fixtures = destructuredKeys(fnArg)\n const gotos = fnArg.body ? literalGotos(fnArg.body) : []\n cases.push({ title: titleArg.text, fixtures, gotos })\n } else {\n // String title but no analyzable body (e.g. a bare reference) — still emittable.\n cases.push({ title: titleArg.text, fixtures: [], gotos: [] })\n }\n } else if (titleArg) {\n // Non-static title: template literal, identifier, call, etc. Report, don't emit.\n dynamicTitles.push(approxTitle(titleArg))\n }\n }\n ts.forEachChild(node, visit)\n }\n visit(sf)\n\n // `cases` keep AST traversal order (deterministic per file); the suite build\n // re-sorts across specs. `dynamicTitles` are sorted for stable reporting.\n dynamicTitles.sort((a, b) => a.localeCompare(b))\n return { cases, dynamicTitles }\n}\n\n/** A short, human-readable approximation of a non-static `test()` title argument. */\nfunction approxTitle(node: ts.Node): string {\n const text = node.getText().replace(/\\s+/g, ' ').trim()\n const kind = ts.isTemplateExpression(node)\n ? 'template literal'\n : ts.isIdentifier(node)\n ? 'identifier'\n : 'non-static'\n return `${text.slice(0, 80)} (${kind} title)`\n}\n\n/** List spec files under `e2eDir` matching the `tests/**​/*.spec.ts`-style glob. */\nfunction specFiles(e2eDir: string, testsGlob: string): string[] {\n // The glob is `<baseDir>/**/<suffix>` — derive base dir and a filename test.\n const starIdx = testsGlob.indexOf('**')\n const baseRel = starIdx >= 0 ? testsGlob.slice(0, starIdx).replace(/\\/+$/, '') : dirname(testsGlob)\n const suffixMatch = testsGlob.match(/\\*([^*/]+)$/)\n const suffix = suffixMatch?.[1] ?? '.spec.ts'\n const baseAbs = join(e2eDir, baseRel)\n\n const out: string[] = []\n const walk = (dir: string): void => {\n let entries: import('node:fs').Dirent[]\n try {\n entries = readdirSync(dir, { withFileTypes: true, encoding: 'utf8' })\n } catch {\n return\n }\n for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {\n const full = join(dir, entry.name)\n if (entry.isDirectory()) walk(full)\n else if (entry.name.endsWith(suffix)) out.push(full)\n }\n }\n walk(baseAbs)\n return out.sort((a, b) => a.localeCompare(b))\n}\n\n// ──────────────────────────────────────────────── orchestration ─────────────\n\n/**\n * Reporting attached to a generation run so that NO spec can vanish silently.\n */\nexport interface GenerateReport {\n /** Per-section generated suites, keyed by section. */\n suites: Map<string, GeneratedSuiteDoc>\n /** Emitted cases that could not be mapped to a page: `<spec> :: <title>`. */\n unmappable: string[]\n /** `test()` calls skipped because their title is not a static string literal: `<spec> :: <approx>`. */\n dynamicTitles: string[]\n /**\n * Spec files matched by the glob that produced ZERO emittable cases (e.g. a\n * factory `messageCrudTest(...)` whose `test()` lives in an imported base).\n * These are NOT in any suite, so they must be surfaced.\n */\n zeroCaseSpecs: string[]\n}\n\n/**\n * Pure generation. Besides the per-section suites, returns three reporting\n * lists so the caller can warn loudly: unmappable cases, dynamic-title tests\n * that were skipped, and spec files that yielded no extractable cases.\n */\nexport function generateSuites(cfg: QaConfig): GenerateReport {\n const version = cliVersion()\n const e2eDir = cfg.e2eDir\n const testsBase = (() => {\n const starIdx = cfg.testsGlob.indexOf('**')\n return starIdx >= 0 ? cfg.testsGlob.slice(0, starIdx).replace(/\\/+$/, '') : dirname(cfg.testsGlob)\n })()\n\n // Step 1 — fixtures map: fixtureName → page-object file path.\n const fixtureToFile = new Map<string, string>()\n const fixturesAbs = join(e2eDir, cfg.fixturesFile)\n if (existsSync(fixturesAbs)) {\n const sf = parseFile(fixturesAbs)\n const imports = importMap(sf)\n for (const [fixtureName, className] of fixtureClasses(sf)) {\n const target = imports.get(className)\n if (target) fixtureToFile.set(fixtureName, target)\n }\n }\n\n // Step 2 — fixtureName → route pattern (parse each referenced page-object once).\n const fixtureClassMap = existsSync(fixturesAbs) ? fixtureClasses(parseFile(fixturesAbs)) : new Map()\n const patternByFile = new Map<string, ts.SourceFile>()\n const fixtureToPattern = new Map<string, string>()\n for (const [fixtureName, file] of fixtureToFile) {\n const className = fixtureClassMap.get(fixtureName)\n if (!className || !existsSync(file)) continue\n let sf = patternByFile.get(file)\n if (!sf) {\n sf = parseFile(file)\n patternByFile.set(file, sf)\n }\n const pattern = gotoPattern(sf, className)\n if (pattern) fixtureToPattern.set(fixtureName, pattern)\n }\n\n const unmappable: string[] = []\n const dynamicTitles: string[] = []\n const zeroCaseSpecs: string[] = []\n // section → accumulated cases + page registry\n interface Section {\n cases: SuiteTestCase[]\n pages: Map<string, SuitePage>\n }\n const sections = new Map<string, Section>()\n\n const files = specFiles(e2eDir, cfg.testsGlob)\n for (const file of files) {\n const specRel = relative(e2eDir, file).split('\\\\').join('/')\n const specBasename = basenameNoSpec(specRel)\n const section = sectionFor(specRel, testsBase)\n const text = readFileSync(file, 'utf8')\n const header = parseHeader(text, cfg.annotationPrefix)\n const sf = parseFile(file)\n const scan = specCases(sf)\n const cases = scan.cases\n\n for (const approx of scan.dynamicTitles) dynamicTitles.push(`${specRel} :: ${approx}`)\n // A spec matched by the glob that produced no emittable cases must be surfaced\n // — its tests live elsewhere (factory/imported base) or use only dynamic titles.\n if (cases.length === 0) zeroCaseSpecs.push(specRel)\n\n if (!sections.has(section)) sections.set(section, { cases: [], pages: new Map() })\n const sec = sections.get(section)!\n\n cases.forEach((spec, index) => {\n let primary: string | undefined\n let traversed: string[] = []\n let pagesSource: 'inferred' | 'annotation' = 'inferred'\n\n if (header.pages && header.pages.length > 0) {\n // Annotation replaces inference for every case in this spec.\n const normalized = header.pages.map((p) => normalizePattern(p))\n primary = normalized[0]\n traversed = normalized.slice(1)\n pagesSource = 'annotation'\n } else {\n // Inference from destructured fixtures, then literal gotos in the body.\n const inferred: string[] = []\n for (const fx of spec.fixtures) {\n const pat = fixtureToPattern.get(fx)\n if (pat) inferred.push(pat)\n }\n for (const g of spec.gotos) inferred.push(g)\n if (inferred.length > 0) {\n primary = inferred[0]\n traversed = inferred.slice(1)\n }\n }\n\n const externalKey = header.scenario ?? `${specRel}::${slug(spec.title)}`\n\n let appliesTo: string[] = []\n if (primary) {\n const key = registerPage(sec.pages, primary, specRel, spec.title)\n appliesTo = [key]\n } else {\n unmappable.push(`${specRel} :: ${spec.title}`)\n }\n\n const testCase: SuiteTestCase = {\n id: `${specBasename}-${index}`,\n title: spec.title,\n suite: specBasename,\n status: 'covered',\n automation: { type: 'playwright', spec: specRel, test_title: spec.title },\n applies_to: appliesTo,\n ...(traversed.length > 0 ? { traverses: traversed.map((pattern) => ({ pattern })) } : {}),\n external_key: externalKey,\n pages_source: pagesSource,\n }\n sec.cases.push(testCase)\n })\n }\n\n // Build the sorted, deterministic suites.\n const suites = new Map<string, GeneratedSuiteDoc>()\n for (const section of [...sections.keys()].sort((a, b) => a.localeCompare(b))) {\n const sec = sections.get(section)!\n // A section with no emittable cases is not a suite — its specs are already\n // surfaced via zeroCaseSpecs/dynamicTitles. Don't write an empty YAML file.\n if (sec.cases.length === 0) continue\n const pages: Record<string, SuitePage> = {}\n for (const key of [...sec.pages.keys()].sort((a, b) => a.localeCompare(b))) {\n pages[key] = sec.pages.get(key)!\n }\n const test_cases = [...sec.cases].sort((a, b) => {\n const specA = a.automation?.spec ?? ''\n const specB = b.automation?.spec ?? ''\n if (specA !== specB) return specA.localeCompare(specB)\n return a.id.localeCompare(b.id)\n })\n suites.set(section, {\n meta: {\n section,\n format: 1,\n origin: 'generated',\n generated_by: `qa-meter@${version}`,\n },\n pages,\n test_cases,\n })\n }\n\n return {\n suites,\n unmappable,\n dynamicTitles: dynamicTitles.sort((a, b) => a.localeCompare(b)),\n zeroCaseSpecs: zeroCaseSpecs.sort((a, b) => a.localeCompare(b)),\n }\n}\n\n/** Register a primary pattern in the section registry; first case for a pattern wins. */\nfunction registerPage(\n registry: Map<string, SuitePage>,\n pattern: string,\n spec: string,\n testTitle: string,\n): string {\n const key = patternKey(pattern)\n if (!registry.has(key)) {\n registry.set(key, { pattern, spec, test_title: testTitle })\n }\n return key\n}\n\n/** `tests/widgets/list.spec.ts` → `list`. */\nfunction basenameNoSpec(specRel: string): string {\n const base = specRel.split('/').pop() ?? specRel\n return base.replace(/\\.spec\\.ts$/, '').replace(/\\.ts$/, '')\n}\n\n/** Section = the immediate parent dir name under the tests base (`tests/widgets/list.spec.ts` → `widgets`). */\nfunction sectionFor(specRel: string, testsBase: string): string {\n let rel = specRel\n const prefix = `${testsBase}/`\n if (rel.startsWith(prefix)) rel = rel.slice(prefix.length)\n const parts = rel.split('/')\n if (parts.length > 1 && parts[0]) return parts[0]\n return testsBase.split('/').filter(Boolean).pop() ?? 'tests'\n}\n\n// ─────────────────────────────────────────────── serialization ──────────────\n\n/** Serialize a generated suite the way `runGenerate` writes it (header + YAML). */\nexport function serializeSuite(doc: GeneratedSuiteDoc): string {\n const yaml = stringify(doc, { lineWidth: 0 })\n return `${HEADER_COMMENT}\\n${yaml}`\n}\n\n// ─────────────────────────────────────────────────── CLI entry ──────────────\n\nexport async function runGenerate(_args: string[]): Promise<void> {\n const cfg = loadQaConfig(process.cwd())\n const { suites, unmappable, dynamicTitles, zeroCaseSpecs } = generateSuites(cfg)\n\n const outDir = join(cfg.suitesDir, 'generated')\n mkdirSync(outDir, { recursive: true })\n\n const sections = [...suites.keys()].sort((a, b) => a.localeCompare(b))\n for (const section of sections) {\n const doc = suites.get(section)!\n const file = join(outDir, `${section}-test-suite.yaml`)\n writeFileSync(file, serializeSuite(doc), 'utf8')\n }\n\n const totalCases = sections.reduce((n, s) => n + suites.get(s)!.test_cases.length, 0)\n process.stderr.write(\n `qa generate: ${sections.length} suite(s), ${totalCases} case(s) → ${relative(process.cwd(), outDir) || outDir}\\n`,\n )\n if (unmappable.length > 0) {\n process.stderr.write(`qa generate: ${unmappable.length} unmappable test(s):\\n`)\n for (const entry of unmappable) process.stderr.write(` warning: unmappable — ${entry}\\n`)\n }\n if (zeroCaseSpecs.length > 0) {\n process.stderr.write(\n `qa generate: ${zeroCaseSpecs.length} spec(s) produced no extractable cases ` +\n `(factory/loop/dynamic-title) — add @mhosaic:pages/@mhosaic:scenario or a string title:\\n`,\n )\n for (const spec of zeroCaseSpecs) process.stderr.write(` warning: zero-case spec — ${spec}\\n`)\n }\n if (dynamicTitles.length > 0) {\n process.stderr.write(\n `qa generate: ${dynamicTitles.length} test(s) skipped for a non-static title ` +\n `(give them a string literal title or a @mhosaic:scenario id):\\n`,\n )\n for (const entry of dynamicTitles) process.stderr.write(` warning: dynamic-title — ${entry}\\n`)\n }\n}\n"],"mappings":"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","names":[]}
@@ -1,8 +0,0 @@
1
- #!/usr/bin/env node
2
- import {
3
- loadQaConfig
4
- } from "./chunk-IHJPCMYF.js";
5
- export {
6
- loadQaConfig
7
- };
8
- //# sourceMappingURL=config-JE3QRZVH.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/commands/doctor.ts"],"sourcesContent":["import { existsSync, readFileSync, readdirSync } from 'node:fs'\nimport type { Dirent } from 'node:fs'\nimport { dirname, join } from 'node:path'\n\nimport kleur from 'kleur'\n\nimport { detectFramework } from '../detect'\n\n// A bare `@mhosaic/feedback` import (no subpath) pulls the WHOLE widget into\n// the host's build — freezing its version to package.json. That's the\n// \"bundled\" delivery path: Mhosaic version pins/promotions never reach the\n// app without a dependency bump + redeploy. The loader subpaths\n// (`@mhosaic/feedback/loader`, `.../loader/react`) instead resolve the\n// version at runtime from the manifest, so a pin reaches every client with\n// zero per-repo work. This matcher flags the bundled path; see\n// scanDeliveryPath below. (2026-07-07: a client was found silently bundled\n// and stuck four releases behind — doctor missed it because it only checked\n// the React provider, not vanilla `createFeedback` installs.)\nconst BUNDLED_IMPORT = /from\\s+['\"]@mhosaic\\/feedback['\"]/\nconst CODE_EXT = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.vue', '.svelte', '.astro'])\nconst SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'build', '.next', '.output', '.svelte-kit'])\n\n/** Shallow-bounded walk of `src/` (and the entry) for a bare bundled import.\n * Returns the first offending repo-relative path, or null if none. Capped so\n * doctor stays fast on large repos. */\nexport function scanDeliveryPath(cwd: string, entry?: string): string | null {\n const seen = new Set<string>()\n const check = (rel: string): boolean => {\n const abs = join(cwd, rel)\n if (seen.has(abs) || !existsSync(abs)) return false\n seen.add(abs)\n try {\n return BUNDLED_IMPORT.test(readFileSync(abs, 'utf8'))\n } catch {\n return false\n }\n }\n if (entry && check(entry)) return entry\n\n let budget = 2000 // files; a backstop, real hosts are far smaller\n const walk = (dir: string): string | null => {\n let entries: Dirent[]\n try {\n entries = readdirSync(join(cwd, dir), { withFileTypes: true })\n } catch {\n return null\n }\n for (const e of entries) {\n if (budget-- <= 0) return null\n const rel = dir ? `${dir}/${e.name}` : e.name\n if (e.isDirectory()) {\n if (SKIP_DIRS.has(e.name)) continue\n const hit = walk(rel)\n if (hit) return hit\n } else if (CODE_EXT.has(e.name.slice(e.name.lastIndexOf('.')))) {\n if (check(rel)) return rel\n }\n }\n return null\n }\n return existsSync(join(cwd, 'src')) ? walk('src') : null\n}\n\n// The loader injects `<script src=\"https://cdn.jsdelivr.net/...widget.min.js\">`\n// at runtime, so a strict CSP must allow that host in `script-src` or the\n// widget silently never loads — the \"Refused to load … not in the script-src\n// directive\" failure that took a client's widget down on 2026-07-08 right\n// after it migrated to the loader. This is the companion requirement to the\n// import swap; catching it here means at migration time, not in production.\nconst CDN_HOST = 'cdn.jsdelivr.net'\n// CSP config lives in many shapes (index.html meta, _headers, nginx confs,\n// *.toml/json framework config). We only need the text.\nconst CSP_EXT = new Set(['.html', '.htm', '.conf', '.toml', '.json', '.js', '.cjs', '.mjs', '.ts'])\nconst CSP_EXTRA_NAMES = new Set(['_headers'])\n// A source token that already permits any cross-origin script host — no CDN\n// entry needed. `'unsafe-inline'`/`'unsafe-eval'` do NOT help a cross-origin\n// src, so they don't count here.\nconst PERMISSIVE_SRC = /(?:^|\\s)(?:\\*|https:)(?=\\s|;|\"|'|$)/\n\n/** The effective script-src value from one CSP string (falls back to\n * default-src), whitespace-flattened so a multi-line meta tag parses. Null\n * when neither directive is present. */\nexport function scriptSrcOf(csp: string): string | null {\n const flat = csp.replace(/\\s+/g, ' ')\n const m = /script-src([^;]*)/i.exec(flat) ?? /default-src([^;]*)/i.exec(flat)\n return m ? (m[1] ?? null) : null\n}\n\n/** Bounded repo walk for a CSP whose (effective) script-src is restrictive\n * AND omits the widget CDN — i.e. would block the loader. Returns the first\n * offending repo-relative file, or null. Permissive policies (`*`, `https:`)\n * and policies that already allow the CDN pass. */\nexport function scanCsp(cwd: string): string | null {\n let budget = 3000\n const walk = (dir: string): string | null => {\n let entries: Dirent[]\n try {\n entries = readdirSync(join(cwd, dir), { withFileTypes: true })\n } catch {\n return null\n }\n for (const e of entries) {\n if (budget-- <= 0) return null\n const rel = dir ? `${dir}/${e.name}` : e.name\n if (e.isDirectory()) {\n if (SKIP_DIRS.has(e.name)) continue\n const hit = walk(rel)\n if (hit) return hit\n continue\n }\n const ext = e.name.slice(e.name.lastIndexOf('.'))\n if (!CSP_EXT.has(ext) && !CSP_EXTRA_NAMES.has(e.name)) continue\n let text: string\n try {\n text = readFileSync(join(cwd, rel), 'utf8')\n } catch {\n continue\n }\n if (!text.includes('script-src') && !text.includes('Content-Security-Policy')) continue\n const scriptSrc = scriptSrcOf(text)\n if (scriptSrc === null) continue // CSP present but no script/default-src → not restricting scripts\n if (scriptSrc.includes(CDN_HOST) || PERMISSIVE_SRC.test(scriptSrc)) continue // already allowed\n return rel // restrictive script-src that omits the CDN → would block the loader\n }\n return null\n }\n return walk('')\n}\n\n/** Nearest `.gitignore` on the path from `cwd` up to the repo root that\n * covers `.env.local`, as a path relative to `cwd` — or null if none does.\n *\n * Only cwd's own `.gitignore` used to be read, so the very common\n * `backend/ + frontend/` layout — one `.gitignore` at the repo root, doctor\n * run from `frontend/` — reported a miss for a file git was already\n * ignoring, and sent operators off to write a second `.gitignore` they\n * didn't need. The walk stops AT the repo root because a `.gitignore` above\n * it isn't git's to apply; `.git` is a directory in a normal clone and a\n * file in a worktree, so existence is the test, not type. Substring match on\n * purpose: this is the same shallow check as before, just widened to the\n * ancestors — parsing gitignore semantics properly is not worth it to\n * answer \"did they remember the line\". */\nexport function findGitignoreCovering(cwd: string): string | null {\n let dir = cwd\n for (let depth = 0; depth < 32; depth++) {\n const gitignore = join(dir, '.gitignore')\n if (existsSync(gitignore)) {\n try {\n if (readFileSync(gitignore, 'utf8').includes('.env.local')) {\n return depth === 0 ? '.gitignore' : '../'.repeat(depth) + '.gitignore'\n }\n } catch {\n // Unreadable — treat as absent and keep walking.\n }\n }\n if (existsSync(join(dir, '.git'))) return null // repo root, stop\n const parent = dirname(dir)\n if (parent === dir) return null // filesystem root\n dir = parent\n }\n return null\n}\n\n/** How the host instantiates the widget, or null if it never does. */\nexport type WidgetWiring = 'provider' | 'factory'\n\n// Any widget import — bare or subpath. Which one it is, is the delivery\n// check's business (scanDeliveryPath), not this one's.\nconst FEEDBACK_IMPORT = /from\\s+['\"]@mhosaic\\/feedback(?:\\/[a-z/-]+)?['\"]/\nconst FACTORY_CALL = /\\bcreateFeedback\\s*\\(/\n\n/** Detect that the widget is actually instantiated somewhere.\n *\n * Two sanctioned shapes, not one. `<FeedbackProvider>` in the entry is what\n * `init` auto-writes. But `consumer-install-vite.md` tells hosts that to add\n * withErrorTracking / withReplay / withWebVitals they should REPLACE the\n * provider with a `createFeedback()` call in a small module imported for its\n * side effect — so checking only for the provider made doctor report a\n * correctly-wired host as unwired, and told it to undo what the docs had\n * just asked for. (2026-08-19: hit on a client integration doing exactly\n * that.) */\nexport function scanWidgetWiring(cwd: string, entry?: string): WidgetWiring | null {\n const read = (rel: string): string | null => {\n const abs = join(cwd, rel)\n if (!existsSync(abs)) return null\n try {\n return readFileSync(abs, 'utf8')\n } catch {\n return null\n }\n }\n\n const entrySrc = entry ? read(entry) : null\n if (\n entrySrc &&\n entrySrc.includes('<FeedbackProvider') &&\n (entrySrc.includes('@mhosaic/feedback/loader/react') ||\n entrySrc.includes('@mhosaic/feedback/react'))\n ) {\n return 'provider'\n }\n\n const instantiates = (src: string): boolean =>\n FACTORY_CALL.test(src) && FEEDBACK_IMPORT.test(src)\n\n if (entrySrc && instantiates(entrySrc)) return 'factory'\n\n // Same bounded walk + skip list as scanDeliveryPath: the factory module is\n // conventionally src/feedback.ts, but nothing enforces that.\n let budget = 2000\n const walk = (dir: string): WidgetWiring | null => {\n let entries: Dirent[]\n try {\n entries = readdirSync(join(cwd, dir), { withFileTypes: true })\n } catch {\n return null\n }\n for (const e of entries) {\n if (budget-- <= 0) return null\n const rel = dir ? `${dir}/${e.name}` : e.name\n if (e.isDirectory()) {\n if (SKIP_DIRS.has(e.name)) continue\n const hit = walk(rel)\n if (hit) return hit\n } else if (CODE_EXT.has(e.name.slice(e.name.lastIndexOf('.')))) {\n const src = read(rel)\n if (src && instantiates(src)) return 'factory'\n }\n }\n return null\n }\n return existsSync(join(cwd, 'src')) ? walk('src') : null\n}\n\n/** A host can declare it is intentionally bundled — e.g. a strict CSP that\n * forbids external CDN scripts, so the loader physically cannot run (its\n * jsDelivr <script> is refused). It opts out via package.json\n * `{\"mhosaicFeedback\": {\"delivery\": \"bundled\"}}`. This is the ONE escape\n * hatch; every host without it still gets the hard \"switch to the loader\"\n * failure, so the default stays strong. Expected to be rare (one known case:\n * a CSP-locked client whose widget updates are done manually). */\nexport function isBundledByDesign(cwd: string): boolean {\n try {\n const pkg = JSON.parse(readFileSync(join(cwd, 'package.json'), 'utf8'))\n return pkg?.mhosaicFeedback?.delivery === 'bundled'\n } catch {\n return false\n }\n}\n\nexport async function runDoctor(argv: string[]): Promise<void> {\n const cwd = argv.includes('--cwd') ? argv[argv.indexOf('--cwd') + 1] ?? process.cwd() : process.cwd()\n\n const checks: Array<{ name: string; ok: boolean; hint?: string }> = []\n\n const framework = await detectFramework(cwd)\n checks.push({ name: `framework detected: ${framework.kind}`, ok: framework.kind !== 'unknown' && framework.kind !== 'plain' })\n\n const envPath = join(cwd, '.env.local')\n const envOk = existsSync(envPath) && readFileSync(envPath, 'utf8').includes('VITE_FEEDBACK_API_KEY=')\n checks.push({ name: '.env.local has VITE_FEEDBACK_API_KEY', ok: envOk, hint: 'run `mhosaic-feedback init`' })\n\n const giAt = findGitignoreCovering(cwd)\n checks.push({\n // Name the file when it isn't the local one, so a monorepo operator can\n // see WHICH .gitignore answered rather than wondering if we looked.\n name:\n giAt && giAt !== '.gitignore'\n ? `.gitignore ignores .env.local (${giAt})`\n : '.gitignore ignores .env.local',\n ok: giAt !== null,\n hint: 'add `.env.local` to .gitignore',\n })\n\n const wiring = scanWidgetWiring(cwd, framework.entry ?? undefined)\n checks.push({\n name:\n wiring === 'provider'\n ? 'widget wired in the app (<FeedbackProvider> in entry)'\n : wiring === 'factory'\n ? 'widget wired in the app (createFeedback module)'\n : 'widget wired in the app',\n ok: wiring !== null,\n hint: 'run `mhosaic-feedback init`, or instantiate the widget yourself with createFeedback()',\n })\n\n // Delivery path: the loader resolves the widget version at runtime, so\n // Mhosaic version pins reach this app automatically. A bare\n // `@mhosaic/feedback` import bundles a frozen version instead. This is the\n // one check that keeps \"every client is loader\" true over time — unless the\n // host has explicitly opted into bundling (isBundledByDesign), the one\n // sanctioned exception for a CSP that can't allow the CDN.\n if (isBundledByDesign(cwd)) {\n checks.push({\n name: 'delivery: bundled on purpose (mhosaicFeedback.delivery=\"bundled\") — update the version manually',\n ok: true,\n })\n // CSP check deliberately skipped: a bundled widget is served same-origin,\n // so there is no CDN host to allowlist.\n } else {\n const bundledAt = scanDeliveryPath(cwd, framework.entry ?? undefined)\n checks.push({\n name: 'loader delivery path (version pins reach this app)',\n ok: bundledAt === null,\n ...(bundledAt\n ? {\n hint: `bundled import in ${bundledAt} — change '@mhosaic/feedback' to '@mhosaic/feedback/loader' (same API) so pins land without a redeploy. If a strict CSP forbids the CDN, keep it bundled and set package.json \"mhosaicFeedback\": { \"delivery\": \"bundled\" }`,\n }\n : {}),\n })\n\n // CSP: the loader fetches the widget from the CDN, so a strict script-src\n // must allow it. Only meaningful on the loader path — a bundled app serves\n // the widget same-origin and needs no CDN entry (the check above already\n // tells it to switch). Gating on `!bundledAt` avoids a confusing double\n // warning on apps that haven't migrated yet.\n if (bundledAt === null) {\n const cspBlockedAt = scanCsp(cwd)\n checks.push({\n name: `CSP allows the widget CDN (${CDN_HOST})`,\n ok: cspBlockedAt === null,\n ...(cspBlockedAt\n ? {\n hint: `CSP in ${cspBlockedAt} omits the CDN — add 'https://${CDN_HOST}' to script-src (and the platform origin to connect-src), or if the CSP can't allow an external CDN keep the widget bundled and set package.json \"mhosaicFeedback\": { \"delivery\": \"bundled\" }`,\n }\n : {}),\n })\n }\n }\n\n let hasFailure = false\n for (const c of checks) {\n const icon = c.ok ? kleur.green('✓') : kleur.red('✗')\n process.stdout.write(`${icon} ${c.name}${!c.ok && c.hint ? kleur.gray(' — ' + c.hint) : ''}\\n`)\n if (!c.ok) hasFailure = true\n }\n if (hasFailure) process.exitCode = 1\n}\n"],"mappings":"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","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/commands/eject.ts"],"sourcesContent":["import { existsSync, readFileSync } from 'node:fs'\nimport { writeFile } from 'node:fs/promises'\nimport { join } from 'node:path'\n\nimport kleur from 'kleur'\n\nimport { detectFramework } from '../detect'\n\nexport async function runEject(argv: string[]): Promise<void> {\n const cwd = argv.includes('--cwd') ? argv[argv.indexOf('--cwd') + 1] ?? process.cwd() : process.cwd()\n\n const framework = await detectFramework(cwd)\n if (framework.entry) {\n const entryPath = join(cwd, framework.entry)\n if (existsSync(entryPath)) {\n const src = readFileSync(entryPath, 'utf8')\n const stripped = src\n .replace(/\\/\\/ === mhosaic-feedback:import:start ===[\\s\\S]*?\\/\\/ === mhosaic-feedback:import:end ===\\n?/g, '')\n .replace(/\\{\\/\\* === mhosaic-feedback:wrap:start === \\*\\/\\}[\\s\\S]*?\\{\\/\\* === mhosaic-feedback:wrap:end === \\*\\/\\}\\n?/g, '')\n if (stripped !== src) {\n await writeFile(entryPath, stripped)\n process.stdout.write(kleur.green(`✓ Stripped markers from ${framework.entry}\\n`))\n }\n }\n }\n\n const envPath = join(cwd, '.env.local')\n if (existsSync(envPath)) {\n const src = readFileSync(envPath, 'utf8')\n const cleaned = src.replace(/# === mhosaic-feedback ===\\n[\\s\\S]*?# === mhosaic-feedback ===\\n?/g, '').replace(/\\n{3,}/g, '\\n\\n')\n if (cleaned !== src) {\n await writeFile(envPath, cleaned)\n process.stdout.write(kleur.green('✓ Removed env block from .env.local\\n'))\n }\n }\n\n process.stdout.write(kleur.gray(\"Run `pnpm remove @mhosaic/feedback` (or npm/yarn equivalent) to uninstall the package.\\n\"))\n}\n"],"mappings":";;;;;;AAAA,SAAS,YAAY,oBAAoB;AACzC,SAAS,iBAAiB;AAC1B,SAAS,YAAY;AAErB,OAAO,WAAW;AAIlB,eAAsB,SAAS,MAA+B;AAC5D,QAAM,MAAM,KAAK,SAAS,OAAO,IAAI,KAAK,KAAK,QAAQ,OAAO,IAAI,CAAC,KAAK,QAAQ,IAAI,IAAI,QAAQ,IAAI;AAEpG,QAAM,YAAY,MAAM,gBAAgB,GAAG;AAC3C,MAAI,UAAU,OAAO;AACnB,UAAM,YAAY,KAAK,KAAK,UAAU,KAAK;AAC3C,QAAI,WAAW,SAAS,GAAG;AACzB,YAAM,MAAM,aAAa,WAAW,MAAM;AAC1C,YAAM,WAAW,IACd,QAAQ,kGAAkG,EAAE,EAC5G,QAAQ,gHAAgH,EAAE;AAC7H,UAAI,aAAa,KAAK;AACpB,cAAM,UAAU,WAAW,QAAQ;AACnC,gBAAQ,OAAO,MAAM,MAAM,MAAM,gCAA2B,UAAU,KAAK;AAAA,CAAI,CAAC;AAAA,MAClF;AAAA,IACF;AAAA,EACF;AAEA,QAAM,UAAU,KAAK,KAAK,YAAY;AACtC,MAAI,WAAW,OAAO,GAAG;AACvB,UAAM,MAAM,aAAa,SAAS,MAAM;AACxC,UAAM,UAAU,IAAI,QAAQ,sEAAsE,EAAE,EAAE,QAAQ,WAAW,MAAM;AAC/H,QAAI,YAAY,KAAK;AACnB,YAAM,UAAU,SAAS,OAAO;AAChC,cAAQ,OAAO,MAAM,MAAM,MAAM,4CAAuC,CAAC;AAAA,IAC3E;AAAA,EACF;AAEA,UAAQ,OAAO,MAAM,MAAM,KAAK,0FAA0F,CAAC;AAC7H;","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}