@mozole/cli 1.4.3 → 2.0.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 (253) hide show
  1. package/LICENSE +16 -4
  2. package/README.md +85 -112
  3. package/dist/cli.d.ts +3 -2
  4. package/dist/cli.d.ts.map +1 -1
  5. package/dist/cli.js +231 -181
  6. package/dist/cli.js.map +1 -1
  7. package/dist/commands/adopt.d.ts +5 -10
  8. package/dist/commands/adopt.d.ts.map +1 -1
  9. package/dist/commands/adopt.js +49 -849
  10. package/dist/commands/adopt.js.map +1 -1
  11. package/dist/commands/doctor.d.ts +1 -7
  12. package/dist/commands/doctor.d.ts.map +1 -1
  13. package/dist/commands/doctor.js +69 -377
  14. package/dist/commands/doctor.js.map +1 -1
  15. package/dist/commands/new.d.ts +8 -10
  16. package/dist/commands/new.d.ts.map +1 -1
  17. package/dist/commands/new.js +32 -197
  18. package/dist/commands/new.js.map +1 -1
  19. package/dist/commands/phase.d.ts +11 -0
  20. package/dist/commands/phase.d.ts.map +1 -0
  21. package/dist/commands/phase.js +106 -0
  22. package/dist/commands/phase.js.map +1 -0
  23. package/dist/commands/prototype.d.ts +6 -0
  24. package/dist/commands/prototype.d.ts.map +1 -0
  25. package/dist/commands/prototype.js +28 -0
  26. package/dist/commands/prototype.js.map +1 -0
  27. package/dist/commands/repomap.d.ts +6 -0
  28. package/dist/commands/repomap.d.ts.map +1 -0
  29. package/dist/commands/repomap.js +71 -0
  30. package/dist/commands/repomap.js.map +1 -0
  31. package/dist/commands/test.d.ts +6 -0
  32. package/dist/commands/test.d.ts.map +1 -0
  33. package/dist/commands/test.js +115 -0
  34. package/dist/commands/test.js.map +1 -0
  35. package/dist/commands/ui.d.ts +6 -41
  36. package/dist/commands/ui.d.ts.map +1 -1
  37. package/dist/commands/ui.js +132 -459
  38. package/dist/commands/ui.js.map +1 -1
  39. package/dist/commands/verify.d.ts +12 -7
  40. package/dist/commands/verify.d.ts.map +1 -1
  41. package/dist/commands/verify.js +180 -65
  42. package/dist/commands/verify.js.map +1 -1
  43. package/dist/qa/a11y.d.ts +24 -0
  44. package/dist/qa/a11y.d.ts.map +1 -0
  45. package/dist/qa/a11y.js +124 -0
  46. package/dist/qa/a11y.js.map +1 -0
  47. package/dist/qa/breakpoints.d.ts +16 -0
  48. package/dist/qa/breakpoints.d.ts.map +1 -0
  49. package/dist/qa/breakpoints.js +45 -0
  50. package/dist/qa/breakpoints.js.map +1 -0
  51. package/dist/qa/contract.d.ts +21 -0
  52. package/dist/qa/contract.d.ts.map +1 -0
  53. package/dist/qa/contract.js +116 -0
  54. package/dist/qa/contract.js.map +1 -0
  55. package/dist/qa/index.d.ts +6 -0
  56. package/dist/qa/index.d.ts.map +1 -0
  57. package/dist/qa/index.js +6 -0
  58. package/dist/qa/index.js.map +1 -0
  59. package/dist/qa/probe.d.ts +48 -0
  60. package/dist/qa/probe.d.ts.map +1 -0
  61. package/dist/qa/probe.js +206 -0
  62. package/dist/qa/probe.js.map +1 -0
  63. package/dist/qa/runner.d.ts +27 -33
  64. package/dist/qa/runner.d.ts.map +1 -1
  65. package/dist/qa/runner.js +286 -332
  66. package/dist/qa/runner.js.map +1 -1
  67. package/dist/scaffold/agents.d.ts +5 -0
  68. package/dist/scaffold/agents.d.ts.map +1 -0
  69. package/dist/scaffold/agents.js +84 -0
  70. package/dist/scaffold/agents.js.map +1 -0
  71. package/dist/scaffold/backend.d.ts +6 -8
  72. package/dist/scaffold/backend.d.ts.map +1 -1
  73. package/dist/scaffold/backend.js +318 -199
  74. package/dist/scaffold/backend.js.map +1 -1
  75. package/dist/scaffold/design.d.ts +4 -3
  76. package/dist/scaffold/design.d.ts.map +1 -1
  77. package/dist/scaffold/design.js +38 -47
  78. package/dist/scaffold/design.js.map +1 -1
  79. package/dist/scaffold/flagship.d.ts +8 -0
  80. package/dist/scaffold/flagship.d.ts.map +1 -0
  81. package/dist/scaffold/flagship.js +337 -0
  82. package/dist/scaffold/flagship.js.map +1 -0
  83. package/dist/scaffold/index.d.ts +9 -0
  84. package/dist/scaffold/index.d.ts.map +1 -0
  85. package/dist/scaffold/index.js +48 -0
  86. package/dist/scaffold/index.js.map +1 -0
  87. package/dist/scaffold/phases.d.ts +10 -0
  88. package/dist/scaffold/phases.d.ts.map +1 -0
  89. package/dist/scaffold/phases.js +165 -0
  90. package/dist/scaffold/phases.js.map +1 -0
  91. package/dist/scaffold/repomap.d.ts +6 -0
  92. package/dist/scaffold/repomap.d.ts.map +1 -0
  93. package/dist/scaffold/repomap.js +89 -0
  94. package/dist/scaffold/repomap.js.map +1 -0
  95. package/dist/scaffold/standard.d.ts +13 -0
  96. package/dist/scaffold/standard.d.ts.map +1 -0
  97. package/dist/scaffold/standard.js +371 -0
  98. package/dist/scaffold/standard.js.map +1 -0
  99. package/dist/scaffold/tokens.d.ts +3 -0
  100. package/dist/scaffold/tokens.d.ts.map +1 -0
  101. package/dist/scaffold/tokens.js +126 -0
  102. package/dist/scaffold/tokens.js.map +1 -0
  103. package/dist/server/manager.d.ts +23 -0
  104. package/dist/server/manager.d.ts.map +1 -0
  105. package/dist/server/manager.js +96 -0
  106. package/dist/server/manager.js.map +1 -0
  107. package/dist/ui/CockpitApp.d.ts +13 -0
  108. package/dist/ui/CockpitApp.d.ts.map +1 -0
  109. package/dist/ui/CockpitApp.js +292 -0
  110. package/dist/ui/CockpitApp.js.map +1 -0
  111. package/dist/utils/ai-trace.d.ts +8 -0
  112. package/dist/utils/ai-trace.d.ts.map +1 -0
  113. package/dist/utils/ai-trace.js +69 -0
  114. package/dist/utils/ai-trace.js.map +1 -0
  115. package/dist/utils/browser.d.ts +10 -0
  116. package/dist/utils/browser.d.ts.map +1 -0
  117. package/dist/utils/browser.js +127 -0
  118. package/dist/utils/browser.js.map +1 -0
  119. package/dist/utils/fs.d.ts +18 -0
  120. package/dist/utils/fs.d.ts.map +1 -0
  121. package/dist/utils/fs.js +296 -0
  122. package/dist/utils/fs.js.map +1 -0
  123. package/dist/utils/git.d.ts +9 -0
  124. package/dist/utils/git.d.ts.map +1 -0
  125. package/dist/utils/git.js +69 -0
  126. package/dist/utils/git.js.map +1 -0
  127. package/dist/utils/process.d.ts +21 -0
  128. package/dist/utils/process.d.ts.map +1 -0
  129. package/dist/utils/process.js +120 -0
  130. package/dist/utils/process.js.map +1 -0
  131. package/dist/version.d.ts +21 -36
  132. package/dist/version.d.ts.map +1 -1
  133. package/dist/version.js +21 -44
  134. package/dist/version.js.map +1 -1
  135. package/package.json +22 -38
  136. package/CHANGELOG.md +0 -38
  137. package/dist/commands/cce.d.ts +0 -7
  138. package/dist/commands/cce.d.ts.map +0 -1
  139. package/dist/commands/cce.js +0 -37
  140. package/dist/commands/cce.js.map +0 -1
  141. package/dist/commands/logs.d.ts +0 -3
  142. package/dist/commands/logs.d.ts.map +0 -1
  143. package/dist/commands/logs.js +0 -40
  144. package/dist/commands/logs.js.map +0 -1
  145. package/dist/commands/qa.d.ts +0 -12
  146. package/dist/commands/qa.d.ts.map +0 -1
  147. package/dist/commands/qa.js +0 -143
  148. package/dist/commands/qa.js.map +0 -1
  149. package/dist/commands/stats.d.ts +0 -37
  150. package/dist/commands/stats.d.ts.map +0 -1
  151. package/dist/commands/stats.js +0 -169
  152. package/dist/commands/stats.js.map +0 -1
  153. package/dist/commands/template.d.ts +0 -8
  154. package/dist/commands/template.d.ts.map +0 -1
  155. package/dist/commands/template.js +0 -392
  156. package/dist/commands/template.js.map +0 -1
  157. package/dist/config/project.d.ts +0 -54
  158. package/dist/config/project.d.ts.map +0 -1
  159. package/dist/config/project.js +0 -100
  160. package/dist/config/project.js.map +0 -1
  161. package/dist/context/cce.d.ts +0 -39
  162. package/dist/context/cce.d.ts.map +0 -1
  163. package/dist/context/cce.js +0 -455
  164. package/dist/context/cce.js.map +0 -1
  165. package/dist/governor/fingerprint.d.ts +0 -7
  166. package/dist/governor/fingerprint.d.ts.map +0 -1
  167. package/dist/governor/fingerprint.js +0 -120
  168. package/dist/governor/fingerprint.js.map +0 -1
  169. package/dist/governor/hook.d.ts +0 -13
  170. package/dist/governor/hook.d.ts.map +0 -1
  171. package/dist/governor/hook.js +0 -389
  172. package/dist/governor/hook.js.map +0 -1
  173. package/dist/governor/state.d.ts +0 -45
  174. package/dist/governor/state.d.ts.map +0 -1
  175. package/dist/governor/state.js +0 -60
  176. package/dist/governor/state.js.map +0 -1
  177. package/dist/integrations/antigravity.d.ts +0 -20
  178. package/dist/integrations/antigravity.d.ts.map +0 -1
  179. package/dist/integrations/antigravity.js +0 -153
  180. package/dist/integrations/antigravity.js.map +0 -1
  181. package/dist/output-safety.d.ts +0 -3
  182. package/dist/output-safety.d.ts.map +0 -1
  183. package/dist/output-safety.js +0 -44
  184. package/dist/output-safety.js.map +0 -1
  185. package/dist/output.d.ts +0 -5
  186. package/dist/output.d.ts.map +0 -1
  187. package/dist/output.js +0 -2
  188. package/dist/output.js.map +0 -1
  189. package/dist/platform/executable.d.ts +0 -11
  190. package/dist/platform/executable.d.ts.map +0 -1
  191. package/dist/platform/executable.js +0 -69
  192. package/dist/platform/executable.js.map +0 -1
  193. package/dist/platform/process.d.ts +0 -27
  194. package/dist/platform/process.d.ts.map +0 -1
  195. package/dist/platform/process.js +0 -189
  196. package/dist/platform/process.js.map +0 -1
  197. package/dist/progress.d.ts +0 -44
  198. package/dist/progress.d.ts.map +0 -1
  199. package/dist/progress.js +0 -123
  200. package/dist/progress.js.map +0 -1
  201. package/dist/qa/artifacts.d.ts +0 -3
  202. package/dist/qa/artifacts.d.ts.map +0 -1
  203. package/dist/qa/artifacts.js +0 -15
  204. package/dist/qa/artifacts.js.map +0 -1
  205. package/dist/qa/diff.d.ts +0 -10
  206. package/dist/qa/diff.d.ts.map +0 -1
  207. package/dist/qa/diff.js +0 -90
  208. package/dist/qa/diff.js.map +0 -1
  209. package/dist/qa/server.d.ts +0 -10
  210. package/dist/qa/server.d.ts.map +0 -1
  211. package/dist/qa/server.js +0 -127
  212. package/dist/qa/server.js.map +0 -1
  213. package/dist/scaffold/adoption.d.ts +0 -17
  214. package/dist/scaffold/adoption.d.ts.map +0 -1
  215. package/dist/scaffold/adoption.js +0 -279
  216. package/dist/scaffold/adoption.js.map +0 -1
  217. package/dist/scaffold/legacy.d.ts +0 -4
  218. package/dist/scaffold/legacy.d.ts.map +0 -1
  219. package/dist/scaffold/legacy.js +0 -139
  220. package/dist/scaffold/legacy.js.map +0 -1
  221. package/dist/scaffold/routes.d.ts +0 -3
  222. package/dist/scaffold/routes.d.ts.map +0 -1
  223. package/dist/scaffold/routes.js +0 -37
  224. package/dist/scaffold/routes.js.map +0 -1
  225. package/dist/scaffold/standalone.d.ts +0 -7
  226. package/dist/scaffold/standalone.d.ts.map +0 -1
  227. package/dist/scaffold/standalone.js +0 -374
  228. package/dist/scaffold/standalone.js.map +0 -1
  229. package/dist/scaffold/templates.d.ts +0 -9
  230. package/dist/scaffold/templates.d.ts.map +0 -1
  231. package/dist/scaffold/templates.js +0 -317
  232. package/dist/scaffold/templates.js.map +0 -1
  233. package/dist/scaffold/write.d.ts +0 -2
  234. package/dist/scaffold/write.d.ts.map +0 -1
  235. package/dist/scaffold/write.js +0 -38
  236. package/dist/scaffold/write.js.map +0 -1
  237. package/dist/telemetry/store.d.ts +0 -33
  238. package/dist/telemetry/store.d.ts.map +0 -1
  239. package/dist/telemetry/store.js +0 -75
  240. package/dist/telemetry/store.js.map +0 -1
  241. package/dist/template/servers.d.ts +0 -21
  242. package/dist/template/servers.d.ts.map +0 -1
  243. package/dist/template/servers.js +0 -193
  244. package/dist/template/servers.js.map +0 -1
  245. package/dist/template/workspace.d.ts +0 -23
  246. package/dist/template/workspace.d.ts.map +0 -1
  247. package/dist/template/workspace.js +0 -152
  248. package/dist/template/workspace.js.map +0 -1
  249. package/dist/verify/runner.d.ts +0 -16
  250. package/dist/verify/runner.d.ts.map +0 -1
  251. package/dist/verify/runner.js +0 -112
  252. package/dist/verify/runner.js.map +0 -1
  253. package/docs/agent-audit.md +0 -24
package/dist/qa/runner.js CHANGED
@@ -1,364 +1,318 @@
1
- import { mkdir, writeFile } from "node:fs/promises";
1
+ import fs from "node:fs/promises";
2
+ import http from "node:http";
2
3
  import path from "node:path";
3
- import { chromium } from "playwright";
4
- import { updateTelemetry } from "../telemetry/store.js";
5
- import { visualArtifactKey } from "./artifacts.js";
6
- import { compareVisualArtifact } from "./diff.js";
7
- import { acquireServer } from "./server.js";
8
- export function formatVisualDiff(diff) {
9
- const percent = (diff.ratio * 100).toFixed(2);
10
- return `${diff.viewport.padEnd(11)} ${diff.route.padEnd(12)} ${percent}% changed (${diff.changedPixels}/${diff.totalPixels} px)${diff.diffPath ? ` -> ${diff.diffPath}` : ""}`;
11
- }
12
- export function qaResultPassed(issues, visualDiffs) {
13
- return issues.length === 0 && visualDiffs.length === 0;
14
- }
15
- function label(viewport) {
16
- return `${viewport.width}x${viewport.height}`;
17
- }
18
- export { visualArtifactSlug } from "./artifacts.js";
19
- async function inspectPage(page, baseUrl, route, viewport) {
20
- const issues = [];
21
- let probes = 0;
22
- const geometry = await page.evaluate(() => {
23
- const root = document.documentElement;
24
- const overflow = Math.max(root.scrollWidth, document.body?.scrollWidth ?? 0) -
25
- window.innerWidth;
26
- const offenders = [...document.querySelectorAll("body *")]
27
- .filter((element) => element.getBoundingClientRect().right > window.innerWidth + 1)
28
- .slice(0, 5)
29
- .map((element) => ({
30
- selector: element.id
31
- ? `#${element.id}`
32
- : element.classList.length
33
- ? `${element.tagName.toLowerCase()}.${[...element.classList].join(".")}`
34
- : element.tagName.toLowerCase(),
35
- pixels: Math.ceil(element.getBoundingClientRect().right - window.innerWidth),
36
- }));
37
- const unnamed = [
38
- ...document.querySelectorAll("button, input, select, textarea, [role=button], [role=link]"),
39
- ]
40
- .filter((element) => {
41
- const text = element.getAttribute("aria-label") ||
42
- element.getAttribute("aria-labelledby") ||
43
- element.getAttribute("title") ||
44
- element.textContent?.trim() ||
45
- element.name;
46
- return !text && !element.hasAttribute("disabled");
47
- })
48
- .slice(0, 5)
49
- .map((element) => element.outerHTML.slice(0, 120));
50
- const hiddenImportant = [
51
- ...document.querySelectorAll("[data-mozole-important]"),
52
- ]
53
- .filter((element) => {
54
- const box = element.getBoundingClientRect();
55
- const style = getComputedStyle(element);
56
- return (box.width === 0 ||
57
- box.height === 0 ||
58
- style.visibility === "hidden" ||
59
- style.display === "none");
60
- })
61
- .map((element) => element.id || element.className || element.tagName.toLowerCase());
62
- return {
63
- overflow,
64
- offenders,
65
- unnamed,
66
- hiddenImportant,
67
- title: document.title,
68
- lang: document.documentElement.lang,
69
- headings: document.querySelectorAll("h1").length,
70
- };
71
- });
72
- probes += 6;
73
- if (geometry.overflow > 1)
74
- issues.push({
75
- viewport,
76
- route,
77
- kind: "overflow",
78
- evidence: `${geometry.offenders[0]?.selector ?? "document"}: horizontal overflow ${Math.ceil(geometry.overflow)}px`,
79
- });
80
- if (!geometry.title)
81
- issues.push({
82
- viewport,
83
- route,
84
- kind: "accessibility",
85
- evidence: "document title is empty",
86
- });
87
- if (!geometry.lang)
88
- issues.push({
89
- viewport,
90
- route,
91
- kind: "accessibility",
92
- evidence: "html lang is missing",
93
- });
94
- if (!geometry.headings)
95
- issues.push({
96
- viewport,
97
- route,
98
- kind: "accessibility",
99
- evidence: "page has no h1",
100
- });
101
- if (geometry.unnamed.length)
102
- issues.push({
103
- viewport,
104
- route,
105
- kind: "accessibility",
106
- evidence: `unnamed interactive control: ${geometry.unnamed[0]}`,
107
- });
108
- for (const element of geometry.hiddenImportant)
109
- issues.push({
110
- viewport,
111
- route,
112
- kind: "visibility",
113
- evidence: `${element}: visible false`,
114
- });
115
- const links = await page.locator("a[href]").evaluateAll((nodes) => nodes
116
- .map((node) => node.href)
117
- .filter((href) => {
4
+ import pc from "picocolors";
5
+ import { chromium } from "playwright-core";
6
+ import { findAvailablePort } from "../server/manager.js";
7
+ import { ensureChromiumBrowser, getBrowserLaunchArgs } from "../utils/browser.js";
8
+ import { exists, findProjectRoot } from "../utils/fs.js";
9
+ import { extractBreakpointBoundaries } from "./breakpoints.js";
10
+ import { probeDOM } from "./probe.js";
11
+ const MIME_TYPES = {
12
+ ".html": "text/html; charset=utf-8",
13
+ ".css": "text/css; charset=utf-8",
14
+ ".js": "application/javascript; charset=utf-8",
15
+ ".mjs": "application/javascript; charset=utf-8",
16
+ ".json": "application/json; charset=utf-8",
17
+ ".svg": "image/svg+xml",
18
+ ".png": "image/png",
19
+ ".jpg": "image/jpeg",
20
+ ".jpeg": "image/jpeg",
21
+ ".webp": "image/webp",
22
+ ".woff2": "font/woff2",
23
+ ".woff": "font/woff",
24
+ ".ico": "image/x-icon",
25
+ };
26
+ async function createStaticServer(rootDirectory, port) {
27
+ const realRoot = await fs.realpath(rootDirectory).catch(() => path.resolve(rootDirectory));
28
+ const realRootPrefix = realRoot.endsWith(path.sep) ? realRoot : realRoot + path.sep;
29
+ const server = http.createServer(async (req, res) => {
118
30
  try {
119
- const url = new URL(href);
120
- return (url.origin === location.origin &&
121
- ["http:", "https:"].includes(url.protocol));
31
+ const parsedUrl = new URL(req.url ?? "/", `http://127.0.0.1:${port}`);
32
+ let reqPath;
33
+ try {
34
+ reqPath = decodeURIComponent(parsedUrl.pathname);
35
+ }
36
+ catch {
37
+ res.writeHead(400, { "Content-Type": "text/plain" });
38
+ res.end("Bad Request");
39
+ return;
40
+ }
41
+ const normalizedRoot = path.resolve(rootDirectory);
42
+ let filePath = path.resolve(normalizedRoot, `.${path.normalize(reqPath)}`);
43
+ // Prevent directory traversal attacks escaping rootDirectory
44
+ if (!filePath.startsWith(normalizedRoot + path.sep) && filePath !== normalizedRoot) {
45
+ res.writeHead(403, { "Content-Type": "text/plain" });
46
+ res.end("Forbidden: Access denied outside web root");
47
+ return;
48
+ }
49
+ let stat = await fs.stat(filePath).catch(() => null);
50
+ if (stat?.isDirectory()) {
51
+ filePath = path.join(filePath, "index.html");
52
+ stat = await fs.stat(filePath).catch(() => null);
53
+ }
54
+ // Route fallback: /about -> /about.html
55
+ if (!stat && !path.extname(reqPath)) {
56
+ const withHtml = path.resolve(normalizedRoot, `.${path.normalize(reqPath)}.html`);
57
+ if (withHtml.startsWith(normalizedRoot + path.sep)) {
58
+ stat = await fs.stat(withHtml).catch(() => null);
59
+ if (stat)
60
+ filePath = withHtml;
61
+ }
62
+ }
63
+ // SPA fallback to index.html
64
+ if (!stat) {
65
+ const spaFallback = path.join(normalizedRoot, "index.html");
66
+ stat = await fs.stat(spaFallback).catch(() => null);
67
+ if (stat)
68
+ filePath = spaFallback;
69
+ }
70
+ if (!stat || !stat.isFile()) {
71
+ res.writeHead(404, { "Content-Type": "text/plain" });
72
+ res.end("Not Found");
73
+ return;
74
+ }
75
+ const realTarget = await fs.realpath(filePath).catch(() => null);
76
+ if (!realTarget || (realTarget !== realRoot && !realTarget.startsWith(realRootPrefix))) {
77
+ res.writeHead(403, { "Content-Type": "text/plain" });
78
+ res.end("Forbidden: Access denied outside web root");
79
+ return;
80
+ }
81
+ const ext = path.extname(filePath).toLowerCase();
82
+ const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
83
+ const content = await fs.readFile(filePath);
84
+ res.writeHead(200, {
85
+ "Content-Type": contentType,
86
+ "Content-Length": content.length,
87
+ "Cache-Control": "no-cache",
88
+ });
89
+ res.end(content);
122
90
  }
123
91
  catch {
124
- return false;
92
+ res.writeHead(500, { "Content-Type": "text/plain" });
93
+ res.end("Internal Server Error");
125
94
  }
126
- }));
127
- const unique = [...new Set(links)].slice(0, 100);
128
- probes += unique.length;
129
- const chunkSize = 5;
130
- for (let i = 0; i < unique.length; i += chunkSize) {
131
- const chunk = unique.slice(i, i + chunkSize);
132
- await Promise.all(chunk.map(async (href) => {
133
- try {
134
- const response = await page.request.get(href, { timeout: 5_000 });
135
- if (response.status() >= 400) {
136
- let pathname = href;
137
- try {
138
- pathname = new URL(href).pathname;
139
- }
140
- catch { }
141
- issues.push({
142
- viewport,
143
- route,
144
- kind: "broken-link",
145
- evidence: `${pathname}: HTTP ${response.status()}`,
146
- });
95
+ });
96
+ await new Promise((resolve, reject) => {
97
+ server.listen(port, "127.0.0.1", () => resolve());
98
+ server.once("error", reject);
99
+ });
100
+ return {
101
+ serverUrl: `http://127.0.0.1:${port}`,
102
+ close: () => new Promise((resolve) => {
103
+ server.close(() => resolve());
104
+ }),
105
+ };
106
+ }
107
+ async function discoverRoutesAndDirectory(projectRoot, explicitStaticDir) {
108
+ let staticDir = explicitStaticDir;
109
+ if (!staticDir) {
110
+ const candidates = [
111
+ path.join(projectRoot, "build", "client"),
112
+ path.join(projectRoot, "dist"),
113
+ projectRoot,
114
+ ];
115
+ for (const dir of candidates) {
116
+ if (await exists(dir)) {
117
+ const indexHtml = path.join(dir, "index.html");
118
+ if (await exists(indexHtml)) {
119
+ staticDir = dir;
120
+ break;
147
121
  }
148
122
  }
149
- catch {
150
- let pathname = href;
151
- try {
152
- pathname = new URL(href, baseUrl).pathname;
123
+ }
124
+ }
125
+ if (!staticDir || !(await exists(staticDir))) {
126
+ throw new Error(`No built static directory found in ${projectRoot}. Run 'npm run build' before running live DOM probe.`);
127
+ }
128
+ const routes = new Set(["/"]);
129
+ async function scanHtmlRoutes(dir, base) {
130
+ if (!(await exists(dir)))
131
+ return;
132
+ const entries = await fs.readdir(dir, { withFileTypes: true });
133
+ for (const entry of entries) {
134
+ const full = path.join(dir, entry.name);
135
+ if (entry.isDirectory()) {
136
+ await scanHtmlRoutes(full, `${base}/${entry.name}`);
137
+ }
138
+ else if (entry.isFile() && entry.name.endsWith(".html")) {
139
+ if (entry.name === "__spa-fallback.html")
140
+ continue;
141
+ if (entry.name === "index.html") {
142
+ routes.add(base || "/");
143
+ }
144
+ else {
145
+ const routeName = entry.name.replace(/\.html$/, "");
146
+ routes.add(`${base}/${routeName}`);
153
147
  }
154
- catch { }
155
- issues.push({
156
- viewport,
157
- route,
158
- kind: "broken-link",
159
- evidence: `${pathname}: request failed`,
160
- });
161
148
  }
162
- }));
149
+ }
163
150
  }
164
- return { issues, probes };
151
+ await scanHtmlRoutes(staticDir, "");
152
+ return { staticDir, routes: Array.from(routes) };
165
153
  }
166
- async function interactionProbes(page, viewport, route) {
167
- const issues = [];
168
- let probes = 0;
169
- const details = page.locator("details");
170
- for (let index = 0; index < Math.min(await details.count(), 10); index++) {
171
- await details.nth(index).locator("summary").click();
172
- probes++;
173
- if (!(await details
174
- .nth(index)
175
- .evaluate((node) => node.open)))
176
- issues.push({
177
- viewport,
178
- route,
179
- kind: "interaction",
180
- evidence: `details[${index}] did not open`,
181
- });
182
- }
183
- const safe = page.locator("[data-mozole-qa=click]");
184
- for (let index = 0; index < Math.min(await safe.count(), 10); index++) {
185
- try {
186
- await safe.nth(index).click({ timeout: 2_000 });
187
- probes++;
154
+ export async function runLiveGeometryProbe(options = {}) {
155
+ const startTime = Date.now();
156
+ const cwd = options.cwd ?? process.cwd();
157
+ const projectRoot = (await findProjectRoot(cwd)) ?? cwd;
158
+ console.log(pc.cyan(`\n🌐 Launching Headless DOM Geometry & Trace Runner in ${projectRoot}...`));
159
+ // 1. Detect or provision Chromium/Brave
160
+ const browserInfo = await ensureChromiumBrowser();
161
+ console.log(pc.dim(` • Using browser engine: ${browserInfo.name}${browserInfo.version ? ` (${browserInfo.version})` : ""}`));
162
+ // 2. Discover built static files & routes
163
+ const { staticDir, routes: discoveredRoutes } = await discoverRoutesAndDirectory(projectRoot, options.staticDir);
164
+ const targetRoutes = options.routes ?? discoveredRoutes;
165
+ // 3. Extract CSS breakpoints to populate boundary viewports
166
+ const defaultViewports = [
167
+ { width: 320, height: 600, name: "Mobile Small (320px)" },
168
+ { width: 390, height: 844, name: "Mobile Standard (390px)" },
169
+ { width: 768, height: 1024, name: "Tablet (768px)" },
170
+ { width: 1280, height: 800, name: "Desktop (1280px)" },
171
+ ];
172
+ const cssList = [];
173
+ async function collectCss(dir) {
174
+ if (!(await exists(dir)))
175
+ return;
176
+ for (const e of await fs.readdir(dir, { withFileTypes: true })) {
177
+ const full = path.join(dir, e.name);
178
+ if (e.isDirectory() && !["node_modules", ".git"].includes(e.name)) {
179
+ await collectCss(full);
180
+ }
181
+ else if (e.isFile() && e.name.endsWith(".css")) {
182
+ cssList.push(await fs.readFile(full, "utf8"));
183
+ }
188
184
  }
189
- catch (error) {
190
- issues.push({
191
- viewport,
192
- route,
193
- kind: "interaction",
194
- evidence: `safe click ${index} failed: ${String(error).split("\n")[0]}`,
195
- });
185
+ }
186
+ await collectCss(staticDir);
187
+ const boundaryPoints = extractBreakpointBoundaries(cssList, { minWidth: 320, maxWidth: 1440 });
188
+ for (const bp of boundaryPoints) {
189
+ if (!defaultViewports.some((v) => v.width === bp)) {
190
+ defaultViewports.push({ width: bp, height: 800, name: `Breakpoint boundary (${bp}px)` });
196
191
  }
197
192
  }
198
- await page.keyboard.press("Tab");
199
- probes++;
200
- if (!(await page.evaluate(() => document.activeElement !== document.body)))
201
- issues.push({
202
- viewport,
203
- route,
204
- kind: "keyboard",
205
- evidence: "Tab did not move focus",
206
- });
207
- return { issues, probes };
208
- }
209
- export async function runQa(root, config, mode, signal) {
210
- const server = await acquireServer(root, config, signal);
193
+ defaultViewports.sort((a, b) => a.width - b.width);
194
+ const viewportsToTest = options.viewports ?? defaultViewports;
195
+ // 4. Start local static HTTP server
196
+ const port = options.port ?? (await findAvailablePort(4173));
197
+ const staticServer = await createStaticServer(staticDir, port);
198
+ const findings = [];
211
199
  let browser;
212
- const issues = [];
213
- const visualDiffs = [];
214
- let probes = 0;
215
- let screenshots = 0;
216
- let consoleErrors = 0;
217
- let failedRequests = 0;
200
+ let tracePath;
218
201
  try {
219
- browser = await chromium.launch({ headless: true });
220
- const artifactRoot = path.join(root, ".mozole", "qa");
221
- if (mode.visual)
222
- await mkdir(artifactRoot, { recursive: true });
223
- for (const viewport of config.qa.viewports) {
224
- signal?.throwIfAborted();
225
- const context = await browser.newContext({ viewport });
226
- const page = await context.newPage();
227
- const viewportName = label(viewport);
228
- page.on("console", (message) => {
229
- if (message.type() === "error") {
230
- consoleErrors++;
231
- issues.push({
232
- viewport: viewportName,
233
- route: page.url(),
234
- kind: "console",
235
- evidence: message.text().slice(0, 500),
236
- });
237
- }
238
- });
239
- page.on("pageerror", (error) => issues.push({
240
- viewport: viewportName,
241
- route: page.url(),
242
- kind: "runtime",
243
- evidence: error.message.slice(0, 500),
244
- }));
245
- page.on("requestfailed", (request) => {
246
- const failure = request.failure()?.errorText ?? "failed";
247
- if (!failure.includes("ERR_ABORTED")) {
248
- failedRequests++;
249
- issues.push({
250
- viewport: viewportName,
251
- route: page.url(),
252
- kind: "network",
253
- evidence: `${request.method()} ${request.url()} ${failure}`,
254
- });
202
+ browser = await chromium.launch({
203
+ executablePath: browserInfo.executablePath,
204
+ headless: true,
205
+ args: getBrowserLaunchArgs(),
206
+ });
207
+ const context = await browser.newContext({
208
+ serviceWorkers: "block",
209
+ });
210
+ // Zero-screenshot policy: snapshots = true, screenshots = false
211
+ await context.tracing.start({
212
+ screenshots: false,
213
+ snapshots: true,
214
+ sources: true,
215
+ });
216
+ const page = await context.newPage();
217
+ for (const route of targetRoutes) {
218
+ const url = `${staticServer.serverUrl}${route}`;
219
+ for (const vp of viewportsToTest) {
220
+ await page.setViewportSize({ width: vp.width, height: vp.height });
221
+ await page.goto(url, { waitUntil: "domcontentloaded" });
222
+ await page.waitForTimeout(50);
223
+ // Run live probe in browser context
224
+ const result = (await page.evaluate(probeDOM));
225
+ for (const [rule, issues] of Object.entries(result.issues)) {
226
+ for (const issue of issues) {
227
+ findings.push({
228
+ rule,
229
+ issue,
230
+ route,
231
+ viewport: vp,
232
+ });
233
+ }
255
234
  }
256
- });
257
- page.on("response", (response) => {
258
- if (response.status() >= 400) {
259
- failedRequests++;
260
- issues.push({
261
- viewport: viewportName,
262
- route: page.url(),
263
- kind: "network",
264
- evidence: `${response.request().method()} ${response.url()} HTTP ${response.status()}`,
235
+ // Stress test: 200% font size reflow check (capture sizes first to prevent compounding inheritance)
236
+ await page.evaluate(() => {
237
+ const elements = Array.from(document.querySelectorAll("body, body *"));
238
+ const sizes = elements.map((el) => Number.parseFloat(window.getComputedStyle(el).fontSize));
239
+ elements.forEach((el, i) => {
240
+ if (sizes[i] > 0) {
241
+ el.style.setProperty("font-size", `${sizes[i] * 2}px`, "important");
242
+ }
265
243
  });
266
- }
267
- });
268
- for (const route of config.routes) {
269
- signal?.throwIfAborted();
270
- const response = await page.goto(`${server.url}${route}`, {
271
- waitUntil: "networkidle",
272
- timeout: 30_000,
273
244
  });
274
- probes++;
275
- if (!response || response.status() >= 400) {
276
- issues.push({
277
- viewport: viewportName,
245
+ await page.waitForTimeout(50);
246
+ const stressResult = (await page.evaluate(probeDOM));
247
+ for (const issue of stressResult.issues.overflow) {
248
+ findings.push({
249
+ rule: "stress-overflow-200",
250
+ issue,
278
251
  route,
279
- kind: "route",
280
- evidence: `HTTP ${response?.status() ?? "no response"}`,
252
+ viewport: vp,
281
253
  });
282
- continue;
283
254
  }
284
- const inspected = await inspectPage(page, server.url, route, viewportName);
285
- issues.push(...inspected.issues);
286
- probes += inspected.probes;
287
- if (mode.interactive) {
288
- const interacted = await interactionProbes(page, viewportName, route);
289
- issues.push(...interacted.issues);
290
- probes += interacted.probes;
291
- }
292
- if (mode.visual) {
293
- const screenshotPath = path.join(artifactRoot, `${viewportName}-${visualArtifactKey(route)}.png`);
294
- const screenshot = await page.screenshot({ fullPage: true });
295
- if (mode.diff) {
296
- const comparison = await compareVisualArtifact(screenshotPath, screenshot, config.qa.visualDiffThreshold ?? 0.01);
297
- if (comparison.exceeded) {
298
- visualDiffs.push({
299
- viewport: viewportName,
300
- route,
301
- ratio: comparison.ratio,
302
- changedPixels: comparison.changedPixels,
303
- totalPixels: comparison.totalPixels,
304
- diffPath: comparison.diffPath
305
- ? path.relative(root, comparison.diffPath)
306
- : undefined,
307
- });
308
- }
309
- }
310
- else {
311
- await writeFile(screenshotPath, screenshot);
312
- }
313
- screenshots++;
255
+ for (const issue of stressResult.issues.clipping) {
256
+ findings.push({
257
+ rule: "stress-clipping-200",
258
+ issue,
259
+ route,
260
+ viewport: vp,
261
+ });
314
262
  }
315
263
  }
316
- await context.close();
317
264
  }
265
+ if (findings.length > 0 && options.saveTraceOnFailure !== false) {
266
+ const traceDir = path.join(projectRoot, "docs", "qa");
267
+ await fs.mkdir(traceDir, { recursive: true });
268
+ tracePath = options.tracePath ?? path.join(traceDir, "trace.zip");
269
+ await context.tracing.stop({ path: tracePath });
270
+ }
271
+ else {
272
+ await context.tracing.stop();
273
+ }
274
+ await context.close();
318
275
  }
319
276
  finally {
320
- await browser?.close();
321
- await server.stop();
277
+ if (browser) {
278
+ await browser.close().catch(() => { });
279
+ }
280
+ await staticServer.close().catch(() => { });
281
+ }
282
+ const durationMs = Date.now() - startTime;
283
+ if (findings.length > 0) {
284
+ console.log(pc.red(`\n ✗ Found ${findings.length} live DOM geometry / accessibility defect(s) during probe:`));
285
+ for (const f of findings.slice(0, 15)) {
286
+ console.log(pc.yellow(` [${f.rule.toUpperCase()}] ${f.route} @ ${f.viewport.width}px (${f.viewport.name ?? ""})`));
287
+ console.log(pc.dim(` Selector: ${f.issue.selector}`));
288
+ if (f.issue.problem)
289
+ console.log(pc.dim(` Problem: ${f.issue.problem}`));
290
+ if (f.issue.text)
291
+ console.log(pc.dim(` Text: "${f.issue.text}"`));
292
+ if (f.issue.width || f.issue.height) {
293
+ console.log(pc.dim(` Size: ${f.issue.width ?? 0}x${f.issue.height ?? 0}px`));
294
+ }
295
+ }
296
+ if (findings.length > 15) {
297
+ console.log(pc.dim(` ... and ${findings.length - 15} more findings.`));
298
+ }
299
+ if (tracePath) {
300
+ console.log(pc.cyan(`\n 📦 Trace recording saved to: ${tracePath}`));
301
+ console.log(pc.dim(" Inspect trace online at https://trace.playwright.dev (zero screenshots, full DOM snapshots & network)."));
302
+ }
303
+ }
304
+ else {
305
+ console.log(pc.green(` ✓ Live DOM geometry verified across ${targetRoutes.length} route(s) and ${viewportsToTest.length} viewport(s) with zero defects.`));
306
+ console.log(pc.dim(` • Verified in ${durationMs}ms with zero raster screenshots.`));
322
307
  }
323
- const unique = [
324
- ...new Map(issues.map((issue) => [
325
- `${issue.viewport}|${issue.route}|${issue.kind}|${issue.evidence}`,
326
- issue,
327
- ])).values(),
328
- ];
329
- const passed = qaResultPassed(unique, visualDiffs);
330
- const diffOutput = mode.diff
331
- ? visualDiffs.length
332
- ? `\n\nVISUAL DIFFS\n${visualDiffs.map(formatVisualDiff).join("\n")}`
333
- : "\n\nVISUAL DIFFS\nNo changes above threshold."
334
- : "";
335
- const output = (passed
336
- ? `QA PASS\n\n${config.qa.viewports.map((viewport) => `${label(viewport).padEnd(11)} ✓`).join("\n")}\n\nconsole errors ${consoleErrors}\nfailed requests ${failedRequests}\noverflow ${unique.filter((issue) => issue.kind === "overflow").length}\nbroken links ${unique.filter((issue) => issue.kind === "broken-link").length}${mode.visual ? `\nscreenshots ${screenshots}` : ""}`
337
- : `QA FAIL\n\n${unique
338
- .slice(0, 12)
339
- .map((issue) => `${issue.viewport} ${issue.route}\n${issue.evidence}`)
340
- .join("\n\n")}`) + diffOutput;
341
- if (config.telemetry?.enabled)
342
- await updateTelemetry(root, (telemetry) => {
343
- telemetry.qa.runs++;
344
- telemetry.qa.probes += probes;
345
- telemetry.qa.screenshots += screenshots;
346
- if (mode.visual)
347
- telemetry.qa.visualRuns++;
348
- telemetry.governor.modelFacingDiagnosticChars += output.length;
349
- });
350
308
  return {
351
- passed,
352
- issues: unique,
353
- viewports: config.qa.viewports.map(label),
354
- consoleErrors,
355
- failedRequests,
356
- overflows: unique.filter((issue) => issue.kind === "overflow").length,
357
- brokenLinks: unique.filter((issue) => issue.kind === "broken-link").length,
358
- probes,
359
- screenshots,
360
- visualDiffs,
361
- output,
309
+ success: findings.length === 0,
310
+ browserName: browserInfo.name,
311
+ findings,
312
+ routesChecked: targetRoutes,
313
+ viewportsChecked: viewportsToTest,
314
+ durationMs,
315
+ traceSavedTo: tracePath,
362
316
  };
363
317
  }
364
318
  //# sourceMappingURL=runner.js.map