@mhosaic/feedback-cli 0.20.0 → 0.22.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 (31) hide show
  1. package/dist/bin.js +6 -1
  2. package/dist/bin.js.map +1 -1
  3. package/dist/build-RCLWV2WL.js +565 -0
  4. package/dist/build-RCLWV2WL.js.map +1 -0
  5. package/dist/check-FOJPEE4Q.js +342 -0
  6. package/dist/check-FOJPEE4Q.js.map +1 -0
  7. package/dist/chunk-AZQSQJNQ.js +83 -0
  8. package/dist/chunk-AZQSQJNQ.js.map +1 -0
  9. package/dist/chunk-IHJPCMYF.js +126 -0
  10. package/dist/chunk-IHJPCMYF.js.map +1 -0
  11. package/dist/chunk-SSLQOK2Z.js +456 -0
  12. package/dist/chunk-SSLQOK2Z.js.map +1 -0
  13. package/dist/config-JE3QRZVH.js +8 -0
  14. package/dist/config-JE3QRZVH.js.map +1 -0
  15. package/dist/generate-VFRQNPOI.js +14 -0
  16. package/dist/generate-VFRQNPOI.js.map +1 -0
  17. package/dist/{install-skill-QJ4ZDVVR.js → install-skill-PO5YSXWY.js} +4 -2
  18. package/dist/{install-skill-QJ4ZDVVR.js.map → install-skill-PO5YSXWY.js.map} +1 -1
  19. package/dist/qa-YR4GCV6T.js +43 -0
  20. package/dist/qa-YR4GCV6T.js.map +1 -0
  21. package/dist/sitemap-react-QTEAUKN5.js +330 -0
  22. package/dist/sitemap-react-QTEAUKN5.js.map +1 -0
  23. package/dist/sitemap-vue-EJDQTCYM.js +243 -0
  24. package/dist/sitemap-vue-EJDQTCYM.js.map +1 -0
  25. package/package.json +4 -3
  26. package/skills/integrate-feedback/SKILL.md +7 -0
  27. package/skills/integrate-qa-meter/SKILL.md +201 -0
  28. package/skills/integrate-qa-meter/references/qa-meter-config.md +168 -0
  29. package/skills/integrate-qa-meter/references/qa-meter-troubleshooting.md +37 -0
  30. package/skills/integrate-qa-meter/references/qa-meter.config.example.json +20 -0
  31. package/skills/integrate-qa-meter/references/qa-meter.config.example.react.json +19 -0
package/dist/bin.js CHANGED
@@ -20,9 +20,13 @@ async function main() {
20
20
  return runVerify(args);
21
21
  }
22
22
  if (cmd === "install-skill") {
23
- const { runInstallSkill } = await import("./install-skill-QJ4ZDVVR.js");
23
+ const { runInstallSkill } = await import("./install-skill-PO5YSXWY.js");
24
24
  return runInstallSkill(args);
25
25
  }
26
+ if (cmd === "qa") {
27
+ const { runQa } = await import("./qa-YR4GCV6T.js");
28
+ return runQa(args);
29
+ }
26
30
  if (cmd === "--version" || cmd === "-v") {
27
31
  process.stdout.write("0.14.1\n");
28
32
  return;
@@ -35,6 +39,7 @@ Usage:
35
39
  mhosaic-feedback verify Ping the backend with the configured key
36
40
  mhosaic-feedback eject Strip widget integration from this project
37
41
  mhosaic-feedback install-skill Copy the Claude Code skill to ~/.claude/skills
42
+ mhosaic-feedback qa Run QA-meter commands (sitemap|generate|build|check|refresh)
38
43
  `);
39
44
  process.exitCode = 1;
40
45
  }
package/dist/bin.js.map CHANGED
@@ -1 +1 @@
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 === '--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`)\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,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,CAAoX;AACha,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
+ {"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":[]}
@@ -0,0 +1,565 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ loadQaConfig
4
+ } from "./chunk-IHJPCMYF.js";
5
+
6
+ // src/qa/build.ts
7
+ import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync3, writeFileSync } from "fs";
8
+ import { dirname } from "path";
9
+
10
+ // src/qa/color.ts
11
+ function deriveScenarioColor(automated, dev, client) {
12
+ const rejected = dev.rejected === true || client.rejected === true;
13
+ if (automated.status === "failing" || rejected) {
14
+ return "red";
15
+ }
16
+ const everRun = automated.status !== "never_run";
17
+ const anyHuman = dev.validated || client.validated;
18
+ if (!everRun && !anyHuman) {
19
+ return "gray";
20
+ }
21
+ const fresh = dev.stale !== true && client.stale !== true;
22
+ if (automated.status === "passing" && dev.validated && client.validated && fresh) {
23
+ return "green";
24
+ }
25
+ return "yellow";
26
+ }
27
+ function aggregateColor(colors) {
28
+ if (colors.length === 0) {
29
+ return "gray";
30
+ }
31
+ if (colors.includes("red")) {
32
+ return "red";
33
+ }
34
+ if (colors.every((c) => c === "green")) {
35
+ return "green";
36
+ }
37
+ if (colors.every((c) => c === "gray")) {
38
+ return "gray";
39
+ }
40
+ return "yellow";
41
+ }
42
+ function isValidationStale(opts) {
43
+ if (!opts.validatedAt) {
44
+ return false;
45
+ }
46
+ return opts.automatedStatus === "failing" && !!opts.automatedAt && opts.automatedAt > opts.validatedAt;
47
+ }
48
+
49
+ // src/qa/results.ts
50
+ import { readFileSync } from "fs";
51
+ function provenanceFor(retried) {
52
+ const parts = [];
53
+ const runner = process.env.QA_RUNNER;
54
+ const database = process.env.QA_DATABASE;
55
+ const authMode = process.env.QA_AUTH_MODE;
56
+ if (runner) parts.push(runner);
57
+ if (database) parts.push(database);
58
+ if (authMode) parts.push(authMode);
59
+ if (retried) parts.push("retried");
60
+ return parts.length > 0 ? parts.join(" \xB7 ") : void 0;
61
+ }
62
+ function laneFor(test) {
63
+ const results = test.results ?? [];
64
+ if (results.length === 0) {
65
+ return { status: "never_run" };
66
+ }
67
+ const last = results[results.length - 1];
68
+ const at = last.startTime;
69
+ const retried = (last.retry ?? 0) > 0 || results.length > 1;
70
+ let status;
71
+ if (last.status === "passed") {
72
+ status = retried ? "flaky" : "passing";
73
+ } else {
74
+ status = "failing";
75
+ }
76
+ const provenance = provenanceFor(retried);
77
+ return { status, ...at ? { at } : {}, ...provenance ? { provenance } : {} };
78
+ }
79
+ function readResults(file) {
80
+ const lanes = /* @__PURE__ */ new Map();
81
+ let report;
82
+ try {
83
+ report = JSON.parse(readFileSync(file, "utf8"));
84
+ } catch (err) {
85
+ throw new Error(`qa build: failed to read Playwright report ${file}: ${err instanceof Error ? err.message : String(err)}`);
86
+ }
87
+ const walk = (suite) => {
88
+ for (const spec of suite.specs ?? []) {
89
+ const specFile = spec.file ?? suite.file;
90
+ if (!specFile || !spec.title) continue;
91
+ for (const test of spec.tests ?? []) {
92
+ lanes.set(`${specFile}::${spec.title}`, laneFor(test));
93
+ }
94
+ }
95
+ for (const child of suite.suites ?? []) walk(child);
96
+ };
97
+ for (const suite of report.suites ?? []) walk(suite);
98
+ return lanes;
99
+ }
100
+
101
+ // src/qa/load-suites.ts
102
+ import { existsSync, readFileSync as readFileSync2, readdirSync, statSync } from "fs";
103
+ import { join } from "path";
104
+ import { parse } from "yaml";
105
+ function discoverModuleSuites(suitesDir) {
106
+ const modulesDir = join(suitesDir, "modules");
107
+ if (!existsSync(modulesDir)) return [];
108
+ const out = [];
109
+ for (const name of readdirSync(modulesDir).sort()) {
110
+ const sub = join(modulesDir, name);
111
+ if (!statSync(sub).isDirectory()) continue;
112
+ const file = readdirSync(sub).find((f) => f.endsWith("-test-suite.yaml"));
113
+ if (file) out.push(join(sub, file));
114
+ }
115
+ return out;
116
+ }
117
+ function discoverGeneratedSuites(suitesDir) {
118
+ const genDir = join(suitesDir, "generated");
119
+ if (!existsSync(genDir)) return [];
120
+ return readdirSync(genDir).filter((f) => f.endsWith(".yaml") || f.endsWith(".yml")).sort().map((f) => join(genDir, f));
121
+ }
122
+ function discoverCrossSuites(suitesDir) {
123
+ if (!existsSync(suitesDir)) return [];
124
+ return readdirSync(suitesDir).filter((f) => f.endsWith("-test-suite.yaml")).sort().map((f) => join(suitesDir, f));
125
+ }
126
+ function parseSuiteLabels(rawText) {
127
+ const labels = {};
128
+ const re = /^\s*#\s*-+\s*Suite\s+(\S+)\s*[—–-]\s*(.+?)\s*-*\s*$/gm;
129
+ let m;
130
+ while ((m = re.exec(rawText)) !== null) {
131
+ labels[m[1]] = m[2].replace(/\s*\([^)]*\)\s*$/, "").trim();
132
+ }
133
+ return labels;
134
+ }
135
+ function expandRoutes(value, pageSets) {
136
+ if (!value) return [];
137
+ if (typeof value === "string") return pageSets[value] ?? [value];
138
+ return value.flatMap((entry) => pageSets[entry] ? pageSets[entry] : [entry]);
139
+ }
140
+ function caseStatusOf(testCase) {
141
+ const s = testCase.status;
142
+ if (s === "covered" || s === "manual" || s === "blocked" || s === "open-question") return s;
143
+ return "manual";
144
+ }
145
+ function loadScenarioSeeds(suitesDir) {
146
+ const modules = {};
147
+ const byPattern = /* @__PURE__ */ new Map();
148
+ const coveredPatterns = /* @__PURE__ */ new Set();
149
+ const push = (pattern, seed) => {
150
+ if (!pattern) return;
151
+ const list = byPattern.get(pattern);
152
+ if (list) list.push(seed);
153
+ else byPattern.set(pattern, [seed]);
154
+ };
155
+ const moduleFiles = [
156
+ ...discoverModuleSuites(suitesDir).map((path) => ({ path, origin: "authored" })),
157
+ ...discoverGeneratedSuites(suitesDir).map((path) => ({ path, origin: "generated" }))
158
+ ];
159
+ for (const { path, origin } of moduleFiles) {
160
+ const raw = readFileSync2(path, "utf8");
161
+ const doc = parse(raw);
162
+ if (!doc || !doc.meta) continue;
163
+ const section = doc.meta.section ?? path;
164
+ const existing = modules[section];
165
+ if (existing) {
166
+ existing.cases.push(...doc.test_cases ?? []);
167
+ if (origin === "authored") existing.origin = "authored";
168
+ } else {
169
+ modules[section] = {
170
+ section,
171
+ pages: doc.pages ?? {},
172
+ pageSets: doc.page_sets ?? {},
173
+ labels: parseSuiteLabels(raw),
174
+ cases: [...doc.test_cases ?? []],
175
+ origin
176
+ };
177
+ }
178
+ if (existing) {
179
+ for (const [key, page] of Object.entries(doc.pages ?? {})) {
180
+ if (!(key in existing.pages)) existing.pages[key] = page;
181
+ }
182
+ for (const [key, set] of Object.entries(doc.page_sets ?? {})) {
183
+ if (!(key in existing.pageSets)) existing.pageSets[key] = set;
184
+ }
185
+ }
186
+ }
187
+ for (const { path, origin } of moduleFiles) {
188
+ const raw = readFileSync2(path, "utf8");
189
+ const doc = parse(raw);
190
+ if (!doc || !doc.meta) continue;
191
+ const section = doc.meta.section ?? path;
192
+ const mod = modules[section];
193
+ const labels = mod.labels;
194
+ const localPages = { ...mod.pages, ...doc.pages ?? {} };
195
+ const localPageSets = { ...mod.pageSets, ...doc.page_sets ?? {} };
196
+ for (const testCase of doc.test_cases ?? []) {
197
+ const routes = expandRoutes(testCase.applies_to, localPageSets);
198
+ const traversed = (testCase.traverses ?? []).map((t) => t.pattern).filter(Boolean);
199
+ const groupKey = testCase.suite ?? "?";
200
+ const label = labels[groupKey] ?? `Suite ${groupKey}`;
201
+ const status = caseStatusOf(testCase);
202
+ const specFallback = routes.length > 0 ? localPages[routes[0]]?.spec : void 0;
203
+ const specPath = testCase.automation?.spec ?? specFallback;
204
+ const testTitle = testCase.automation?.test_title ?? localPages[routes[0]]?.test_title;
205
+ const dedupKey = specPath && testTitle ? `${specPath}::${testTitle}` : void 0;
206
+ const displayTitle = origin === "authored" && testCase.automation?.type === "playwright" && testCase.automation?.test_title ? testCase.automation.test_title : testCase.title;
207
+ for (const route of routes) {
208
+ const page = localPages[route];
209
+ const pattern = page?.pattern;
210
+ if (!pattern) continue;
211
+ if (status === "covered") coveredPatterns.add(pattern);
212
+ push(pattern, {
213
+ id: `s-${section}-${testCase.id}-${route}`,
214
+ external_key: testCase.external_key ?? `${section}#${testCase.id}`,
215
+ title: displayTitle,
216
+ source: testCase.automation?.type === "playwright" ? "reporter" : "manual",
217
+ ...specPath ? { spec_path: specPath } : {},
218
+ ...testTitle ? { test_title: testTitle } : {},
219
+ group: { key: groupKey, label },
220
+ ...testCase.priority ? { priority: testCase.priority } : {},
221
+ caseStatus: status,
222
+ ...testCase.grounding ? { grounding: testCase.grounding } : {},
223
+ pages: [
224
+ { pattern, role: "primary" },
225
+ ...traversed.map((p) => ({ pattern: p, role: "traversed" }))
226
+ ],
227
+ pattern,
228
+ origin,
229
+ ...dedupKey ? { dedupKey } : {}
230
+ });
231
+ }
232
+ }
233
+ }
234
+ for (const crossPath of discoverCrossSuites(suitesDir)) {
235
+ const raw = readFileSync2(crossPath, "utf8");
236
+ const doc = parse(raw);
237
+ if (!doc || doc.meta?.kind !== "cross-module") continue;
238
+ const pair = doc.meta?.pair ?? [];
239
+ const crossLabel = pair.length ? `Inter-modules (${pair.join(" \xD7 ")})` : "Inter-modules";
240
+ for (const testCase of doc.test_cases ?? []) {
241
+ const refs = [];
242
+ for (const touch of testCase.touches ?? []) {
243
+ const mod = modules[touch.module];
244
+ if (!mod) continue;
245
+ const routes = touch.route ? [touch.route] : expandRoutes(touch.page_set, mod.pageSets);
246
+ for (const route of routes) {
247
+ const pattern = mod.pages[route]?.pattern;
248
+ if (pattern) refs.push({ pattern, role: touch.role === "origin" ? "primary" : "traversed" });
249
+ }
250
+ }
251
+ const patterns = [...new Set(refs.map((r) => r.pattern))];
252
+ const status = caseStatusOf(testCase);
253
+ for (const pattern of patterns) {
254
+ push(pattern, {
255
+ id: `s-cross-${testCase.id}-${pattern}`,
256
+ external_key: `cross#${testCase.id}`,
257
+ title: testCase.title,
258
+ source: "manual",
259
+ group: { key: "X", label: crossLabel },
260
+ ...testCase.priority ? { priority: testCase.priority } : {},
261
+ caseStatus: status,
262
+ ...testCase.grounding ? { grounding: testCase.grounding } : {},
263
+ pages: [{ pattern, role: "primary" }, ...refs.filter((r) => r.pattern !== pattern)],
264
+ pattern,
265
+ origin: "authored"
266
+ });
267
+ }
268
+ }
269
+ }
270
+ return { byPattern, coveredPatterns };
271
+ }
272
+
273
+ // src/qa/build.ts
274
+ var EMPTY_HUMAN = { validated: false, stale: false };
275
+ var FALLBACK_VERSION = "0.0.0";
276
+ function idFor(pattern) {
277
+ return "p" + pattern.replace(/[^a-zA-Z0-9]+/g, "-").replace(/-+$/, "");
278
+ }
279
+ function humanize(str) {
280
+ if (!str) return "";
281
+ const cleaned = str.replace(/[:?()*]/g, "").replace(/[-_/]+/g, " ").trim();
282
+ return cleaned ? cleaned.charAt(0).toUpperCase() + cleaned.slice(1) : "";
283
+ }
284
+ function lastSegment(pattern) {
285
+ const parts = pattern.split("/").filter(Boolean);
286
+ return parts[parts.length - 1] ?? "";
287
+ }
288
+ function titleForPage(page) {
289
+ return page.title || humanize(page.route_name ?? "") || humanize(lastSegment(page.path_pattern)) || page.path_pattern;
290
+ }
291
+ function emptyCounts() {
292
+ return { scenarios: 0, passing: 0, dev_validated: 0, client_validated: 0 };
293
+ }
294
+ function countsFor(scenarios) {
295
+ return {
296
+ scenarios: scenarios.length,
297
+ passing: scenarios.filter((s) => s.automated.status === "passing").length,
298
+ dev_validated: scenarios.filter((s) => s.dev.validated).length,
299
+ client_validated: scenarios.filter((s) => s.client.validated).length
300
+ };
301
+ }
302
+ function loadDevValidations(file) {
303
+ const out = /* @__PURE__ */ new Map();
304
+ if (!file || !existsSync2(file)) return out;
305
+ let parsed;
306
+ try {
307
+ parsed = JSON.parse(readFileSync3(file, "utf8"));
308
+ } catch (err) {
309
+ throw new Error(`qa build: failed to read dev validations ${file}: ${err instanceof Error ? err.message : String(err)}`);
310
+ }
311
+ const events = Array.isArray(parsed) ? parsed : Array.isArray(parsed.events) ? parsed.events : [];
312
+ for (const ev of events) {
313
+ if (!ev.external_key) continue;
314
+ const prev = out.get(ev.external_key);
315
+ if (!prev || (ev.at ?? "") >= (prev.at ?? "")) out.set(ev.external_key, ev);
316
+ }
317
+ return out;
318
+ }
319
+ function assembleScenario(seed, results, devEvents) {
320
+ let automated = { status: "never_run" };
321
+ if (seed.caseStatus === "covered" && seed.spec_path && seed.test_title) {
322
+ const lane = results.get(`${seed.spec_path}::${seed.test_title}`);
323
+ if (lane) {
324
+ automated = {
325
+ status: lane.status,
326
+ ...lane.at ? { at: lane.at } : {},
327
+ ...lane.provenance ? { provenance: lane.provenance } : {}
328
+ };
329
+ }
330
+ }
331
+ let dev = { ...EMPTY_HUMAN };
332
+ const ev = devEvents.get(seed.external_key);
333
+ if (ev) {
334
+ const stale = ev.validated ? isValidationStale({
335
+ ...ev.at ? { validatedAt: ev.at } : {},
336
+ automatedStatus: automated.status,
337
+ ...automated.at ? { automatedAt: automated.at } : {}
338
+ }) : false;
339
+ dev = {
340
+ validated: ev.validated === true,
341
+ stale,
342
+ ...ev.rejected ? { rejected: true } : {},
343
+ ...ev.by ? { by: ev.by } : {},
344
+ ...ev.at ? { at: ev.at } : {},
345
+ ...ev.app_version ? { app_version: ev.app_version } : {},
346
+ ...ev.note ? { note: ev.note } : {}
347
+ };
348
+ }
349
+ const client = { ...EMPTY_HUMAN };
350
+ const color = deriveScenarioColor(automated, dev, client);
351
+ return {
352
+ id: seed.id,
353
+ external_key: seed.external_key,
354
+ title: seed.title,
355
+ color,
356
+ source: seed.source,
357
+ ...seed.spec_path ? { spec_path: seed.spec_path } : {},
358
+ pages: seed.pages,
359
+ group: seed.group,
360
+ ...seed.priority ? { priority: seed.priority } : {},
361
+ ...seed.caseStatus ? { caseStatus: seed.caseStatus } : {},
362
+ ...seed.grounding ? { grounding: seed.grounding } : {},
363
+ automated,
364
+ dev,
365
+ client
366
+ };
367
+ }
368
+ function dedupSeeds(byPattern) {
369
+ const authoredKeys = /* @__PURE__ */ new Set();
370
+ for (const seeds of byPattern.values()) {
371
+ for (const seed of seeds) {
372
+ if (seed.origin === "authored" && seed.dedupKey) authoredKeys.add(seed.dedupKey);
373
+ }
374
+ }
375
+ const out = /* @__PURE__ */ new Map();
376
+ for (const [pattern, seeds] of byPattern) {
377
+ const kept = seeds.filter(
378
+ (seed) => !(seed.origin === "generated" && seed.dedupKey && authoredKeys.has(seed.dedupKey))
379
+ );
380
+ out.set(pattern, kept);
381
+ }
382
+ return out;
383
+ }
384
+ function primaryColors(scenarios, pattern) {
385
+ return scenarios.filter((s) => (s.pages ?? []).some((ref) => ref.pattern === pattern && ref.role === "primary")).map((s) => s.color);
386
+ }
387
+ function loadSitemap(file) {
388
+ let parsed;
389
+ try {
390
+ parsed = JSON.parse(readFileSync3(file, "utf8"));
391
+ } catch (err) {
392
+ throw new Error(`qa build: failed to read sitemap ${file}: ${err instanceof Error ? err.message : String(err)}`);
393
+ }
394
+ if (typeof parsed !== "object" || parsed === null || !Array.isArray(parsed.pages)) {
395
+ throw new Error(`qa build: sitemap ${file} is malformed (expected { format, pages: [] })`);
396
+ }
397
+ return parsed;
398
+ }
399
+ function buildArtifact(cfg, opts = {}) {
400
+ const appVersion = opts.appVersion ?? cfg.appVersion ?? FALLBACK_VERSION;
401
+ const generatedAt = opts.generatedAt ?? FALLBACK_VERSION;
402
+ const { byPattern: rawByPattern } = loadScenarioSeeds(cfg.suitesDir);
403
+ const byPattern = dedupSeeds(rawByPattern);
404
+ const results = cfg.resultsFile && existsSync2(cfg.resultsFile) ? readResults(cfg.resultsFile) : /* @__PURE__ */ new Map();
405
+ const devEvents = loadDevValidations(cfg.devValidationsFile);
406
+ const scenariosByPattern = /* @__PURE__ */ new Map();
407
+ for (const [pattern, seeds] of byPattern) {
408
+ scenariosByPattern.set(
409
+ pattern,
410
+ seeds.map((seed) => assembleScenario(seed, results, devEvents))
411
+ );
412
+ }
413
+ if (!cfg.sitemap) {
414
+ return buildDegraded(scenariosByPattern, cfg, appVersion, generatedAt);
415
+ }
416
+ return buildWithSitemap(loadSitemap(cfg.sitemap.file), scenariosByPattern, cfg, appVersion, generatedAt);
417
+ }
418
+ function pageStatusFor(pattern, title, scenarios) {
419
+ return {
420
+ page: { id: idFor(pattern), path_pattern: pattern, title },
421
+ color: aggregateColor(primaryColors(scenarios, pattern)),
422
+ counts: countsFor(scenarios),
423
+ scenarios
424
+ };
425
+ }
426
+ function pct(num, den) {
427
+ return den === 0 ? 0 : Math.round(100 * num / den);
428
+ }
429
+ function metricFor(pageStatuses, navigableTotal) {
430
+ const pagesWithScenarios = pageStatuses.filter((s) => s.counts.scenarios > 0).length;
431
+ const scenariosTotal = pageStatuses.reduce((n, s) => n + s.counts.scenarios, 0);
432
+ const scenariosPassing = pageStatuses.reduce((n, s) => n + s.counts.passing, 0);
433
+ return {
434
+ pages_with_scenarios_pct: pct(pagesWithScenarios, navigableTotal),
435
+ scenarios_passing_pct: pct(scenariosPassing, scenariosTotal)
436
+ };
437
+ }
438
+ function buildWithSitemap(sitemap, scenariosByPattern, cfg, appVersion, generatedAt) {
439
+ const allPages = sitemap.pages;
440
+ const navigable = allPages.filter((p) => !p.structural);
441
+ const titleByPattern = new Map(allPages.map((p) => [p.path_pattern, titleForPage(p)]));
442
+ const pageStatusByPattern = /* @__PURE__ */ new Map();
443
+ for (const page of navigable) {
444
+ const scenarios = scenariosByPattern.get(page.path_pattern) ?? [];
445
+ pageStatusByPattern.set(
446
+ page.path_pattern,
447
+ pageStatusFor(page.path_pattern, titleByPattern.get(page.path_pattern), scenarios)
448
+ );
449
+ }
450
+ const orphanPages = [];
451
+ for (const [pattern, scenarios] of scenariosByPattern) {
452
+ if (!pageStatusByPattern.has(pattern)) {
453
+ orphanPages.push(pageStatusFor(pattern, humanize(lastSegment(pattern)) || pattern, scenarios));
454
+ }
455
+ }
456
+ const pages = [...pageStatusByPattern.values(), ...orphanPages];
457
+ const childrenByParent = /* @__PURE__ */ new Map();
458
+ for (const p of allPages) {
459
+ const list = childrenByParent.get(p.parent);
460
+ if (list) list.push(p);
461
+ else childrenByParent.set(p.parent, [p]);
462
+ }
463
+ const subtree = (pattern) => {
464
+ const out = [pattern];
465
+ for (const child of childrenByParent.get(pattern) ?? []) out.push(...subtree(child.path_pattern));
466
+ return out;
467
+ };
468
+ const rollupOver = (statuses) => ({
469
+ pages_total: statuses.length,
470
+ pages_green: statuses.filter((s) => s.color === "green").length,
471
+ scenarios_total: statuses.reduce((n, s) => n + s.counts.scenarios, 0),
472
+ scenarios_green: statuses.reduce((n, s) => n + s.scenarios.filter((x) => x.color === "green").length, 0)
473
+ });
474
+ const nodes = allPages.map((page) => {
475
+ const status = pageStatusByPattern.get(page.path_pattern);
476
+ const ownCounts = status ? status.counts : emptyCounts();
477
+ const descStatuses = subtree(page.path_pattern).map((pat) => pageStatusByPattern.get(pat)).filter((s) => Boolean(s));
478
+ return {
479
+ id: idFor(page.path_pattern),
480
+ path_pattern: page.path_pattern,
481
+ title: titleByPattern.get(page.path_pattern),
482
+ // Map the input `parent` (a path_pattern) → that node's id; null stays a root.
483
+ parent_id: page.parent ? idFor(page.parent) : null,
484
+ order: page.order,
485
+ ...page.structural ? { structural: true } : {},
486
+ color: aggregateColor(descStatuses.map((s) => s.color)),
487
+ counts: ownCounts,
488
+ rollup: rollupOver(descStatuses)
489
+ };
490
+ });
491
+ const metric = metricFor(pages, navigable.length + orphanPages.length);
492
+ return {
493
+ format: 1,
494
+ environment: cfg.environment,
495
+ app_version: appVersion,
496
+ generated_at: generatedAt,
497
+ pages,
498
+ sitemap: nodes,
499
+ metric
500
+ };
501
+ }
502
+ function buildDegraded(scenariosByPattern, cfg, appVersion, generatedAt) {
503
+ const patterns = [...scenariosByPattern.keys()].sort((a, b) => a.localeCompare(b));
504
+ const pages = patterns.map(
505
+ (pattern) => pageStatusFor(pattern, humanize(lastSegment(pattern)) || pattern, scenariosByPattern.get(pattern))
506
+ );
507
+ const sitemap = pages.map((ps, i) => ({
508
+ id: ps.page.id,
509
+ path_pattern: ps.page.path_pattern,
510
+ title: ps.page.title,
511
+ parent_id: null,
512
+ order: i,
513
+ color: ps.color,
514
+ counts: ps.counts,
515
+ rollup: {
516
+ pages_total: 1,
517
+ pages_green: ps.color === "green" ? 1 : 0,
518
+ scenarios_total: ps.counts.scenarios,
519
+ scenarios_green: ps.scenarios.filter((x) => x.color === "green").length
520
+ }
521
+ }));
522
+ return {
523
+ format: 1,
524
+ environment: cfg.environment,
525
+ app_version: appVersion,
526
+ generated_at: generatedAt,
527
+ pages,
528
+ sitemap,
529
+ metric: metricFor(pages, pages.length)
530
+ };
531
+ }
532
+ function parseVersion(args) {
533
+ const i = args.indexOf("--version");
534
+ if (i >= 0 && args[i + 1]) return args[i + 1];
535
+ return void 0;
536
+ }
537
+ async function runBuild(args) {
538
+ const cfg = loadQaConfig(process.cwd());
539
+ const appVersion = parseVersion(args);
540
+ let artifact;
541
+ try {
542
+ artifact = buildArtifact(cfg, {
543
+ ...appVersion ? { appVersion } : {},
544
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString()
545
+ });
546
+ } catch (err) {
547
+ process.stderr.write(`${err instanceof Error ? err.message : String(err)}
548
+ `);
549
+ process.exitCode = 1;
550
+ return;
551
+ }
552
+ mkdirSync(dirname(cfg.outFile), { recursive: true });
553
+ writeFileSync(cfg.outFile, `${JSON.stringify(artifact, null, 2)}
554
+ `, "utf8");
555
+ process.stderr.write(
556
+ `qa build \u2192 ${cfg.outFile}
557
+ ${artifact.metric.pages_with_scenarios_pct}% pages with scenarios \xB7 ${artifact.metric.scenarios_passing_pct}% scenarios passing
558
+ `
559
+ );
560
+ }
561
+ export {
562
+ buildArtifact,
563
+ runBuild
564
+ };
565
+ //# sourceMappingURL=build-RCLWV2WL.js.map