@autoview/cli 0.1.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 (297) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +407 -0
  3. package/lib/AutoViewAgent.d.ts +109 -0
  4. package/lib/AutoViewAgent.js +123 -0
  5. package/lib/AutoViewAgent.js.map +1 -0
  6. package/lib/agent/emitMcpServer.d.ts +15 -0
  7. package/lib/agent/emitMcpServer.js +157 -0
  8. package/lib/agent/emitMcpServer.js.map +1 -0
  9. package/lib/agent/emitReport.d.ts +14 -0
  10. package/lib/agent/emitReport.js +85 -0
  11. package/lib/agent/emitReport.js.map +1 -0
  12. package/lib/agent/toolSurface.d.ts +130 -0
  13. package/lib/agent/toolSurface.js +342 -0
  14. package/lib/agent/toolSurface.js.map +1 -0
  15. package/lib/agent/verifyAgentTasks.d.ts +87 -0
  16. package/lib/agent/verifyAgentTasks.js +126 -0
  17. package/lib/agent/verifyAgentTasks.js.map +1 -0
  18. package/lib/cli/main.d.ts +2 -0
  19. package/lib/cli/main.js +295 -0
  20. package/lib/cli/main.js.map +1 -0
  21. package/lib/compiler/AutoViewInterfaceCompiler.d.ts +27 -0
  22. package/lib/compiler/AutoViewInterfaceCompiler.js +68 -0
  23. package/lib/compiler/AutoViewInterfaceCompiler.js.map +1 -0
  24. package/lib/constants/AutoViewFrontendTemplate.d.ts +1 -0
  25. package/lib/constants/AutoViewFrontendTemplate.js +46 -0
  26. package/lib/constants/AutoViewFrontendTemplate.js.map +1 -0
  27. package/lib/constants/AutoViewSystemPromptConstant.d.ts +5 -0
  28. package/lib/constants/AutoViewSystemPromptConstant.js +4 -0
  29. package/lib/constants/AutoViewSystemPromptConstant.js.map +1 -0
  30. package/lib/context/IAutoViewAgentContext.d.ts +60 -0
  31. package/lib/context/IAutoViewAgentContext.js +3 -0
  32. package/lib/context/IAutoViewAgentContext.js.map +1 -0
  33. package/lib/fromSwagger.d.ts +53 -0
  34. package/lib/fromSwagger.js +513 -0
  35. package/lib/fromSwagger.js.map +1 -0
  36. package/lib/generateDeterministic.d.ts +26 -0
  37. package/lib/generateDeterministic.js +75 -0
  38. package/lib/generateDeterministic.js.map +1 -0
  39. package/lib/index.d.ts +15 -0
  40. package/lib/index.js +41 -0
  41. package/lib/index.js.map +1 -0
  42. package/lib/orchestrate/orchestrateAutoView.d.ts +17 -0
  43. package/lib/orchestrate/orchestrateAutoView.js +491 -0
  44. package/lib/orchestrate/orchestrateAutoView.js.map +1 -0
  45. package/lib/orchestrate/orchestrateAutoViewProductPlan.d.ts +37 -0
  46. package/lib/orchestrate/orchestrateAutoViewProductPlan.js +109 -0
  47. package/lib/orchestrate/orchestrateAutoViewProductPlan.js.map +1 -0
  48. package/lib/orchestrate/orchestrateAutoViewRender.d.ts +133 -0
  49. package/lib/orchestrate/orchestrateAutoViewRender.js +943 -0
  50. package/lib/orchestrate/orchestrateAutoViewRender.js.map +1 -0
  51. package/lib/orchestrate/orchestrateAutoViewRenderDeterministic.d.ts +24 -0
  52. package/lib/orchestrate/orchestrateAutoViewRenderDeterministic.js +92 -0
  53. package/lib/orchestrate/orchestrateAutoViewRenderDeterministic.js.map +1 -0
  54. package/lib/orchestrate/orchestrateAutoViewReview.d.ts +48 -0
  55. package/lib/orchestrate/orchestrateAutoViewReview.js +328 -0
  56. package/lib/orchestrate/orchestrateAutoViewReview.js.map +1 -0
  57. package/lib/orchestrate/orchestrateAutoViewScaffold.d.ts +45 -0
  58. package/lib/orchestrate/orchestrateAutoViewScaffold.js +586 -0
  59. package/lib/orchestrate/orchestrateAutoViewScaffold.js.map +1 -0
  60. package/lib/orchestrate/orchestrateAutoViewSdkStudy.d.ts +26 -0
  61. package/lib/orchestrate/orchestrateAutoViewSdkStudy.js +85 -0
  62. package/lib/orchestrate/orchestrateAutoViewSdkStudy.js.map +1 -0
  63. package/lib/orchestrate/structures/IAutoViewProductPlan.d.ts +96 -0
  64. package/lib/orchestrate/structures/IAutoViewProductPlan.js +3 -0
  65. package/lib/orchestrate/structures/IAutoViewProductPlan.js.map +1 -0
  66. package/lib/orchestrate/structures/IAutoViewProductPlanApplication.d.ts +38 -0
  67. package/lib/orchestrate/structures/IAutoViewProductPlanApplication.js +3 -0
  68. package/lib/orchestrate/structures/IAutoViewProductPlanApplication.js.map +1 -0
  69. package/lib/orchestrate/structures/IAutoViewRenderApplication.d.ts +38 -0
  70. package/lib/orchestrate/structures/IAutoViewRenderApplication.js +3 -0
  71. package/lib/orchestrate/structures/IAutoViewRenderApplication.js.map +1 -0
  72. package/lib/orchestrate/structures/IAutoViewReviewApplication.d.ts +40 -0
  73. package/lib/orchestrate/structures/IAutoViewReviewApplication.js +3 -0
  74. package/lib/orchestrate/structures/IAutoViewReviewApplication.js.map +1 -0
  75. package/lib/orchestrate/structures/IAutoViewSdkMap.d.ts +63 -0
  76. package/lib/orchestrate/structures/IAutoViewSdkMap.js +3 -0
  77. package/lib/orchestrate/structures/IAutoViewSdkMap.js.map +1 -0
  78. package/lib/orchestrate/structures/IAutoViewSdkStudyApplication.d.ts +37 -0
  79. package/lib/orchestrate/structures/IAutoViewSdkStudyApplication.js +3 -0
  80. package/lib/orchestrate/structures/IAutoViewSdkStudyApplication.js.map +1 -0
  81. package/lib/orchestrate/utils/HistoryMessage.d.ts +10 -0
  82. package/lib/orchestrate/utils/HistoryMessage.js +25 -0
  83. package/lib/orchestrate/utils/HistoryMessage.js.map +1 -0
  84. package/lib/orchestrate/utils/auditFrontendRuntime.d.ts +53 -0
  85. package/lib/orchestrate/utils/auditFrontendRuntime.js +362 -0
  86. package/lib/orchestrate/utils/auditFrontendRuntime.js.map +1 -0
  87. package/lib/orchestrate/utils/buildDeterministicPlan.d.ts +4 -0
  88. package/lib/orchestrate/utils/buildDeterministicPlan.js +233 -0
  89. package/lib/orchestrate/utils/buildDeterministicPlan.js.map +1 -0
  90. package/lib/orchestrate/utils/buildDeterministicSdkMap.d.ts +22 -0
  91. package/lib/orchestrate/utils/buildDeterministicSdkMap.js +154 -0
  92. package/lib/orchestrate/utils/buildDeterministicSdkMap.js.map +1 -0
  93. package/lib/orchestrate/utils/cacheNodeModules.d.ts +31 -0
  94. package/lib/orchestrate/utils/cacheNodeModules.js +134 -0
  95. package/lib/orchestrate/utils/cacheNodeModules.js.map +1 -0
  96. package/lib/orchestrate/utils/describeEndpointPropsShape.d.ts +37 -0
  97. package/lib/orchestrate/utils/describeEndpointPropsShape.js +192 -0
  98. package/lib/orchestrate/utils/describeEndpointPropsShape.js.map +1 -0
  99. package/lib/orchestrate/utils/describeEndpointRequestBodyShape.d.ts +22 -0
  100. package/lib/orchestrate/utils/describeEndpointRequestBodyShape.js +29 -0
  101. package/lib/orchestrate/utils/describeEndpointRequestBodyShape.js.map +1 -0
  102. package/lib/orchestrate/utils/describeEndpointResponseShape.d.ts +19 -0
  103. package/lib/orchestrate/utils/describeEndpointResponseShape.js +30 -0
  104. package/lib/orchestrate/utils/describeEndpointResponseShape.js.map +1 -0
  105. package/lib/orchestrate/utils/executeCachedBatch.d.ts +22 -0
  106. package/lib/orchestrate/utils/executeCachedBatch.js +64 -0
  107. package/lib/orchestrate/utils/executeCachedBatch.js.map +1 -0
  108. package/lib/orchestrate/utils/loadShoppingFixture.d.ts +33 -0
  109. package/lib/orchestrate/utils/loadShoppingFixture.js +17 -0
  110. package/lib/orchestrate/utils/loadShoppingFixture.js.map +1 -0
  111. package/lib/orchestrate/utils/normalizeProductPlanPaths.d.ts +24 -0
  112. package/lib/orchestrate/utils/normalizeProductPlanPaths.js +77 -0
  113. package/lib/orchestrate/utils/normalizeProductPlanPaths.js.map +1 -0
  114. package/lib/orchestrate/utils/renderJsonSchema.d.ts +23 -0
  115. package/lib/orchestrate/utils/renderJsonSchema.js +122 -0
  116. package/lib/orchestrate/utils/renderJsonSchema.js.map +1 -0
  117. package/lib/orchestrate/utils/renderResourcePage.d.ts +36 -0
  118. package/lib/orchestrate/utils/renderResourcePage.js +1415 -0
  119. package/lib/orchestrate/utils/renderResourcePage.js.map +1 -0
  120. package/lib/orchestrate/utils/validateFrontendTypecheck.d.ts +109 -0
  121. package/lib/orchestrate/utils/validateFrontendTypecheck.js +274 -0
  122. package/lib/orchestrate/utils/validateFrontendTypecheck.js.map +1 -0
  123. package/lib/preview/renderPreview.d.ts +22 -0
  124. package/lib/preview/renderPreview.js +198 -0
  125. package/lib/preview/renderPreview.js.map +1 -0
  126. package/lib/typings/compiler.d.ts +39 -0
  127. package/lib/typings/compiler.js +3 -0
  128. package/lib/typings/compiler.js.map +1 -0
  129. package/lib/typings/events.d.ts +106 -0
  130. package/lib/typings/events.js +3 -0
  131. package/lib/typings/events.js.map +1 -0
  132. package/lib/typings/index.d.ts +10 -0
  133. package/lib/typings/index.js +27 -0
  134. package/lib/typings/index.js.map +1 -0
  135. package/lib/typings/misc.d.ts +78 -0
  136. package/lib/typings/misc.js +3 -0
  137. package/lib/typings/misc.js.map +1 -0
  138. package/lib/utils/ArrayUtil.d.ts +8 -0
  139. package/lib/utils/ArrayUtil.js +30 -0
  140. package/lib/utils/ArrayUtil.js.map +1 -0
  141. package/lib/utils/StringUtil.d.ts +11 -0
  142. package/lib/utils/StringUtil.js +28 -0
  143. package/lib/utils/StringUtil.js.map +1 -0
  144. package/lib/utils/classifyEndpoints.d.ts +62 -0
  145. package/lib/utils/classifyEndpoints.js +216 -0
  146. package/lib/utils/classifyEndpoints.js.map +1 -0
  147. package/lib/utils/endpointFilter.d.ts +26 -0
  148. package/lib/utils/endpointFilter.js +0 -0
  149. package/lib/utils/endpointFilter.js.map +1 -0
  150. package/lib/utils/extractFields.d.ts +85 -0
  151. package/lib/utils/extractFields.js +231 -0
  152. package/lib/utils/extractFields.js.map +1 -0
  153. package/lib/utils/index.d.ts +13 -0
  154. package/lib/utils/index.js +30 -0
  155. package/lib/utils/index.js.map +1 -0
  156. package/lib/utils/normalizeForNestia.d.ts +34 -0
  157. package/lib/utils/normalizeForNestia.js +133 -0
  158. package/lib/utils/normalizeForNestia.js.map +1 -0
  159. package/lib/utils/resourcePlan.d.ts +39 -0
  160. package/lib/utils/resourcePlan.js +95 -0
  161. package/lib/utils/resourcePlan.js.map +1 -0
  162. package/lib/utils/sliceDocument.d.ts +17 -0
  163. package/lib/utils/sliceDocument.js +114 -0
  164. package/lib/utils/sliceDocument.js.map +1 -0
  165. package/lib/utils/toEndpoints.d.ts +90 -0
  166. package/lib/utils/toEndpoints.js +227 -0
  167. package/lib/utils/toEndpoints.js.map +1 -0
  168. package/lib/verify/runWorkflows.d.ts +25 -0
  169. package/lib/verify/runWorkflows.js +366 -0
  170. package/lib/verify/runWorkflows.js.map +1 -0
  171. package/lib/verify/workflows.d.ts +53 -0
  172. package/lib/verify/workflows.js +107 -0
  173. package/lib/verify/workflows.js.map +1 -0
  174. package/package.json +82 -0
  175. package/prompts/AUTOVIEW_RENDER.md +398 -0
  176. package/prompts/AUTOVIEW_REVIEW.md +60 -0
  177. package/prompts/AUTOVIEW_SDK_STUDY.md +89 -0
  178. package/src/AutoViewAgent.ts +222 -0
  179. package/src/agent/emitMcpServer.integration.test.ts +168 -0
  180. package/src/agent/emitMcpServer.test.ts +51 -0
  181. package/src/agent/emitMcpServer.ts +178 -0
  182. package/src/agent/emitReport.ts +117 -0
  183. package/src/agent/toolSurface.test.ts +243 -0
  184. package/src/agent/toolSurface.ts +501 -0
  185. package/src/agent/verifyAgentTasks.test.ts +106 -0
  186. package/src/agent/verifyAgentTasks.ts +171 -0
  187. package/src/cli/main.ts +363 -0
  188. package/src/compiler/AutoViewInterfaceCompiler.ts +69 -0
  189. package/src/constants/AutoViewFrontendTemplate.ts +42 -0
  190. package/src/constants/AutoViewSystemPromptConstant.ts +6 -0
  191. package/src/context/IAutoViewAgentContext.ts +84 -0
  192. package/src/fromSwagger.test.ts +269 -0
  193. package/src/fromSwagger.ts +500 -0
  194. package/src/generateDeterministic.test.ts +39 -0
  195. package/src/generateDeterministic.ts +77 -0
  196. package/src/index.ts +30 -0
  197. package/src/orchestrate/orchestrateAutoView.ts +590 -0
  198. package/src/orchestrate/orchestrateAutoViewProductPlan.ts +121 -0
  199. package/src/orchestrate/orchestrateAutoViewRender.ts +1117 -0
  200. package/src/orchestrate/orchestrateAutoViewRenderDeterministic.ts +101 -0
  201. package/src/orchestrate/orchestrateAutoViewReview.ts +272 -0
  202. package/src/orchestrate/orchestrateAutoViewScaffold.ts +627 -0
  203. package/src/orchestrate/orchestrateAutoViewSdkStudy.ts +90 -0
  204. package/src/orchestrate/renderNavTs.test.ts +74 -0
  205. package/src/orchestrate/structures/IAutoViewProductPlan.ts +119 -0
  206. package/src/orchestrate/structures/IAutoViewProductPlanApplication.ts +41 -0
  207. package/src/orchestrate/structures/IAutoViewRenderApplication.ts +40 -0
  208. package/src/orchestrate/structures/IAutoViewReviewApplication.ts +42 -0
  209. package/src/orchestrate/structures/IAutoViewSdkMap.ts +72 -0
  210. package/src/orchestrate/structures/IAutoViewSdkStudyApplication.ts +40 -0
  211. package/src/orchestrate/utils/HistoryMessage.ts +41 -0
  212. package/src/orchestrate/utils/auditFrontendRuntime.test.ts +18 -0
  213. package/src/orchestrate/utils/auditFrontendRuntime.ts +454 -0
  214. package/src/orchestrate/utils/buildDeterministicPlan.test.ts +170 -0
  215. package/src/orchestrate/utils/buildDeterministicPlan.ts +289 -0
  216. package/src/orchestrate/utils/buildDeterministicSdkMap.test.ts +90 -0
  217. package/src/orchestrate/utils/buildDeterministicSdkMap.ts +169 -0
  218. package/src/orchestrate/utils/cacheNodeModules.ts +136 -0
  219. package/src/orchestrate/utils/describeEndpointPropsShape.test.ts +86 -0
  220. package/src/orchestrate/utils/describeEndpointPropsShape.ts +202 -0
  221. package/src/orchestrate/utils/describeEndpointRequestBodyShape.test.ts +87 -0
  222. package/src/orchestrate/utils/describeEndpointRequestBodyShape.ts +31 -0
  223. package/src/orchestrate/utils/describeEndpointResponseShape.test.ts +70 -0
  224. package/src/orchestrate/utils/describeEndpointResponseShape.ts +32 -0
  225. package/src/orchestrate/utils/executeCachedBatch.ts +59 -0
  226. package/src/orchestrate/utils/loadShoppingFixture.ts +52 -0
  227. package/src/orchestrate/utils/normalizeProductPlanPaths.ts +92 -0
  228. package/src/orchestrate/utils/renderJsonSchema.test.ts +162 -0
  229. package/src/orchestrate/utils/renderJsonSchema.ts +133 -0
  230. package/src/orchestrate/utils/renderResourcePage.test.ts +468 -0
  231. package/src/orchestrate/utils/renderResourcePage.ts +1624 -0
  232. package/src/orchestrate/utils/validateFrontendTypecheck.test.ts +32 -0
  233. package/src/orchestrate/utils/validateFrontendTypecheck.ts +335 -0
  234. package/src/preview/renderPreview.ts +273 -0
  235. package/src/typings/compiler.ts +47 -0
  236. package/src/typings/events.ts +155 -0
  237. package/src/typings/index.ts +10 -0
  238. package/src/typings/misc.ts +93 -0
  239. package/src/utils/ArrayUtil.ts +16 -0
  240. package/src/utils/StringUtil.ts +29 -0
  241. package/src/utils/classifyEndpoints.test.ts +86 -0
  242. package/src/utils/classifyEndpoints.ts +291 -0
  243. package/src/utils/endpointFilter.test.ts +50 -0
  244. package/src/utils/endpointFilter.ts +0 -0
  245. package/src/utils/extractFields.test.ts +82 -0
  246. package/src/utils/extractFields.ts +306 -0
  247. package/src/utils/index.ts +13 -0
  248. package/src/utils/normalizeForNestia.test.ts +93 -0
  249. package/src/utils/normalizeForNestia.ts +139 -0
  250. package/src/utils/resourcePlan.test.ts +104 -0
  251. package/src/utils/resourcePlan.ts +180 -0
  252. package/src/utils/sliceDocument.test.ts +85 -0
  253. package/src/utils/sliceDocument.ts +119 -0
  254. package/src/utils/toEndpoints.test.ts +251 -0
  255. package/src/utils/toEndpoints.ts +343 -0
  256. package/src/verify/runWorkflows.ts +403 -0
  257. package/src/verify/workflows.test.ts +117 -0
  258. package/src/verify/workflows.ts +154 -0
  259. package/template/CLAUDE.md +140 -0
  260. package/template/Dockerfile +31 -0
  261. package/template/PROMPT.md +80 -0
  262. package/template/SANDBOX.md +70 -0
  263. package/template/app/api/health/route.ts +10 -0
  264. package/template/app/globals.css +97 -0
  265. package/template/app/layout.tsx +30 -0
  266. package/template/app/page.tsx +19 -0
  267. package/template/components/AppShell.tsx +114 -0
  268. package/template/components/auto/CatalogGrid.tsx +159 -0
  269. package/template/components/auto/ConfirmButton.tsx +67 -0
  270. package/template/components/auto/EmbeddedCollection.tsx +144 -0
  271. package/template/components/auto/ResourceDashboard.tsx +104 -0
  272. package/template/components/auto/ResourceDetail.tsx +93 -0
  273. package/template/components/auto/ResourceForm.tsx +235 -0
  274. package/template/components/auto/ResourceIcon.tsx +88 -0
  275. package/template/components/auto/ResourceLanding.tsx +155 -0
  276. package/template/components/auto/ResourceTable.tsx +223 -0
  277. package/template/components/auto/formatValue.tsx +186 -0
  278. package/template/components/auto/types.ts +42 -0
  279. package/template/components/ui/badge.tsx +40 -0
  280. package/template/components/ui/button.tsx +57 -0
  281. package/template/components/ui/card.tsx +86 -0
  282. package/template/components/ui/dialog.tsx +119 -0
  283. package/template/components/ui/input.tsx +23 -0
  284. package/template/components/ui/label.tsx +24 -0
  285. package/template/components/ui/pagination.tsx +117 -0
  286. package/template/components/ui/select.tsx +92 -0
  287. package/template/components/ui/sheet.tsx +135 -0
  288. package/template/components/ui/skeleton.tsx +15 -0
  289. package/template/components/ui/table.tsx +120 -0
  290. package/template/components/ui/tabs.tsx +55 -0
  291. package/template/lib/utils.ts +35 -0
  292. package/template/next.config.mjs +52 -0
  293. package/template/package.json +46 -0
  294. package/template/postcss.config.js +6 -0
  295. package/template/scripts/start-shopping-backend.sh +56 -0
  296. package/template/tailwind.config.ts +96 -0
  297. package/template/tsconfig.json +29 -0
@@ -0,0 +1,454 @@
1
+ import { type ChildProcess, spawn } from "child_process";
2
+ import { createHash } from "crypto";
3
+ import fs from "fs/promises";
4
+ import { createServer } from "net";
5
+ import os from "os";
6
+ import path from "path";
7
+
8
+ import {
9
+ ensureCachedNodeModules,
10
+ tryLinkNodeModules,
11
+ } from "./cacheNodeModules";
12
+
13
+ const PLAYWRIGHT_DEP = "playwright@^1.50.0";
14
+
15
+ /**
16
+ * Per-page runtime defect captured by the Playwright audit. `console` covers
17
+ * `console.error` calls the page made during boot; `pageerror` covers uncaught
18
+ * exceptions; `navigation` covers Playwright's own `page.goto` failure
19
+ * (timeout, net::ERR, etc.) so the orchestrator sees the page as broken even
20
+ * when the runtime did not get a chance to fire a console message.
21
+ */
22
+ export interface IRuntimeAuditDiagnostic {
23
+ /** Screen path the agent emitted (e.g. `/cart`, `/sales/[id]`). */
24
+ pagePath: string;
25
+ /** Category of failure. */
26
+ type: "console" | "pageerror" | "navigation";
27
+ /** Human-readable message — kept short so the retry prompt stays focused. */
28
+ message: string;
29
+ }
30
+
31
+ export interface IAuditFrontendRuntimeResult {
32
+ /** Flat list of every diagnostic captured across all visited pages. */
33
+ diagnostics: IRuntimeAuditDiagnostic[];
34
+ /**
35
+ * Convenience map keyed by screen path → diagnostics on that page. Pages that
36
+ * booted without any console / pageerror / navigation issue are absent from
37
+ * the map (use {@link visited} for the full visit list).
38
+ */
39
+ byPage: Map<string, IRuntimeAuditDiagnostic[]>;
40
+ /**
41
+ * Screen paths that the audit attempted (`length === pages.length` on
42
+ * success).
43
+ */
44
+ visited: string[];
45
+ /** Markdown body for `wiki/runtime-audit.md`. */
46
+ markdown: string;
47
+ /** Wall-clock duration of the whole audit (install + boot + visits + cleanup). */
48
+ elapsedMs: number;
49
+ }
50
+
51
+ const NEXT_DEV_BOOT_TIMEOUT_MS = 90_000;
52
+ const PAGE_VISIT_TIMEOUT_MS = 20_000;
53
+ const PAGE_SETTLE_MS = 2_500;
54
+
55
+ /**
56
+ * Boot the assembled frontend under `next dev`, launch a headless Chromium via
57
+ * Playwright, visit every screen the product plan declared, and return the
58
+ * per-page console / pageerror / navigation diagnostics. Used by the
59
+ * orchestrator as Phase 6 (runtime audit) — the layer above `tsc --noEmit` that
60
+ * catches errors typecheck cannot see (`obj?.array.method(...)`
61
+ * undefined-access patterns, missing client-only guards, react render
62
+ * crashes).
63
+ *
64
+ * Operates in `NEXT_PUBLIC_API_SIMULATE=true` so the audit does not require a
65
+ * live backend — the SDK serves typia-random fixture data and pages exercise
66
+ * their real render paths. Live-backend variants can be added later by
67
+ * threading the host through.
68
+ *
69
+ * Throws on infrastructure failure (`npm install` crashed, port allocation
70
+ * failed, Chromium download blocked). Runtime errors on the pages themselves
71
+ * are returned as data; the whole point of this gate is to surface them.
72
+ */
73
+ export async function auditFrontendRuntime(
74
+ files: Record<string, string>,
75
+ pages: string[],
76
+ ): Promise<IAuditFrontendRuntimeResult> {
77
+ const startedAt = Date.now();
78
+ const tempDir = await fs.mkdtemp(
79
+ path.join(os.tmpdir(), "autoview-runtime-audit-"),
80
+ );
81
+ let nextDev: ChildProcess | null = null;
82
+ try {
83
+ await writeFiles(tempDir, files);
84
+ const packageJson = files["package.json"];
85
+ const hash =
86
+ packageJson !== undefined
87
+ ? createHash("sha256").update(packageJson).digest("hex").slice(0, 16)
88
+ : "no-package-json";
89
+ const cachedNodeModules =
90
+ packageJson !== undefined
91
+ ? await ensureCachedNodeModules({
92
+ cacheNamespace: "runtime-audit",
93
+ hash,
94
+ packageJson,
95
+ extraDeps: [PLAYWRIGHT_DEP],
96
+ })
97
+ : null;
98
+ if (cachedNodeModules !== null) {
99
+ const linked = await tryLinkNodeModules(tempDir, cachedNodeModules);
100
+ if (!linked) {
101
+ await installDependencies(tempDir);
102
+ await installPlaywright(tempDir);
103
+ }
104
+ } else {
105
+ await installDependencies(tempDir);
106
+ await installPlaywright(tempDir);
107
+ }
108
+ await installChromium(tempDir);
109
+
110
+ const port = await getFreePort();
111
+ nextDev = await startNextDev(tempDir, port);
112
+ const baseUrl = `http://127.0.0.1:${port}`;
113
+
114
+ await writeFile(tempDir, "scripts/.audit-runner.cjs", buildRunnerScript());
115
+ const stdout = await spawnCommand(
116
+ "node",
117
+ [
118
+ path.join(tempDir, "scripts", ".audit-runner.cjs"),
119
+ JSON.stringify(pages),
120
+ baseUrl,
121
+ String(PAGE_VISIT_TIMEOUT_MS),
122
+ String(PAGE_SETTLE_MS),
123
+ ],
124
+ { cwd: tempDir },
125
+ );
126
+
127
+ const diagnostics = parseRunnerOutput(stdout);
128
+ const byPage = groupByPage(diagnostics);
129
+ return {
130
+ diagnostics,
131
+ byPage,
132
+ visited: [...pages],
133
+ markdown: renderMarkdownReport(diagnostics, byPage, pages),
134
+ elapsedMs: Date.now() - startedAt,
135
+ };
136
+ } finally {
137
+ if (nextDev !== null) await killNextDev(nextDev);
138
+ await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {});
139
+ }
140
+ }
141
+
142
+ /* -------------------------------------------------------------------------- */
143
+ /* fs */
144
+ /* -------------------------------------------------------------------------- */
145
+
146
+ async function writeFiles(
147
+ root: string,
148
+ files: Record<string, string>,
149
+ ): Promise<void> {
150
+ for (const [relative, content] of Object.entries(files)) {
151
+ await writeFile(root, relative, content);
152
+ }
153
+ }
154
+
155
+ async function writeFile(
156
+ root: string,
157
+ relative: string,
158
+ content: string,
159
+ ): Promise<void> {
160
+ const full = path.join(root, relative);
161
+ await fs.mkdir(path.dirname(full), { recursive: true });
162
+ await fs.writeFile(full, content, "utf-8");
163
+ }
164
+
165
+ /* -------------------------------------------------------------------------- */
166
+ /* install */
167
+ /* -------------------------------------------------------------------------- */
168
+
169
+ async function installDependencies(root: string): Promise<void> {
170
+ await spawnCommand(
171
+ "npm",
172
+ ["install", "--silent", "--no-audit", "--no-fund"],
173
+ { cwd: root },
174
+ );
175
+ }
176
+
177
+ async function installPlaywright(root: string): Promise<void> {
178
+ await spawnCommand(
179
+ "npm",
180
+ [
181
+ "install",
182
+ "--silent",
183
+ "--no-audit",
184
+ "--no-fund",
185
+ "--no-save",
186
+ "playwright@^1.50.0",
187
+ ],
188
+ { cwd: root },
189
+ );
190
+ }
191
+
192
+ async function installChromium(root: string): Promise<void> {
193
+ await spawnCommand(
194
+ path.join(root, "node_modules", ".bin", "playwright"),
195
+ ["install", "chromium"],
196
+ { cwd: root },
197
+ );
198
+ }
199
+
200
+ /* -------------------------------------------------------------------------- */
201
+ /* next dev */
202
+ /* -------------------------------------------------------------------------- */
203
+
204
+ function getFreePort(): Promise<number> {
205
+ return new Promise((resolve, reject) => {
206
+ const server = createServer();
207
+ server.listen(0, "127.0.0.1", () => {
208
+ const addr = server.address();
209
+ if (addr === null || typeof addr === "string") {
210
+ server.close();
211
+ reject(new Error("Failed to allocate a free port"));
212
+ return;
213
+ }
214
+ const port = addr.port;
215
+ server.close(() => resolve(port));
216
+ });
217
+ server.on("error", reject);
218
+ });
219
+ }
220
+
221
+ function startNextDev(root: string, port: number): Promise<ChildProcess> {
222
+ return new Promise((resolve, reject) => {
223
+ const child = spawn(
224
+ "node",
225
+ [path.join(root, "node_modules", "next", "dist", "bin", "next"), "dev"],
226
+ {
227
+ cwd: root,
228
+ env: {
229
+ ...process.env,
230
+ PORT: String(port),
231
+ NEXT_PUBLIC_API_SIMULATE: "true",
232
+ // Force literal Authorization header so the SDK does not crash
233
+ // when bootstrapAuth is a no-op in simulator mode.
234
+ NEXT_PUBLIC_API_TOKEN: "Bearer preview",
235
+ },
236
+ stdio: ["ignore", "pipe", "pipe"],
237
+ },
238
+ );
239
+
240
+ let buffer = "";
241
+ let settled = false;
242
+ const onData = (chunk: Buffer): void => {
243
+ buffer += chunk.toString("utf-8");
244
+ if (!settled && buffer.includes("Ready in")) {
245
+ settled = true;
246
+ child.stdout?.off("data", onData);
247
+ child.stderr?.off("data", onData);
248
+ resolve(child);
249
+ }
250
+ };
251
+ child.stdout?.on("data", onData);
252
+ child.stderr?.on("data", onData);
253
+
254
+ const timer = setTimeout(() => {
255
+ if (settled) return;
256
+ settled = true;
257
+ child.kill("SIGTERM");
258
+ reject(
259
+ new Error(
260
+ `next dev did not become ready within ${NEXT_DEV_BOOT_TIMEOUT_MS}ms:\n${buffer.slice(-1000)}`,
261
+ ),
262
+ );
263
+ }, NEXT_DEV_BOOT_TIMEOUT_MS);
264
+
265
+ child.on("exit", (code) => {
266
+ clearTimeout(timer);
267
+ if (settled) return;
268
+ settled = true;
269
+ reject(
270
+ new Error(
271
+ `next dev exited with code ${code} before becoming ready:\n${buffer.slice(-1000)}`,
272
+ ),
273
+ );
274
+ });
275
+ });
276
+ }
277
+
278
+ async function killNextDev(child: ChildProcess): Promise<void> {
279
+ if (child.exitCode !== null) return;
280
+ child.kill("SIGTERM");
281
+ await new Promise((r) => setTimeout(r, 1500));
282
+ if (child.exitCode === null) child.kill("SIGKILL");
283
+ }
284
+
285
+ /* -------------------------------------------------------------------------- */
286
+ /* runner script (executed inside the temp dir, requires installed playwright) */
287
+ /* -------------------------------------------------------------------------- */
288
+
289
+ function buildRunnerScript(): string {
290
+ // CommonJS so the temp dir does not need to be ESM-aware. Reads pages +
291
+ // baseUrl + timeouts from argv, emits a JSON array of `{ pagePath, type,
292
+ // message }` to stdout. Failures during navigation become `navigation`
293
+ // diagnostics so the caller never silently misses a broken page.
294
+ return `"use strict";
295
+ const { chromium } = require("playwright");
296
+
297
+ (async () => {
298
+ const pages = JSON.parse(process.argv[2]);
299
+ const baseUrl = process.argv[3];
300
+ const visitTimeout = parseInt(process.argv[4], 10);
301
+ const settleMs = parseInt(process.argv[5], 10);
302
+ const browser = await chromium.launch();
303
+ const context = await browser.newContext({
304
+ viewport: { width: 1280, height: 800 },
305
+ });
306
+ const diagnostics = [];
307
+ for (const pagePath of pages) {
308
+ const resolved = pagePath.replace(/\\[[^\\]]+\\]/g, "demo");
309
+ const url = baseUrl + (resolved.startsWith("/") ? resolved : "/" + resolved);
310
+ const page = await context.newPage();
311
+ const pageErrors = [];
312
+ page.on("console", (msg) => {
313
+ if (msg.type() === "error") {
314
+ pageErrors.push({ pagePath, type: "console", message: msg.text() });
315
+ }
316
+ });
317
+ page.on("pageerror", (err) => {
318
+ pageErrors.push({ pagePath, type: "pageerror", message: err.message });
319
+ });
320
+ try {
321
+ // \`domcontentloaded\` instead of \`networkidle\` because typia random
322
+ // simulator data keeps the SDK firing background requests indefinitely
323
+ // on data-heavy pages — \`networkidle\` then times out at 20s on a
324
+ // page that already rendered cleanly. DOMContentLoaded plus the
325
+ // explicit settle window catches the same render errors without
326
+ // mistaking simulator chatter for a broken page.
327
+ await page.goto(url, {
328
+ waitUntil: "domcontentloaded",
329
+ timeout: visitTimeout,
330
+ });
331
+ await page.waitForTimeout(settleMs);
332
+ } catch (err) {
333
+ const message = err && err.message ? err.message : String(err);
334
+ pageErrors.push({ pagePath, type: "navigation", message });
335
+ }
336
+ for (const d of pageErrors) diagnostics.push(d);
337
+ await page.close().catch(() => {});
338
+ }
339
+ await browser.close().catch(() => {});
340
+ process.stdout.write(JSON.stringify(diagnostics));
341
+ })().catch((err) => {
342
+ process.stderr.write(err && err.stack ? err.stack : String(err));
343
+ process.exit(1);
344
+ });
345
+ `;
346
+ }
347
+
348
+ function parseRunnerOutput(stdout: string): IRuntimeAuditDiagnostic[] {
349
+ const trimmed = stdout.trim();
350
+ if (trimmed.length === 0) return [];
351
+ try {
352
+ const parsed: unknown = JSON.parse(trimmed);
353
+ if (!Array.isArray(parsed)) return [];
354
+ return parsed.filter(
355
+ (entry): entry is IRuntimeAuditDiagnostic =>
356
+ entry !== null &&
357
+ typeof entry === "object" &&
358
+ typeof (entry as IRuntimeAuditDiagnostic).pagePath === "string" &&
359
+ typeof (entry as IRuntimeAuditDiagnostic).message === "string" &&
360
+ (entry as IRuntimeAuditDiagnostic).type !== undefined,
361
+ );
362
+ } catch {
363
+ return [];
364
+ }
365
+ }
366
+
367
+ function groupByPage(
368
+ diagnostics: IRuntimeAuditDiagnostic[],
369
+ ): Map<string, IRuntimeAuditDiagnostic[]> {
370
+ const map = new Map<string, IRuntimeAuditDiagnostic[]>();
371
+ for (const d of diagnostics) {
372
+ const bucket = map.get(d.pagePath);
373
+ if (bucket === undefined) map.set(d.pagePath, [d]);
374
+ else bucket.push(d);
375
+ }
376
+ return map;
377
+ }
378
+
379
+ function renderMarkdownReport(
380
+ diagnostics: IRuntimeAuditDiagnostic[],
381
+ byPage: Map<string, IRuntimeAuditDiagnostic[]>,
382
+ pages: string[],
383
+ ): string {
384
+ if (diagnostics.length === 0) {
385
+ return [
386
+ "# Runtime audit",
387
+ "",
388
+ `Visited **${pages.length}** page(s) under \`next dev\` (simulator mode).`,
389
+ "No console errors, page errors, or navigation failures detected.",
390
+ "",
391
+ ].join("\n");
392
+ }
393
+ const head = [
394
+ "# Runtime audit",
395
+ "",
396
+ `Visited **${pages.length}** page(s) under \`next dev\` (simulator mode).`,
397
+ `Detected **${diagnostics.length}** diagnostic(s) across **${byPage.size}** page(s).`,
398
+ "",
399
+ ];
400
+ const sections = [...byPage.entries()]
401
+ .sort(([a], [b]) => a.localeCompare(b))
402
+ .map(([pagePath, items]) => {
403
+ const lines = items.map((d) => `- \`${d.type}\` — ${escape(d.message)}`);
404
+ return [`## \`${pagePath}\` (${items.length})`, "", ...lines, ""].join(
405
+ "\n",
406
+ );
407
+ });
408
+ return [...head, ...sections].join("\n");
409
+ }
410
+
411
+ function escape(message: string): string {
412
+ return message.replace(/`/g, "\\`").replace(/\n+/g, " ").slice(0, 500);
413
+ }
414
+
415
+ /* -------------------------------------------------------------------------- */
416
+ /* spawn */
417
+ /* -------------------------------------------------------------------------- */
418
+
419
+ function spawnCommand(
420
+ cmd: string,
421
+ args: string[],
422
+ options: { cwd: string },
423
+ ): Promise<string> {
424
+ return new Promise((resolve, reject) => {
425
+ const child = spawn(cmd, args, { cwd: options.cwd, stdio: "pipe" });
426
+ let stdout = "";
427
+ let stderr = "";
428
+ child.stdout.on("data", (chunk) => {
429
+ stdout += chunk.toString("utf-8");
430
+ });
431
+ child.stderr.on("data", (chunk) => {
432
+ stderr += chunk.toString("utf-8");
433
+ });
434
+ child.on("error", (err) => {
435
+ reject(
436
+ new Error(
437
+ `Failed to spawn \`${cmd} ${args.join(" ")}\`: ${err.message}`,
438
+ ),
439
+ );
440
+ });
441
+ child.on("close", (code) => {
442
+ if (code === 0) {
443
+ resolve(stdout);
444
+ } else {
445
+ reject(
446
+ new Error(
447
+ `\`${cmd} ${args.slice(0, 3).join(" ")}...\` exited with code ${code}` +
448
+ (stderr.length > 0 ? `:\n${stderr.slice(-1500)}` : ""),
449
+ ),
450
+ );
451
+ }
452
+ });
453
+ });
454
+ }
@@ -0,0 +1,170 @@
1
+ import { OpenApiConverter } from "@typia/utils";
2
+ import { describe, expect, it } from "vitest";
3
+
4
+ import { toEndpoints } from "../../utils/toEndpoints";
5
+ import { buildDeterministicPlan } from "./buildDeterministicPlan";
6
+
7
+ /** Every operation must be referenced by a screen or appear as an omission. */
8
+ function unaccounted(document: ReturnType<typeof doc>): string[] {
9
+ const plan = buildDeterministicPlan(document, "user");
10
+ const referenced = new Set<string>();
11
+ for (const s of plan.screens) for (const a of s.endpoints) referenced.add(a);
12
+ const omitted = new Set(plan.intentionalOmissions.map((o) => o.target));
13
+ return toEndpoints(document)
14
+ .filter((e) => {
15
+ const acc = e.accessor.join(".");
16
+ const target = `${e.method.toUpperCase()} ${e.path}`;
17
+ return !referenced.has(acc) && !omitted.has(target);
18
+ })
19
+ .map((e) => `${e.method.toUpperCase()} ${e.path}`);
20
+ }
21
+
22
+ function doc(paths: Record<string, unknown>, schemas: Record<string, unknown> = {}) {
23
+ return OpenApiConverter.upgradeDocument({
24
+ openapi: "3.0.0",
25
+ info: { title: "t", version: "1.0.0" },
26
+ paths,
27
+ components: { schemas },
28
+ } as never);
29
+ }
30
+
31
+ const ok = { 200: { description: "ok" } };
32
+ const fixture = doc(
33
+ {
34
+ "/pets": {
35
+ get: { operationId: "list", responses: { 200: { description: "ok", content: { "application/json": { schema: { type: "array", items: { $ref: "#/components/schemas/Pet" } } } } } } },
36
+ post: { operationId: "create", requestBody: { content: { "application/json": { schema: { $ref: "#/components/schemas/Pet" } } } }, responses: ok },
37
+ },
38
+ "/pets/{petId}": {
39
+ get: { operationId: "get", parameters: [{ name: "petId", in: "path", required: true, schema: { type: "string" } }], responses: { 200: { description: "ok", content: { "application/json": { schema: { $ref: "#/components/schemas/Pet" } } } } } },
40
+ put: { operationId: "update", parameters: [{ name: "petId", in: "path", required: true, schema: { type: "string" } }], requestBody: { content: { "application/json": { schema: { $ref: "#/components/schemas/Pet" } } } }, responses: ok },
41
+ delete: { operationId: "erase", parameters: [{ name: "petId", in: "path", required: true, schema: { type: "string" } }], responses: ok },
42
+ },
43
+ },
44
+ { Pet: { type: "object", properties: { id: { type: "string" } }, required: ["id"] } },
45
+ );
46
+
47
+ describe("buildDeterministicPlan", () => {
48
+ it("produces a root landing hub + the resource's CRUD screens", () => {
49
+ const plan = buildDeterministicPlan(fixture, "user");
50
+ expect(plan.screens[0]).toMatchObject({ path: "/", uiPattern: "landing" });
51
+ const paths = plan.screens.map((s) => s.path);
52
+ // resource screens are sorted alphabetically by path (after the home hub)
53
+ expect(paths).toEqual(["/", "/pets", "/pets/[petsId]", "/pets/[petsId]/edit", "/pets/new"]);
54
+ expect(plan.navigation[0]!.primary).toContain("/");
55
+ expect(plan.navigation[0]!.primary).toContain("/pets");
56
+ });
57
+
58
+ it("wires each screen to real accessors and hosts write ops on detail", () => {
59
+ const plan = buildDeterministicPlan(fixture, "user");
60
+ const detail = plan.screens.find((s) => s.path === "/pets/[petsId]")!;
61
+ // detail composes its GET plus the update/delete it surfaces as actions
62
+ expect(detail.endpoints.length).toBeGreaterThanOrEqual(2);
63
+ expect(plan.screens.every((s) => s.actor === "user")).toBe(true);
64
+ });
65
+
66
+ it("is reproducible — same swagger yields an identical plan", () => {
67
+ expect(buildDeterministicPlan(fixture, "user")).toEqual(
68
+ buildDeterministicPlan(fixture, "user"),
69
+ );
70
+ });
71
+
72
+ it("accounts for every operation — no silent drops (referenced or omitted)", () => {
73
+ // a resource with TWO creates + TWO deletes — the kind of duplicate-role
74
+ // shape that used to drop the extras on the floor.
75
+ const multi = doc(
76
+ {
77
+ "/orders": {
78
+ post: { operationId: "create", requestBody: { content: { "application/json": { schema: { $ref: "#/components/schemas/O" } } } }, responses: ok },
79
+ patch: { operationId: "index", responses: { 200: { description: "ok", content: { "application/json": { schema: { type: "array", items: { $ref: "#/components/schemas/O" } } } } } } },
80
+ },
81
+ "/orders/direct": {
82
+ post: { operationId: "direct", requestBody: { content: { "application/json": { schema: { $ref: "#/components/schemas/O" } } } }, responses: ok },
83
+ },
84
+ "/orders/{id}": {
85
+ delete: { operationId: "erase", parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }], responses: ok },
86
+ },
87
+ "/orders/{id}/cancel": {
88
+ delete: { operationId: "cancel", parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }], responses: ok },
89
+ },
90
+ },
91
+ { O: { type: "object", properties: { id: { type: "string" } }, required: ["id"] } },
92
+ );
93
+ expect(unaccounted(multi)).toEqual([]);
94
+ expect(unaccounted(fixture)).toEqual([]);
95
+ });
96
+
97
+ it("classifies non-browsable list responses structurally (no domain keywords)", () => {
98
+ const noisy = doc(
99
+ {
100
+ // void GET → omitted
101
+ "/health": { get: { operationId: "health", responses: ok } },
102
+ // single-object GET → singleton read → detail (not a fake table)
103
+ "/system": {
104
+ get: {
105
+ operationId: "system",
106
+ responses: {
107
+ 200: {
108
+ description: "ok",
109
+ content: {
110
+ "application/json": { schema: { $ref: "#/components/schemas/Sys" } },
111
+ },
112
+ },
113
+ },
114
+ },
115
+ },
116
+ // single-object mutating verb → action → omitted
117
+ "/refresh": {
118
+ patch: {
119
+ operationId: "refresh",
120
+ requestBody: {
121
+ content: { "application/json": { schema: { $ref: "#/components/schemas/Sys" } } },
122
+ },
123
+ responses: {
124
+ 200: {
125
+ description: "ok",
126
+ content: {
127
+ "application/json": { schema: { $ref: "#/components/schemas/Sys" } },
128
+ },
129
+ },
130
+ },
131
+ },
132
+ },
133
+ },
134
+ { Sys: { type: "object", properties: { uptime: { type: "number" } }, required: ["uptime"] } },
135
+ );
136
+ const plan = buildDeterministicPlan(noisy, "user");
137
+ const byPath = Object.fromEntries(plan.screens.map((s) => [s.path, s]));
138
+ // no fake tables survived
139
+ expect(plan.screens.some((s) => s.uiPattern === "table")).toBe(false);
140
+ // singleton read became a top-level detail page, still rendering its data
141
+ expect(byPath["/system"]?.uiPattern).toBe("detail");
142
+ expect(plan.navigation[0]!.primary).toContain("/system");
143
+ // void + action verb were omitted, not turned into screens
144
+ expect(byPath["/health"]).toBeUndefined();
145
+ expect(byPath["/refresh"]).toBeUndefined();
146
+ const omitted = plan.intentionalOmissions.map((o) => o.target);
147
+ expect(omitted).toContain("GET /health");
148
+ expect(omitted).toContain("PATCH /refresh");
149
+ });
150
+
151
+ it("omits a write-only resource (no list/detail/search to browse) as nav noise", () => {
152
+ const wo = doc(
153
+ {
154
+ // a real, browsable resource
155
+ "/users": { get: { operationId: "list", responses: { 200: { description: "ok", content: { "application/json": { schema: { type: "array", items: { $ref: "#/components/schemas/U" } } } } } } } },
156
+ // oauth2 token — POST only, no way to browse it: pure nav noise
157
+ "/token": { post: { operationId: "token", requestBody: { content: { "application/json": { schema: { $ref: "#/components/schemas/U" } } } }, responses: ok } },
158
+ },
159
+ { U: { type: "object", properties: { id: { type: "string" } }, required: ["id"] } },
160
+ );
161
+ const plan = buildDeterministicPlan(wo, "user");
162
+ // the browsable resource stays; the write-only one has no screen
163
+ expect(plan.navigation[0]!.primary).toContain("/users");
164
+ expect(plan.screens.some((s) => s.path.includes("token"))).toBe(false);
165
+ // ...but it is recorded as an omission, not silently dropped
166
+ expect(plan.intentionalOmissions.some((o) => o.target.includes("/token"))).toBe(true);
167
+ // and coverage stays total
168
+ expect(unaccounted(wo)).toEqual([]);
169
+ });
170
+ });