@nac3/forge-cli 0.2.0-alpha.1

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 (561) hide show
  1. package/LICENSE +45 -0
  2. package/README.md +371 -0
  3. package/dist/bin/yf.d.ts +5 -0
  4. package/dist/bin/yf.d.ts.map +1 -0
  5. package/dist/bin/yf.js +86 -0
  6. package/dist/bin/yf.js.map +1 -0
  7. package/dist/chat/claude.d.ts +100 -0
  8. package/dist/chat/claude.d.ts.map +1 -0
  9. package/dist/chat/claude.js +228 -0
  10. package/dist/chat/claude.js.map +1 -0
  11. package/dist/chat/ingest_session.d.ts +97 -0
  12. package/dist/chat/ingest_session.d.ts.map +1 -0
  13. package/dist/chat/ingest_session.js +99 -0
  14. package/dist/chat/ingest_session.js.map +1 -0
  15. package/dist/chat/panel.d.ts +15 -0
  16. package/dist/chat/panel.d.ts.map +1 -0
  17. package/dist/chat/panel.js +1526 -0
  18. package/dist/chat/panel.js.map +1 -0
  19. package/dist/chat/persistence.d.ts +37 -0
  20. package/dist/chat/persistence.d.ts.map +1 -0
  21. package/dist/chat/persistence.js +91 -0
  22. package/dist/chat/persistence.js.map +1 -0
  23. package/dist/chat/server.d.ts +34 -0
  24. package/dist/chat/server.d.ts.map +1 -0
  25. package/dist/chat/server.js +1540 -0
  26. package/dist/chat/server.js.map +1 -0
  27. package/dist/chat/spec_extract.d.ts +35 -0
  28. package/dist/chat/spec_extract.d.ts.map +1 -0
  29. package/dist/chat/spec_extract.js +152 -0
  30. package/dist/chat/spec_extract.js.map +1 -0
  31. package/dist/chat/spec_plan.d.ts +65 -0
  32. package/dist/chat/spec_plan.d.ts.map +1 -0
  33. package/dist/chat/spec_plan.js +160 -0
  34. package/dist/chat/spec_plan.js.map +1 -0
  35. package/dist/chat/spec_scaffold.d.ts +95 -0
  36. package/dist/chat/spec_scaffold.d.ts.map +1 -0
  37. package/dist/chat/spec_scaffold.js +220 -0
  38. package/dist/chat/spec_scaffold.js.map +1 -0
  39. package/dist/chat/tools/git.d.ts +59 -0
  40. package/dist/chat/tools/git.d.ts.map +1 -0
  41. package/dist/chat/tools/git.js +313 -0
  42. package/dist/chat/tools/git.js.map +1 -0
  43. package/dist/chat/tools/github.d.ts +59 -0
  44. package/dist/chat/tools/github.d.ts.map +1 -0
  45. package/dist/chat/tools/github.js +310 -0
  46. package/dist/chat/tools/github.js.map +1 -0
  47. package/dist/chat/tools/lifecycle.d.ts +82 -0
  48. package/dist/chat/tools/lifecycle.d.ts.map +1 -0
  49. package/dist/chat/tools/lifecycle.js +295 -0
  50. package/dist/chat/tools/lifecycle.js.map +1 -0
  51. package/dist/chat/tools/manual.d.ts +26 -0
  52. package/dist/chat/tools/manual.d.ts.map +1 -0
  53. package/dist/chat/tools/manual.js +164 -0
  54. package/dist/chat/tools/manual.js.map +1 -0
  55. package/dist/chat/tools/reader.d.ts +80 -0
  56. package/dist/chat/tools/reader.d.ts.map +1 -0
  57. package/dist/chat/tools/reader.js +471 -0
  58. package/dist/chat/tools/reader.js.map +1 -0
  59. package/dist/chat/tools.d.ts +106 -0
  60. package/dist/chat/tools.d.ts.map +1 -0
  61. package/dist/chat/tools.js +587 -0
  62. package/dist/chat/tools.js.map +1 -0
  63. package/dist/codegen/e2e.d.ts +106 -0
  64. package/dist/codegen/e2e.d.ts.map +1 -0
  65. package/dist/codegen/e2e.js +931 -0
  66. package/dist/codegen/e2e.js.map +1 -0
  67. package/dist/codegen/v3_flow_emit.d.ts +70 -0
  68. package/dist/codegen/v3_flow_emit.d.ts.map +1 -0
  69. package/dist/codegen/v3_flow_emit.js +225 -0
  70. package/dist/codegen/v3_flow_emit.js.map +1 -0
  71. package/dist/commands/_stub.d.ts +2 -0
  72. package/dist/commands/_stub.d.ts.map +1 -0
  73. package/dist/commands/_stub.js +21 -0
  74. package/dist/commands/_stub.js.map +1 -0
  75. package/dist/commands/app.d.ts +31 -0
  76. package/dist/commands/app.d.ts.map +1 -0
  77. package/dist/commands/app.js +331 -0
  78. package/dist/commands/app.js.map +1 -0
  79. package/dist/commands/chat.d.ts +18 -0
  80. package/dist/commands/chat.d.ts.map +1 -0
  81. package/dist/commands/chat.js +76 -0
  82. package/dist/commands/chat.js.map +1 -0
  83. package/dist/commands/deploy.d.ts +21 -0
  84. package/dist/commands/deploy.d.ts.map +1 -0
  85. package/dist/commands/deploy.js +121 -0
  86. package/dist/commands/deploy.js.map +1 -0
  87. package/dist/commands/doctor.d.ts +14 -0
  88. package/dist/commands/doctor.d.ts.map +1 -0
  89. package/dist/commands/doctor.js +280 -0
  90. package/dist/commands/doctor.js.map +1 -0
  91. package/dist/commands/figma.d.ts +32 -0
  92. package/dist/commands/figma.d.ts.map +1 -0
  93. package/dist/commands/figma.js +141 -0
  94. package/dist/commands/figma.js.map +1 -0
  95. package/dist/commands/gen-flow-tests.d.ts +8 -0
  96. package/dist/commands/gen-flow-tests.d.ts.map +1 -0
  97. package/dist/commands/gen-flow-tests.js +78 -0
  98. package/dist/commands/gen-flow-tests.js.map +1 -0
  99. package/dist/commands/gen-tests.d.ts +9 -0
  100. package/dist/commands/gen-tests.d.ts.map +1 -0
  101. package/dist/commands/gen-tests.js +118 -0
  102. package/dist/commands/gen-tests.js.map +1 -0
  103. package/dist/commands/license.d.ts +14 -0
  104. package/dist/commands/license.d.ts.map +1 -0
  105. package/dist/commands/license.js +182 -0
  106. package/dist/commands/license.js.map +1 -0
  107. package/dist/commands/log.d.ts +19 -0
  108. package/dist/commands/log.d.ts.map +1 -0
  109. package/dist/commands/log.js +101 -0
  110. package/dist/commands/log.js.map +1 -0
  111. package/dist/commands/migrate.d.ts +118 -0
  112. package/dist/commands/migrate.d.ts.map +1 -0
  113. package/dist/commands/migrate.js +1410 -0
  114. package/dist/commands/migrate.js.map +1 -0
  115. package/dist/commands/mobile.d.ts +27 -0
  116. package/dist/commands/mobile.d.ts.map +1 -0
  117. package/dist/commands/mobile.js +90 -0
  118. package/dist/commands/mobile.js.map +1 -0
  119. package/dist/commands/new.d.ts +32 -0
  120. package/dist/commands/new.d.ts.map +1 -0
  121. package/dist/commands/new.js +107 -0
  122. package/dist/commands/new.js.map +1 -0
  123. package/dist/commands/pilot.d.ts +8 -0
  124. package/dist/commands/pilot.d.ts.map +1 -0
  125. package/dist/commands/pilot.js +104 -0
  126. package/dist/commands/pilot.js.map +1 -0
  127. package/dist/commands/projects.d.ts +21 -0
  128. package/dist/commands/projects.d.ts.map +1 -0
  129. package/dist/commands/projects.js +238 -0
  130. package/dist/commands/projects.js.map +1 -0
  131. package/dist/commands/publish.d.ts +35 -0
  132. package/dist/commands/publish.d.ts.map +1 -0
  133. package/dist/commands/publish.js +194 -0
  134. package/dist/commands/publish.js.map +1 -0
  135. package/dist/commands/repo.d.ts +59 -0
  136. package/dist/commands/repo.d.ts.map +1 -0
  137. package/dist/commands/repo.js +178 -0
  138. package/dist/commands/repo.js.map +1 -0
  139. package/dist/commands/review-screens.d.ts +28 -0
  140. package/dist/commands/review-screens.d.ts.map +1 -0
  141. package/dist/commands/review-screens.js +345 -0
  142. package/dist/commands/review-screens.js.map +1 -0
  143. package/dist/commands/scenarios.d.ts +23 -0
  144. package/dist/commands/scenarios.d.ts.map +1 -0
  145. package/dist/commands/scenarios.js +304 -0
  146. package/dist/commands/scenarios.js.map +1 -0
  147. package/dist/commands/ship.d.ts +18 -0
  148. package/dist/commands/ship.d.ts.map +1 -0
  149. package/dist/commands/ship.js +41 -0
  150. package/dist/commands/ship.js.map +1 -0
  151. package/dist/commands/test.d.ts +29 -0
  152. package/dist/commands/test.d.ts.map +1 -0
  153. package/dist/commands/test.js +62 -0
  154. package/dist/commands/test.js.map +1 -0
  155. package/dist/commands/tunnel.d.ts +22 -0
  156. package/dist/commands/tunnel.d.ts.map +1 -0
  157. package/dist/commands/tunnel.js +77 -0
  158. package/dist/commands/tunnel.js.map +1 -0
  159. package/dist/commands/validate.d.ts +14 -0
  160. package/dist/commands/validate.d.ts.map +1 -0
  161. package/dist/commands/validate.js +51 -0
  162. package/dist/commands/validate.js.map +1 -0
  163. package/dist/commands/vault.d.ts +32 -0
  164. package/dist/commands/vault.d.ts.map +1 -0
  165. package/dist/commands/vault.js +489 -0
  166. package/dist/commands/vault.js.map +1 -0
  167. package/dist/commands/voice.d.ts +16 -0
  168. package/dist/commands/voice.d.ts.map +1 -0
  169. package/dist/commands/voice.js +69 -0
  170. package/dist/commands/voice.js.map +1 -0
  171. package/dist/core/cascade_router.d.ts +90 -0
  172. package/dist/core/cascade_router.d.ts.map +1 -0
  173. package/dist/core/cascade_router.js +131 -0
  174. package/dist/core/cascade_router.js.map +1 -0
  175. package/dist/core/cf_tunnel.d.ts +52 -0
  176. package/dist/core/cf_tunnel.d.ts.map +1 -0
  177. package/dist/core/cf_tunnel.js +134 -0
  178. package/dist/core/cf_tunnel.js.map +1 -0
  179. package/dist/core/gha_dispatcher.d.ts +48 -0
  180. package/dist/core/gha_dispatcher.d.ts.map +1 -0
  181. package/dist/core/gha_dispatcher.js +198 -0
  182. package/dist/core/gha_dispatcher.js.map +1 -0
  183. package/dist/core/logger.d.ts +89 -0
  184. package/dist/core/logger.d.ts.map +1 -0
  185. package/dist/core/logger.js +245 -0
  186. package/dist/core/logger.js.map +1 -0
  187. package/dist/core/mode.d.ts +26 -0
  188. package/dist/core/mode.d.ts.map +1 -0
  189. package/dist/core/mode.js +122 -0
  190. package/dist/core/mode.js.map +1 -0
  191. package/dist/core/pairing.d.ts +40 -0
  192. package/dist/core/pairing.d.ts.map +1 -0
  193. package/dist/core/pairing.js +145 -0
  194. package/dist/core/pairing.js.map +1 -0
  195. package/dist/core/pilot_setup.d.ts +29 -0
  196. package/dist/core/pilot_setup.d.ts.map +1 -0
  197. package/dist/core/pilot_setup.js +119 -0
  198. package/dist/core/pilot_setup.js.map +1 -0
  199. package/dist/core/polar.d.ts +81 -0
  200. package/dist/core/polar.d.ts.map +1 -0
  201. package/dist/core/polar.js +175 -0
  202. package/dist/core/polar.js.map +1 -0
  203. package/dist/core/project_picker.d.ts +56 -0
  204. package/dist/core/project_picker.d.ts.map +1 -0
  205. package/dist/core/project_picker.js +86 -0
  206. package/dist/core/project_picker.js.map +1 -0
  207. package/dist/core/projects.d.ts +58 -0
  208. package/dist/core/projects.d.ts.map +1 -0
  209. package/dist/core/projects.js +146 -0
  210. package/dist/core/projects.js.map +1 -0
  211. package/dist/core/projects_sync.d.ts +80 -0
  212. package/dist/core/projects_sync.d.ts.map +1 -0
  213. package/dist/core/projects_sync.js +278 -0
  214. package/dist/core/projects_sync.js.map +1 -0
  215. package/dist/core/remote_runner.d.ts +70 -0
  216. package/dist/core/remote_runner.d.ts.map +1 -0
  217. package/dist/core/remote_runner.js +133 -0
  218. package/dist/core/remote_runner.js.map +1 -0
  219. package/dist/core/repo_state.d.ts +24 -0
  220. package/dist/core/repo_state.d.ts.map +1 -0
  221. package/dist/core/repo_state.js +109 -0
  222. package/dist/core/repo_state.js.map +1 -0
  223. package/dist/core/target.d.ts +31 -0
  224. package/dist/core/target.d.ts.map +1 -0
  225. package/dist/core/target.js +121 -0
  226. package/dist/core/target.js.map +1 -0
  227. package/dist/deploy/aws.d.ts +43 -0
  228. package/dist/deploy/aws.d.ts.map +1 -0
  229. package/dist/deploy/aws.js +173 -0
  230. package/dist/deploy/aws.js.map +1 -0
  231. package/dist/figma/api.d.ts +35 -0
  232. package/dist/figma/api.d.ts.map +1 -0
  233. package/dist/figma/api.js +40 -0
  234. package/dist/figma/api.js.map +1 -0
  235. package/dist/figma/decorator.d.ts +74 -0
  236. package/dist/figma/decorator.d.ts.map +1 -0
  237. package/dist/figma/decorator.js +210 -0
  238. package/dist/figma/decorator.js.map +1 -0
  239. package/dist/figma/heuristics.d.ts +29 -0
  240. package/dist/figma/heuristics.d.ts.map +1 -0
  241. package/dist/figma/heuristics.js +110 -0
  242. package/dist/figma/heuristics.js.map +1 -0
  243. package/dist/figma/normalize.d.ts +33 -0
  244. package/dist/figma/normalize.d.ts.map +1 -0
  245. package/dist/figma/normalize.js +101 -0
  246. package/dist/figma/normalize.js.map +1 -0
  247. package/dist/figma/tokens.d.ts +23 -0
  248. package/dist/figma/tokens.d.ts.map +1 -0
  249. package/dist/figma/tokens.js +111 -0
  250. package/dist/figma/tokens.js.map +1 -0
  251. package/dist/figma/types.d.ts +118 -0
  252. package/dist/figma/types.d.ts.map +1 -0
  253. package/dist/figma/types.js +12 -0
  254. package/dist/figma/types.js.map +1 -0
  255. package/dist/i18n/index.d.ts +48 -0
  256. package/dist/i18n/index.d.ts.map +1 -0
  257. package/dist/i18n/index.js +135 -0
  258. package/dist/i18n/index.js.map +1 -0
  259. package/dist/i18n/types.d.ts +52 -0
  260. package/dist/i18n/types.d.ts.map +1 -0
  261. package/dist/i18n/types.js +85 -0
  262. package/dist/i18n/types.js.map +1 -0
  263. package/dist/index.d.ts +12 -0
  264. package/dist/index.d.ts.map +1 -0
  265. package/dist/index.js +11 -0
  266. package/dist/index.js.map +1 -0
  267. package/dist/lan/mdns_packet.d.ts +74 -0
  268. package/dist/lan/mdns_packet.d.ts.map +1 -0
  269. package/dist/lan/mdns_packet.js +247 -0
  270. package/dist/lan/mdns_packet.js.map +1 -0
  271. package/dist/lan/mdns_service.d.ts +102 -0
  272. package/dist/lan/mdns_service.d.ts.map +1 -0
  273. package/dist/lan/mdns_service.js +206 -0
  274. package/dist/lan/mdns_service.js.map +1 -0
  275. package/dist/license/activate.d.ts +33 -0
  276. package/dist/license/activate.d.ts.map +1 -0
  277. package/dist/license/activate.js +135 -0
  278. package/dist/license/activate.js.map +1 -0
  279. package/dist/license/fingerprint.d.ts +2 -0
  280. package/dist/license/fingerprint.d.ts.map +1 -0
  281. package/dist/license/fingerprint.js +29 -0
  282. package/dist/license/fingerprint.js.map +1 -0
  283. package/dist/license/hito4_client.d.ts +24 -0
  284. package/dist/license/hito4_client.d.ts.map +1 -0
  285. package/dist/license/hito4_client.js +103 -0
  286. package/dist/license/hito4_client.js.map +1 -0
  287. package/dist/license/index.d.ts +22 -0
  288. package/dist/license/index.d.ts.map +1 -0
  289. package/dist/license/index.js +125 -0
  290. package/dist/license/index.js.map +1 -0
  291. package/dist/license/types.d.ts +38 -0
  292. package/dist/license/types.d.ts.map +1 -0
  293. package/dist/license/types.js +9 -0
  294. package/dist/license/types.js.map +1 -0
  295. package/dist/migrate/ai-apply.d.ts +198 -0
  296. package/dist/migrate/ai-apply.d.ts.map +1 -0
  297. package/dist/migrate/ai-apply.js +833 -0
  298. package/dist/migrate/ai-apply.js.map +1 -0
  299. package/dist/migrate/ai-decorator.d.ts +87 -0
  300. package/dist/migrate/ai-decorator.d.ts.map +1 -0
  301. package/dist/migrate/ai-decorator.js +203 -0
  302. package/dist/migrate/ai-decorator.js.map +1 -0
  303. package/dist/migrate/apply.d.ts +28 -0
  304. package/dist/migrate/apply.d.ts.map +1 -0
  305. package/dist/migrate/apply.js +119 -0
  306. package/dist/migrate/apply.js.map +1 -0
  307. package/dist/migrate/audit.d.ts +9 -0
  308. package/dist/migrate/audit.d.ts.map +1 -0
  309. package/dist/migrate/audit.js +197 -0
  310. package/dist/migrate/audit.js.map +1 -0
  311. package/dist/migrate/diff.d.ts +28 -0
  312. package/dist/migrate/diff.d.ts.map +1 -0
  313. package/dist/migrate/diff.js +154 -0
  314. package/dist/migrate/diff.js.map +1 -0
  315. package/dist/migrate/html-orchestrator.d.ts +81 -0
  316. package/dist/migrate/html-orchestrator.d.ts.map +1 -0
  317. package/dist/migrate/html-orchestrator.js +233 -0
  318. package/dist/migrate/html-orchestrator.js.map +1 -0
  319. package/dist/migrate/html-walker.d.ts +93 -0
  320. package/dist/migrate/html-walker.d.ts.map +1 -0
  321. package/dist/migrate/html-walker.js +288 -0
  322. package/dist/migrate/html-walker.js.map +1 -0
  323. package/dist/migrate/js-template-walker.d.ts +118 -0
  324. package/dist/migrate/js-template-walker.d.ts.map +1 -0
  325. package/dist/migrate/js-template-walker.js +644 -0
  326. package/dist/migrate/js-template-walker.js.map +1 -0
  327. package/dist/migrate/manifest-validator.d.ts +30 -0
  328. package/dist/migrate/manifest-validator.d.ts.map +1 -0
  329. package/dist/migrate/manifest-validator.js +261 -0
  330. package/dist/migrate/manifest-validator.js.map +1 -0
  331. package/dist/migrate/overrides.d.ts +58 -0
  332. package/dist/migrate/overrides.d.ts.map +1 -0
  333. package/dist/migrate/overrides.js +193 -0
  334. package/dist/migrate/overrides.js.map +1 -0
  335. package/dist/migrate/plugin-scope.d.ts +42 -0
  336. package/dist/migrate/plugin-scope.d.ts.map +1 -0
  337. package/dist/migrate/plugin-scope.js +94 -0
  338. package/dist/migrate/plugin-scope.js.map +1 -0
  339. package/dist/migrate/types.d.ts +45 -0
  340. package/dist/migrate/types.d.ts.map +1 -0
  341. package/dist/migrate/types.js +9 -0
  342. package/dist/migrate/types.js.map +1 -0
  343. package/dist/migrate/verb-inference.d.ts +37 -0
  344. package/dist/migrate/verb-inference.d.ts.map +1 -0
  345. package/dist/migrate/verb-inference.js +274 -0
  346. package/dist/migrate/verb-inference.js.map +1 -0
  347. package/dist/nac3/attrs.d.ts +87 -0
  348. package/dist/nac3/attrs.d.ts.map +1 -0
  349. package/dist/nac3/attrs.js +134 -0
  350. package/dist/nac3/attrs.js.map +1 -0
  351. package/dist/nac3/scenario_dsl.d.ts +71 -0
  352. package/dist/nac3/scenario_dsl.d.ts.map +1 -0
  353. package/dist/nac3/scenario_dsl.js +191 -0
  354. package/dist/nac3/scenario_dsl.js.map +1 -0
  355. package/dist/nac3/tokens.d.ts +126 -0
  356. package/dist/nac3/tokens.d.ts.map +1 -0
  357. package/dist/nac3/tokens.js +138 -0
  358. package/dist/nac3/tokens.js.map +1 -0
  359. package/dist/reader/parsers/csv.d.ts +42 -0
  360. package/dist/reader/parsers/csv.d.ts.map +1 -0
  361. package/dist/reader/parsers/csv.js +221 -0
  362. package/dist/reader/parsers/csv.js.map +1 -0
  363. package/dist/reader/parsers/docx.d.ts +31 -0
  364. package/dist/reader/parsers/docx.d.ts.map +1 -0
  365. package/dist/reader/parsers/docx.js +51 -0
  366. package/dist/reader/parsers/docx.js.map +1 -0
  367. package/dist/reader/parsers/epub.d.ts +39 -0
  368. package/dist/reader/parsers/epub.d.ts.map +1 -0
  369. package/dist/reader/parsers/epub.js +265 -0
  370. package/dist/reader/parsers/epub.js.map +1 -0
  371. package/dist/reader/parsers/html.d.ts +40 -0
  372. package/dist/reader/parsers/html.d.ts.map +1 -0
  373. package/dist/reader/parsers/html.js +386 -0
  374. package/dist/reader/parsers/html.js.map +1 -0
  375. package/dist/reader/parsers/md.d.ts +30 -0
  376. package/dist/reader/parsers/md.d.ts.map +1 -0
  377. package/dist/reader/parsers/md.js +199 -0
  378. package/dist/reader/parsers/md.js.map +1 -0
  379. package/dist/reader/parsers/pdf.d.ts +39 -0
  380. package/dist/reader/parsers/pdf.d.ts.map +1 -0
  381. package/dist/reader/parsers/pdf.js +220 -0
  382. package/dist/reader/parsers/pdf.js.map +1 -0
  383. package/dist/reader/parsers/rtf.d.ts +37 -0
  384. package/dist/reader/parsers/rtf.d.ts.map +1 -0
  385. package/dist/reader/parsers/rtf.js +347 -0
  386. package/dist/reader/parsers/rtf.js.map +1 -0
  387. package/dist/reader/parsers/source.d.ts +32 -0
  388. package/dist/reader/parsers/source.d.ts.map +1 -0
  389. package/dist/reader/parsers/source.js +122 -0
  390. package/dist/reader/parsers/source.js.map +1 -0
  391. package/dist/reader/parsers/txt.d.ts +25 -0
  392. package/dist/reader/parsers/txt.d.ts.map +1 -0
  393. package/dist/reader/parsers/txt.js +56 -0
  394. package/dist/reader/parsers/txt.js.map +1 -0
  395. package/dist/reader/parsers/xlsx.d.ts +33 -0
  396. package/dist/reader/parsers/xlsx.d.ts.map +1 -0
  397. package/dist/reader/parsers/xlsx.js +143 -0
  398. package/dist/reader/parsers/xlsx.js.map +1 -0
  399. package/dist/reader/registry.d.ts +39 -0
  400. package/dist/reader/registry.d.ts.map +1 -0
  401. package/dist/reader/registry.js +172 -0
  402. package/dist/reader/registry.js.map +1 -0
  403. package/dist/reader/search.d.ts +27 -0
  404. package/dist/reader/search.d.ts.map +1 -0
  405. package/dist/reader/search.js +77 -0
  406. package/dist/reader/search.js.map +1 -0
  407. package/dist/reader/state.d.ts +56 -0
  408. package/dist/reader/state.d.ts.map +1 -0
  409. package/dist/reader/state.js +179 -0
  410. package/dist/reader/state.js.map +1 -0
  411. package/dist/reader/types.d.ts +119 -0
  412. package/dist/reader/types.d.ts.map +1 -0
  413. package/dist/reader/types.js +23 -0
  414. package/dist/reader/types.js.map +1 -0
  415. package/dist/ship/run.d.ts +26 -0
  416. package/dist/ship/run.d.ts.map +1 -0
  417. package/dist/ship/run.js +123 -0
  418. package/dist/ship/run.js.map +1 -0
  419. package/dist/template/index.d.ts +50 -0
  420. package/dist/template/index.d.ts.map +1 -0
  421. package/dist/template/index.js +140 -0
  422. package/dist/template/index.js.map +1 -0
  423. package/dist/ui/colors.d.ts +13 -0
  424. package/dist/ui/colors.d.ts.map +1 -0
  425. package/dist/ui/colors.js +26 -0
  426. package/dist/ui/colors.js.map +1 -0
  427. package/dist/validate/index.d.ts +19 -0
  428. package/dist/validate/index.d.ts.map +1 -0
  429. package/dist/validate/index.js +181 -0
  430. package/dist/validate/index.js.map +1 -0
  431. package/dist/vault/catalog.d.ts +55 -0
  432. package/dist/vault/catalog.d.ts.map +1 -0
  433. package/dist/vault/catalog.js +424 -0
  434. package/dist/vault/catalog.js.map +1 -0
  435. package/dist/vault/crypto.d.ts +82 -0
  436. package/dist/vault/crypto.d.ts.map +1 -0
  437. package/dist/vault/crypto.js +173 -0
  438. package/dist/vault/crypto.js.map +1 -0
  439. package/dist/vault/git_askpass.d.ts +26 -0
  440. package/dist/vault/git_askpass.d.ts.map +1 -0
  441. package/dist/vault/git_askpass.js +104 -0
  442. package/dist/vault/git_askpass.js.map +1 -0
  443. package/dist/vault/migrator.d.ts +57 -0
  444. package/dist/vault/migrator.d.ts.map +1 -0
  445. package/dist/vault/migrator.js +204 -0
  446. package/dist/vault/migrator.js.map +1 -0
  447. package/dist/vault/redactor.d.ts +73 -0
  448. package/dist/vault/redactor.d.ts.map +1 -0
  449. package/dist/vault/redactor.js +182 -0
  450. package/dist/vault/redactor.js.map +1 -0
  451. package/dist/vault/store.d.ts +132 -0
  452. package/dist/vault/store.d.ts.map +1 -0
  453. package/dist/vault/store.js +335 -0
  454. package/dist/vault/store.js.map +1 -0
  455. package/dist/version.d.ts +8 -0
  456. package/dist/version.d.ts.map +1 -0
  457. package/dist/version.js +8 -0
  458. package/dist/version.js.map +1 -0
  459. package/dist/voice/chunker.d.ts +43 -0
  460. package/dist/voice/chunker.d.ts.map +1 -0
  461. package/dist/voice/chunker.js +133 -0
  462. package/dist/voice/chunker.js.map +1 -0
  463. package/dist/voice/config.d.ts +14 -0
  464. package/dist/voice/config.d.ts.map +1 -0
  465. package/dist/voice/config.js +51 -0
  466. package/dist/voice/config.js.map +1 -0
  467. package/dist/voice/intents.d.ts +71 -0
  468. package/dist/voice/intents.d.ts.map +1 -0
  469. package/dist/voice/intents.js +0 -0
  470. package/dist/voice/intents.js.map +1 -0
  471. package/dist/voice/providers/elevenlabs.d.ts +53 -0
  472. package/dist/voice/providers/elevenlabs.d.ts.map +1 -0
  473. package/dist/voice/providers/elevenlabs.js +159 -0
  474. package/dist/voice/providers/elevenlabs.js.map +1 -0
  475. package/dist/voice/providers/google.d.ts +56 -0
  476. package/dist/voice/providers/google.d.ts.map +1 -0
  477. package/dist/voice/providers/google.js +253 -0
  478. package/dist/voice/providers/google.js.map +1 -0
  479. package/dist/voice/providers/whisper.d.ts +44 -0
  480. package/dist/voice/providers/whisper.d.ts.map +1 -0
  481. package/dist/voice/providers/whisper.js +179 -0
  482. package/dist/voice/providers/whisper.js.map +1 -0
  483. package/dist/voice/registry.d.ts +35 -0
  484. package/dist/voice/registry.d.ts.map +1 -0
  485. package/dist/voice/registry.js +48 -0
  486. package/dist/voice/registry.js.map +1 -0
  487. package/dist/voice/router.d.ts +62 -0
  488. package/dist/voice/router.d.ts.map +1 -0
  489. package/dist/voice/router.js +175 -0
  490. package/dist/voice/router.js.map +1 -0
  491. package/dist/voice/types.d.ts +116 -0
  492. package/dist/voice/types.d.ts.map +1 -0
  493. package/dist/voice/types.js +22 -0
  494. package/dist/voice/types.js.map +1 -0
  495. package/dist/voice/voiceprint/enrollment.d.ts +36 -0
  496. package/dist/voice/voiceprint/enrollment.d.ts.map +1 -0
  497. package/dist/voice/voiceprint/enrollment.js +71 -0
  498. package/dist/voice/voiceprint/enrollment.js.map +1 -0
  499. package/dist/voice/voiceprint/identify.d.ts +16 -0
  500. package/dist/voice/voiceprint/identify.d.ts.map +1 -0
  501. package/dist/voice/voiceprint/identify.js +20 -0
  502. package/dist/voice/voiceprint/identify.js.map +1 -0
  503. package/dist/voice/voiceprint/liveness.d.ts +90 -0
  504. package/dist/voice/voiceprint/liveness.d.ts.map +1 -0
  505. package/dist/voice/voiceprint/liveness.js +251 -0
  506. package/dist/voice/voiceprint/liveness.js.map +1 -0
  507. package/dist/voice/voiceprint/match.d.ts +54 -0
  508. package/dist/voice/voiceprint/match.d.ts.map +1 -0
  509. package/dist/voice/voiceprint/match.js +88 -0
  510. package/dist/voice/voiceprint/match.js.map +1 -0
  511. package/dist/voice/voiceprint/providers/local-mfcc-stub.d.ts +44 -0
  512. package/dist/voice/voiceprint/providers/local-mfcc-stub.d.ts.map +1 -0
  513. package/dist/voice/voiceprint/providers/local-mfcc-stub.js +92 -0
  514. package/dist/voice/voiceprint/providers/local-mfcc-stub.js.map +1 -0
  515. package/dist/voice/voiceprint/store.d.ts +60 -0
  516. package/dist/voice/voiceprint/store.d.ts.map +1 -0
  517. package/dist/voice/voiceprint/store.js +155 -0
  518. package/dist/voice/voiceprint/store.js.map +1 -0
  519. package/dist/voice/voiceprint/trust.d.ts +90 -0
  520. package/dist/voice/voiceprint/trust.d.ts.map +1 -0
  521. package/dist/voice/voiceprint/trust.js +150 -0
  522. package/dist/voice/voiceprint/trust.js.map +1 -0
  523. package/dist/voice/voiceprint/types.d.ts +100 -0
  524. package/dist/voice/voiceprint/types.d.ts.map +1 -0
  525. package/dist/voice/voiceprint/types.js +23 -0
  526. package/dist/voice/voiceprint/types.js.map +1 -0
  527. package/dist/voice/wake.d.ts +64 -0
  528. package/dist/voice/wake.d.ts.map +1 -0
  529. package/dist/voice/wake.js +143 -0
  530. package/dist/voice/wake.js.map +1 -0
  531. package/docs/manuals/manual.ar.html +91 -0
  532. package/docs/manuals/manual.de.html +100 -0
  533. package/docs/manuals/manual.en.html +118 -0
  534. package/docs/manuals/manual.es.html +120 -0
  535. package/docs/manuals/manual.fr.html +102 -0
  536. package/docs/manuals/manual.hi.html +93 -0
  537. package/docs/manuals/manual.it.html +93 -0
  538. package/docs/manuals/manual.ja.html +97 -0
  539. package/docs/manuals/manual.pt.html +103 -0
  540. package/docs/manuals/manual.zh.html +89 -0
  541. package/package.json +94 -0
  542. package/src/i18n/catalogs/ar.json +86 -0
  543. package/src/i18n/catalogs/de.json +86 -0
  544. package/src/i18n/catalogs/en.json +86 -0
  545. package/src/i18n/catalogs/es.json +86 -0
  546. package/src/i18n/catalogs/fr.json +86 -0
  547. package/src/i18n/catalogs/hi.json +86 -0
  548. package/src/i18n/catalogs/it.json +86 -0
  549. package/src/i18n/catalogs/ja.json +86 -0
  550. package/src/i18n/catalogs/pt.json +86 -0
  551. package/src/i18n/catalogs/zh.json +86 -0
  552. package/templates/react-app/README.md +43 -0
  553. package/templates/react-app/index.html +12 -0
  554. package/templates/react-app/package.json +35 -0
  555. package/templates/react-app/src/App.tsx +106 -0
  556. package/templates/react-app/src/main.tsx +21 -0
  557. package/templates/react-app/src/nac/manifest.ts +46 -0
  558. package/templates/react-app/src/styles.css +68 -0
  559. package/templates/react-app/tsconfig.json +19 -0
  560. package/templates/react-app/vite.config.ts +12 -0
  561. package/templates/react-app/yujin.forge.json +15 -0
@@ -0,0 +1,931 @@
1
+ /*
2
+ * src/codegen/e2e.ts
3
+ *
4
+ * E2E test corpus generator. Reads a NAC-3 manifest, builds an
5
+ * abstract TestPlan (framework-agnostic), then emits the actual
6
+ * test files in the requested framework's syntax.
7
+ *
8
+ * Supported frameworks (v1):
9
+ * - playwright (TypeScript, .spec.ts under tests/)
10
+ * - cypress (JavaScript, .cy.js under cypress/e2e/)
11
+ * - vitest (TypeScript, .test.ts with jsdom under tests/)
12
+ * - wdio (TypeScript, .test.ts under test/specs/)
13
+ *
14
+ * Coverage metric reported: "agent-relevant surface" -- one test
15
+ * per (nac_id, role, verb) tuple in the manifest. This is the
16
+ * honest number to publish (NOT line-coverage, which the manifest
17
+ * cannot guarantee).
18
+ *
19
+ * ASCII-only.
20
+ */
21
+ import { promises as fs } from 'node:fs';
22
+ import path from 'node:path';
23
+ export const ALL_FRAMEWORKS = ['playwright', 'cypress', 'vitest', 'wdio'];
24
+ /* ----------------------------------------------------------
25
+ * Plan synthesis -- manifest -> TestPlan
26
+ * ---------------------------------------------------------- */
27
+ export function planFromManifest(manifest, options) {
28
+ const plugin = manifest.plugin_slug || 'unknown';
29
+ const els = manifest.elements || [];
30
+ const cases = [];
31
+ let exercised = 0;
32
+ const fixtures = (manifest && manifest.fixtures) || {};
33
+ const haveFixtureFor = (param) => !!fixtures[param];
34
+ /* H1.v3: pick the test's visit route from el.mounted_at.
35
+ - empty / undefined -> default '/'
36
+ - ['*'] -> '/' (always-mounted top-level chrome)
37
+ - first entry with :params -> KEEP the params verbatim if every
38
+ :param has a fixture spec; the emitter will interpolate them
39
+ from the per-test aliases. If a :param lacks a fixture, fall
40
+ back to the v2 behaviour (strip the param).
41
+
42
+ H1.v5: when mounted_at is missing, use keyword heuristics on the
43
+ element id to infer the most likely route. Reusable components
44
+ (ArticleEditorForm, FormFieldset, etc) get a sensible route
45
+ instead of defaulting to '/'. The heuristic checks routes
46
+ declared in the manifest's route_map (when present) and matches
47
+ by id substring; falls through to '/' only when nothing matches. */
48
+ function inferRouteFromId(id) {
49
+ const lower = id.toLowerCase();
50
+ /* Keyword -> candidate route patterns. Order matters: most
51
+ specific first.
52
+ Note: word boundaries (\b) only match between word and
53
+ non-word chars; for substrings flanked by underscores we
54
+ allow either \b or _ at either side. */
55
+ const rules = [
56
+ { keys: /(article_editor|publish_article|editor_form|\.field\.title|\.field\.description|\.field\.body|\.field\.tags)/, route: '/editor' },
57
+ { keys: /(delete_article|edit_article|favorite_article|article_meta|article_buttons|article_preview|edit_article_link|comment_author|delete_comment|follow_author|comment_body|post_comment|follow_button)/, route: '/article/:slug' },
58
+ { keys: /(signup|sign_up_form|auth_page_container__sign_up)/, route: '/register' },
59
+ { keys: /(login_form|login_page)/, route: '/login' },
60
+ { keys: /(edit_profile_settings|profile_articles|profile_fav|follow_user|fav_articles_preview|fav_articles_pagination)/, route: '/@:username' },
61
+ { keys: /\.settings\b|region\.settings\b/, route: '/settings' },
62
+ { keys: /(paginate|tag_button|feed_nav_link|feed_)/, route: '/' },
63
+ { keys: /(auth_link|dropdown_item|nav_item|form_fieldset)/, route: '/' },
64
+ ];
65
+ for (const r of rules) {
66
+ if (r.keys.test(lower))
67
+ return r.route;
68
+ }
69
+ return null;
70
+ }
71
+ function pickVisitRoute(el) {
72
+ let ma = el.mounted_at;
73
+ let raw;
74
+ if (!ma || ma.length === 0) {
75
+ const inferred = inferRouteFromId(el.id || '');
76
+ if (!inferred)
77
+ return { route: '/', params: [] };
78
+ raw = inferred;
79
+ }
80
+ else {
81
+ if (ma[0] === '*')
82
+ return { route: '/', params: [] };
83
+ /* H1.v6: when multiple mounted_at routes exist, prefer a
84
+ conventional `/segment/:param` over unusual prefixes like
85
+ `/@:param`. Most React Router apps don't use `@` as a path
86
+ segment; when both forms appear in mounted_at, the
87
+ conventional one is more likely to be the real route. When
88
+ only the `/@:x` form is present, rewrite it to `/profile/:x`
89
+ since that's the canonical RealWorld convention. */
90
+ const conventional = ma.find((r) => r !== '*' && !r.includes('@'));
91
+ raw = (conventional || ma[0]);
92
+ if (raw.startsWith('/@'))
93
+ raw = raw.replace(/^\/@/, '/profile/');
94
+ }
95
+ const paramTokens = Array.from(raw.matchAll(/:([a-zA-Z_][a-zA-Z0-9_]*)/g)).map(m => ':' + m[1]);
96
+ if (paramTokens.length === 0)
97
+ return { route: raw, params: [] };
98
+ const allHaveFixture = paramTokens.every(haveFixtureFor);
99
+ if (allHaveFixture) {
100
+ return { route: raw, params: paramTokens };
101
+ }
102
+ /* At least one :param has no fixture spec -- strip them all and
103
+ fall back to the param-less prefix. */
104
+ return { route: raw.replace(/:[a-zA-Z_][a-zA-Z0-9_]*/g, ''), params: [] };
105
+ }
106
+ for (const el of els) {
107
+ if (!el || !el.id)
108
+ continue;
109
+ /* H1.v5: skip region_present tests for elements that --ai-apply
110
+ flagged as marker-only ("covers the outer container region",
111
+ "marks the mount point"). The region id never reaches the
112
+ DOM; testing presence is a guaranteed false-positive failure.
113
+ Field / action / tab tests on warned elements ARE kept --
114
+ they may now resolve after operator-level FormFieldset-style
115
+ prop-forwarding fixes. The warning list shipped at apply-time
116
+ remains the canonical audit trail. */
117
+ const isMarkerRegion = (el.role === 'region') && !!el.warning;
118
+ if (isMarkerRegion)
119
+ continue;
120
+ /* H1.v6: skip presence-checking tests for elements whose
121
+ manifest entry is a TEMPLATE (carries instance_pattern). Such
122
+ entries are never in DOM unsuffixed -- DOM has the qualified
123
+ instances only (`conduit.field.form_fieldset__title` etc).
124
+ region_present + field_read + tab_switch on the bare id are
125
+ guaranteed false negatives. Action/field_write tests with
126
+ qualifier wiring (H1.v6 below) can still resolve. */
127
+ const isTemplate = !!el.instance_pattern;
128
+ const role = el.role;
129
+ const id = el.id;
130
+ const picked = pickVisitRoute(el);
131
+ const visit_route = picked.route;
132
+ const fixture_params = picked.params.length ? picked.params : undefined;
133
+ const requires_auth = !!el.requires_auth;
134
+ if (role === 'action') {
135
+ const verbs = (el.actions || []).map((a) => a && a.verb).filter(Boolean);
136
+ /* H1.v6: when this element is a template, derive a qualifier
137
+ alias from the first fixture param. The emitter wraps the
138
+ test in a cy.get('@alias').then() and forwards the value
139
+ as opts.qualifier to NAC.click_by_verb. */
140
+ const qualifier_alias = (isTemplate && fixture_params && fixture_params.length > 0)
141
+ ? fixture_params[0].replace(/^:/, '')
142
+ : undefined;
143
+ for (const verb of verbs) {
144
+ /* H1.v5: skip verbs that mutate global session state (logout
145
+ drops the JWT, signup creates a new user, save on a
146
+ settings-shaped page rewrites the current user's password
147
+ with the value of empty/inputted fields). Without a
148
+ per-test user reset, these tests poison every test that
149
+ follows. Operator can opt back in via --include-session
150
+ when test isolation is in place. */
151
+ const sessionMutating = /^(logout|signup|save)$/i.test(verb);
152
+ if (sessionMutating)
153
+ continue;
154
+ /* H1.v6 item 4: verbs that require viewer != author. The
155
+ target object (e.g. an article) must be authored by
156
+ someone OTHER than the test user; otherwise the affected
157
+ UI element doesn't render. The emitter wires a secondary
158
+ user into the fixture create path. */
159
+ const viewer_role = /^(follow|unfollow|favorite|unfavorite)$/i.test(verb) && qualifier_alias
160
+ ? 'secondary_author'
161
+ : 'primary';
162
+ cases.push({
163
+ id: 'action__' + id.replace(/\./g, '_') + '__' + verb,
164
+ kind: 'action_dispatch',
165
+ name: 'dispatches ' + plugin + '.' + verb,
166
+ plugin, nac_id: id, verb,
167
+ visit_route, requires_auth, fixture_params, qualifier_alias,
168
+ viewer_role,
169
+ });
170
+ }
171
+ exercised++;
172
+ }
173
+ else if (role === 'field') {
174
+ /* H1.v6: skip field_read for templates -- bare id never in DOM. */
175
+ if (!isTemplate)
176
+ cases.push({
177
+ id: 'field_read__' + id.replace(/\./g, '_'),
178
+ kind: 'field_read',
179
+ name: 'reads ' + id,
180
+ plugin, nac_id: id,
181
+ visit_route, requires_auth, fixture_params,
182
+ });
183
+ /* H1.v5: skip field_write for credential / identity fields.
184
+ Their values mutate the test user's session and break every
185
+ subsequent test that relies on the same login. Sample
186
+ inputs cannot be made round-trip-safe without app-specific
187
+ knowledge of the save endpoint. Operator can opt back in
188
+ via --include-credentials when they accept the side effect. */
189
+ const isCredential = /password|email|username|api[-_]?key|token/i.test(id);
190
+ if (!isCredential && /input|edit|name|title|search|comment|body/i.test(id)) {
191
+ cases.push({
192
+ id: 'field_write__' + id.replace(/\./g, '_'),
193
+ kind: 'field_write',
194
+ name: 'writes to ' + id,
195
+ plugin, nac_id: id,
196
+ /* Deterministic per-id seed instead of Math.random so the
197
+ suite is byte-reproducible across runs. */
198
+ sample_input: 'forge-e2e-' + id.length.toString(36) + 'v' + (id.split('.').length),
199
+ visit_route, requires_auth, fixture_params,
200
+ });
201
+ }
202
+ exercised++;
203
+ }
204
+ else if (role === 'tab') {
205
+ /* H1.v6: skip tab_switch for templates -- DOM has only qualified
206
+ instances. NAC.tab() runtime fallback handles base→qualified
207
+ when the QUALIFIED siblings carry a known prefix, but for
208
+ pure templates (like conduit.tab.feed_nav_link) there is no
209
+ agent-meaningful target without picking an instance. */
210
+ if (!isTemplate)
211
+ cases.push({
212
+ id: 'tab__' + id.replace(/\./g, '_'),
213
+ kind: 'tab_switch',
214
+ name: 'switches to tab ' + id,
215
+ plugin, nac_id: id,
216
+ visit_route, requires_auth, fixture_params,
217
+ });
218
+ exercised++;
219
+ }
220
+ else if (role === 'region') {
221
+ /* H1.v6: skip region_present for templates -- bare id never in DOM. */
222
+ if (!isTemplate)
223
+ cases.push({
224
+ id: 'region__' + id.replace(/\./g, '_'),
225
+ kind: 'region_present',
226
+ name: 'region ' + id + ' is present',
227
+ plugin, nac_id: id,
228
+ visit_route, requires_auth, fixture_params,
229
+ });
230
+ exercised++;
231
+ }
232
+ else if (role === 'navigation') {
233
+ const verbs = (el.actions || []).map((a) => a && a.verb).filter(Boolean);
234
+ if (verbs.length === 0) {
235
+ cases.push({
236
+ id: 'nav__' + id.replace(/\./g, '_'),
237
+ kind: 'action_dispatch',
238
+ name: 'nav ' + id + ' present',
239
+ plugin, nac_id: id, verb: 'navigate',
240
+ visit_route, requires_auth, fixture_params,
241
+ });
242
+ }
243
+ else {
244
+ for (const verb of verbs) {
245
+ cases.push({
246
+ id: 'nav__' + id.replace(/\./g, '_') + '__' + verb,
247
+ kind: 'action_dispatch',
248
+ name: 'navigates ' + plugin + '.' + verb,
249
+ plugin, nac_id: id, verb,
250
+ visit_route, requires_auth, fixture_params,
251
+ });
252
+ }
253
+ }
254
+ exercised++;
255
+ }
256
+ }
257
+ const rk = manifest && manifest.router_kind;
258
+ const router_kind = (rk === 'hash' || rk === 'memory' || rk === 'browser') ? rk : undefined;
259
+ return {
260
+ plugin,
261
+ manifest_path: 'manifest.json',
262
+ page_url_token: options?.pageUrlToken || 'http://localhost:3000',
263
+ cases,
264
+ total_elements: els.length,
265
+ exercised_elements: exercised,
266
+ fixtures: Object.keys(fixtures).length > 0 ? fixtures : undefined,
267
+ router_kind,
268
+ };
269
+ }
270
+ /* ----------------------------------------------------------
271
+ * Auto-detect a framework from the target dir
272
+ * ---------------------------------------------------------- */
273
+ export async function detectFramework(targetDir) {
274
+ const checks = [
275
+ { file: 'playwright.config.ts', framework: 'playwright' },
276
+ { file: 'playwright.config.js', framework: 'playwright' },
277
+ { file: 'cypress.config.ts', framework: 'cypress' },
278
+ { file: 'cypress.config.js', framework: 'cypress' },
279
+ { file: 'cypress.json', framework: 'cypress' }, // legacy cypress
280
+ { file: 'vitest.config.ts', framework: 'vitest' },
281
+ { file: 'vitest.config.js', framework: 'vitest' },
282
+ { file: 'wdio.conf.ts', framework: 'wdio' },
283
+ { file: 'wdio.conf.js', framework: 'wdio' },
284
+ ];
285
+ for (const ch of checks) {
286
+ const ok = await fs.stat(path.join(targetDir, ch.file)).then(() => true, () => false);
287
+ if (ok)
288
+ return ch.framework;
289
+ }
290
+ /* Also look in subdirs cypress/, e2e/. */
291
+ const cypress = await fs.stat(path.join(targetDir, 'cypress')).then(() => true, () => false);
292
+ if (cypress)
293
+ return 'cypress';
294
+ return null;
295
+ }
296
+ export function emitPlaywright(plan, opts) {
297
+ const baseUrl = opts?.baseUrl || plan.page_url_token;
298
+ const body = [
299
+ '/* Auto-generated by Yujin Forge -- e2e test corpus.',
300
+ ' * Plugin: ' + plan.plugin,
301
+ ' * Surface coverage: ' + plan.exercised_elements + ' / ' + plan.total_elements + ' manifest elements.',
302
+ ' * Framework: Playwright. Re-generate with: yf gen-tests <dir> --framework playwright',
303
+ ' */',
304
+ "import { test, expect } from '@playwright/test';",
305
+ "",
306
+ "const BASE_URL = process.env.FORGE_E2E_BASE_URL || '" + baseUrl + "';",
307
+ "",
308
+ "async function gotoAndWait(page) {",
309
+ " await page.goto(BASE_URL, { waitUntil: 'networkidle' });",
310
+ " await page.waitForFunction(",
311
+ " (slug) => !!(window.NAC && window.NAC.list_registered_plugins().includes(slug)),",
312
+ " '" + plan.plugin + "',",
313
+ " { timeout: 10000 }",
314
+ " );",
315
+ "}",
316
+ "",
317
+ ];
318
+ for (const tc of plan.cases) {
319
+ if (tc.kind === 'action_dispatch') {
320
+ body.push("test('" + tc.name + "', async ({ page }) => {");
321
+ body.push(" await gotoAndWait(page);");
322
+ body.push(" const result = await page.evaluate(async (v) => {");
323
+ body.push(" try { await window.NAC.click_by_verb('" + tc.plugin + "', v); return { ok: true }; }");
324
+ body.push(" catch (e) { return { ok: false, error: String(e.message || e) }; }");
325
+ body.push(" }, '" + tc.verb + "');");
326
+ body.push(" expect(result.ok, JSON.stringify(result)).toBe(true);");
327
+ body.push("});");
328
+ body.push("");
329
+ }
330
+ else if (tc.kind === 'field_read') {
331
+ body.push("test('" + tc.name + "', async ({ page }) => {");
332
+ body.push(" await gotoAndWait(page);");
333
+ body.push(" const el = await page.$('[data-nac-id=\"" + tc.nac_id + "\"]');");
334
+ body.push(" expect(el, 'Element " + tc.nac_id + " not found').not.toBeNull();");
335
+ body.push("});");
336
+ body.push("");
337
+ }
338
+ else if (tc.kind === 'field_write') {
339
+ body.push("test('" + tc.name + "', async ({ page }) => {");
340
+ body.push(" await gotoAndWait(page);");
341
+ body.push(" const result = await page.evaluate(async (id, val) => {");
342
+ body.push(" try { await window.NAC.fill(id, val); return { ok: true }; }");
343
+ body.push(" catch (e) { return { ok: false, error: String(e.message || e) }; }");
344
+ body.push(" }, '" + tc.nac_id + "', '" + tc.sample_input + "');");
345
+ body.push(" expect(result.ok, JSON.stringify(result)).toBe(true);");
346
+ body.push("});");
347
+ body.push("");
348
+ }
349
+ else if (tc.kind === 'tab_switch') {
350
+ body.push("test('" + tc.name + "', async ({ page }) => {");
351
+ body.push(" await gotoAndWait(page);");
352
+ body.push(" const result = await page.evaluate(async (id) => {");
353
+ body.push(" try { await window.NAC.tab(id); return { ok: true }; }");
354
+ body.push(" catch (e) { return { ok: false, error: String(e.message || e) }; }");
355
+ body.push(" }, '" + tc.nac_id + "');");
356
+ body.push(" expect(result.ok, JSON.stringify(result)).toBe(true);");
357
+ body.push("});");
358
+ body.push("");
359
+ }
360
+ else if (tc.kind === 'region_present') {
361
+ body.push("test('" + tc.name + "', async ({ page }) => {");
362
+ body.push(" await gotoAndWait(page);");
363
+ body.push(" const el = await page.$('[data-nac-id=\"" + tc.nac_id + "\"]');");
364
+ body.push(" expect(el, 'Region " + tc.nac_id + " not found').not.toBeNull();");
365
+ body.push("});");
366
+ body.push("");
367
+ }
368
+ }
369
+ return [{ relpath: 'tests/forge-e2e/' + plan.plugin + '.spec.ts', content: body.join('\n') }];
370
+ }
371
+ export function emitCypress(plan, opts) {
372
+ const baseUrl = (opts?.baseUrl || plan.page_url_token).replace(/\/$/, '');
373
+ const fixtures = plan.fixtures || {};
374
+ const body = [
375
+ '/* Auto-generated by Yujin Forge -- e2e test corpus.',
376
+ ' * Plugin: ' + plan.plugin,
377
+ ' * Surface coverage: ' + plan.exercised_elements + ' / ' + plan.total_elements + ' manifest elements.',
378
+ ' * Framework: Cypress. Re-generate with: yf gen-tests <dir> --framework cypress',
379
+ ' * H1.v3: each test that needs dynamic-route data CREATEs the',
380
+ ' * resource via API in beforeEach + DELETEs in afterEach so the',
381
+ ' * suite is reproducible from a clean DB. Seed is deterministic',
382
+ ' * (test index), never random.',
383
+ ' */',
384
+ "const BASE_URL = Cypress.env('FORGE_E2E_BASE_URL') || '" + baseUrl + "';",
385
+ "/* H1.v4: Router kind from manifest. 'hash' -> cy.visit prefixes",
386
+ " the route with '#' so SPA hash router engages. */",
387
+ "const ROUTER_KIND = '" + (plan.router_kind || 'browser') + "';",
388
+ "const FORGE_TEST_USER = Cypress.env('FORGE_TEST_USER') || {",
389
+ " email: 'forge-e2e@yujin.app',",
390
+ " password: 'forge-e2e-2026',",
391
+ " username: 'forge_e2e'",
392
+ "};",
393
+ "/* H1.v6 item 4 -- secondary user for multi-user fixtures.",
394
+ " Verbs like follow_author / favorite_article require the viewer",
395
+ " to differ from the author. This second user authors fixtures",
396
+ " that the primary test user then interacts with. Deterministic",
397
+ " credentials, idempotent registration. */",
398
+ "const FORGE_TEST_USER2 = Cypress.env('FORGE_TEST_USER2') || {",
399
+ " email: 'forge-e2e-other@yujin.app',",
400
+ " password: 'forge-e2e-2026',",
401
+ " username: 'forge_e2e_other'",
402
+ "};",
403
+ "",
404
+ "function ensureTestUser(user) {",
405
+ " /* Idempotent: register if absent, ignore conflict. */",
406
+ " cy.request({",
407
+ " method: 'POST',",
408
+ " url: BASE_URL + '/api/users',",
409
+ " body: { user: user || FORGE_TEST_USER },",
410
+ " failOnStatusCode: false,",
411
+ " });",
412
+ "}",
413
+ "function loginAs(user) {",
414
+ " /* Login as the supplied user and snapshot the resulting token",
415
+ " into Cypress.env. Used by secondary-author fixtures to create",
416
+ " fixtures as user2, then swap back to user1 before the it() body.",
417
+ " H1.v6 stabilizer: tokens are cached per-user in",
418
+ " Cypress.env('FORGE_AUTH_TOKEN_' + email). The cache survives",
419
+ " for the entire spec run since JWT lifetimes far exceed it.",
420
+ " This eliminates the multi-user login race: switching users",
421
+ " becomes a synchronous env swap, not a fresh HTTP round-trip.",
422
+ " Cy.env writes outside cy.then() execute IMMEDIATELY in queue",
423
+ " order, so the env is always swapped before the next command. */",
424
+ " const cacheKey = 'FORGE_AUTH_TOKEN_' + user.email;",
425
+ " const cached = Cypress.env(cacheKey);",
426
+ " if (cached) {",
427
+ " Cypress.env('FORGE_AUTH_TOKEN', cached);",
428
+ " return cy.wrap(cached, { log: false });",
429
+ " }",
430
+ " return cy.request({",
431
+ " method: 'POST',",
432
+ " url: BASE_URL + '/api/users/login',",
433
+ " body: { user: { email: user.email, password: user.password } },",
434
+ " failOnStatusCode: false,",
435
+ " }).then((resp) => {",
436
+ " if (resp.status !== 200 || !resp.body || !resp.body.user) return null;",
437
+ " const tok = resp.body.user.token;",
438
+ " Cypress.env(cacheKey, tok);",
439
+ " Cypress.env('FORGE_AUTH_TOKEN', tok);",
440
+ " return tok;",
441
+ " });",
442
+ "}",
443
+ "",
444
+ "/* loginViaApi caches the token under Cypress.env so subsequent",
445
+ " cy.request calls (fixture CREATE / DELETE) can include the",
446
+ " Authorization header. Also drops the SPA-shaped wrapper into",
447
+ " localStorage so cy.visit boots already logged-in. */",
448
+ "function loginViaApi() {",
449
+ " /* H1.v6 stabilizer: same caching strategy as loginAs(). When the",
450
+ " primary user token is already cached, swap env synchronously. */",
451
+ " const cacheKey = 'FORGE_AUTH_TOKEN_' + FORGE_TEST_USER.email;",
452
+ " const cached = Cypress.env(cacheKey);",
453
+ " if (cached) {",
454
+ " Cypress.env('FORGE_AUTH_TOKEN', cached);",
455
+ " return cy.wrap(cached, { log: false });",
456
+ " }",
457
+ " return cy.request({",
458
+ " method: 'POST',",
459
+ " url: BASE_URL + '/api/users/login',",
460
+ " body: { user: { email: FORGE_TEST_USER.email, password: FORGE_TEST_USER.password } },",
461
+ " failOnStatusCode: false,",
462
+ " }).then((resp) => {",
463
+ " if (resp.status !== 200 || !resp.body || !resp.body.user) return null;",
464
+ " const user = resp.body.user;",
465
+ " Cypress.env(cacheKey, user.token);",
466
+ " Cypress.env('FORGE_AUTH_TOKEN', user.token);",
467
+ " return user.token;",
468
+ " });",
469
+ "}",
470
+ "function authHeaders() {",
471
+ " const t = Cypress.env('FORGE_AUTH_TOKEN');",
472
+ " return t ? { Authorization: 'Token ' + t } : {};",
473
+ "}",
474
+ "",
475
+ "function gotoAndWait(route) {",
476
+ " const r = route || '/';",
477
+ " /* HashRouter: cy.visit must hit BASE_URL/#/<route>. */",
478
+ " const url = ROUTER_KIND === 'hash' ? (BASE_URL + '/#' + r) : (BASE_URL + r);",
479
+ " /* Auth-screen routes must always render as unauth. Skip seeding. */",
480
+ " const isAuthScreen = /^\\/(login|register|signup|sign-up|sign_up)\\b/.test(r);",
481
+ " const tok = isAuthScreen ? null : Cypress.env('FORGE_AUTH_TOKEN');",
482
+ " if (tok) {",
483
+ " cy.visit(url, {",
484
+ " onBeforeLoad(win) {",
485
+ " const u = " + JSON.stringify({ email: 'forge-e2e@yujin.app', username: 'forge_e2e' }) + ";",
486
+ " const wrapper = { headers: { Authorization: 'Token ' + tok }, isAuth: true, loggedUser: Object.assign({}, u, { token: tok }) };",
487
+ " win.localStorage.setItem('loggedUser', JSON.stringify(wrapper));",
488
+ " },",
489
+ " });",
490
+ " } else {",
491
+ " cy.visit(url);",
492
+ " }",
493
+ " cy.window({ timeout: 10000 }).should((win) => {",
494
+ " expect(win.NAC).to.exist;",
495
+ " expect(win.NAC.list_registered_plugins()).to.include('" + plan.plugin + "');",
496
+ " });",
497
+ " /* H1.v6: NAC manifest registration fires when nac.browser.js",
498
+ " evaluates -- BEFORE React renders the route's content. For",
499
+ " parametric routes (article/:slug, @:username) the route's",
500
+ " component fetches data asynchronously; the action buttons",
501
+ " mount only after that resolves. Wait until the DOM has at",
502
+ " least one decorated INTERACTIVE element (data-nac-action),",
503
+ " not just any data-nac-id (some top-level nav exists at all",
504
+ " routes and would satisfy the looser check). */",
505
+ " if (/:[a-zA-Z]/.test(r)) {",
506
+ " cy.document({ timeout: 8000 }).should((doc) => {",
507
+ " const n = doc.querySelectorAll('[data-nac-action]').length;",
508
+ " expect(n, 'data-nac-action element count').to.be.greaterThan(5);",
509
+ " });",
510
+ " }",
511
+ "}",
512
+ "",
513
+ "/* H1.v3 helpers: deterministic seed + dotted-path get/set. */",
514
+ "function getByPath(obj, path) {",
515
+ " if (!obj || !path) return undefined;",
516
+ " return path.split('.').reduce((a, k) => (a == null ? a : a[k]), obj);",
517
+ "}",
518
+ "function setByPath(obj, path, value) {",
519
+ " const parts = path.split('.'); let o = obj;",
520
+ " for (let i = 0; i < parts.length - 1; i++) { if (o[parts[i]] == null) o[parts[i]] = {}; o = o[parts[i]]; }",
521
+ " o[parts[parts.length - 1]] = value;",
522
+ "}",
523
+ "function applySeedToPayload(payload, uniqueCsv, seed) {",
524
+ " const cloned = JSON.parse(JSON.stringify(payload));",
525
+ " if (!uniqueCsv) return cloned;",
526
+ " uniqueCsv.split(',').map(s => s.trim()).filter(Boolean).forEach(p => {",
527
+ " const v = getByPath(cloned, p);",
528
+ " if (typeof v === 'string') setByPath(cloned, p, v.replace(/\\{seed\\}/g, String(seed)));",
529
+ " });",
530
+ " /* Also any string value in the cloned payload that still contains {seed} gets substituted. */",
531
+ " (function walk(node) {",
532
+ " if (node == null) return;",
533
+ " if (Array.isArray(node)) { node.forEach((v, i) => { if (typeof v === 'string' && v.indexOf('{seed}') >= 0) node[i] = v.replace(/\\{seed\\}/g, String(seed)); else walk(v); }); return; }",
534
+ " if (typeof node === 'object') Object.keys(node).forEach(k => { if (typeof node[k] === 'string' && node[k].indexOf('{seed}') >= 0) node[k] = node[k].replace(/\\{seed\\}/g, String(seed)); else walk(node[k]); });",
535
+ " })(cloned);",
536
+ " return cloned;",
537
+ "}",
538
+ "function splitMethodPath(endpoint) {",
539
+ " const m = String(endpoint || '').match(/^(\\S+)\\s+(.+)$/);",
540
+ " return m ? { method: m[1].toUpperCase(), path: m[2] } : null;",
541
+ "}",
542
+ "",
543
+ "/* Fixtures spec emitted by Forge --ai-apply. Static literal so the",
544
+ " suite is reproducible byte-by-byte. */",
545
+ "const FIXTURES = " + JSON.stringify(fixtures, null, 2) + ";",
546
+ "",
547
+ "/* H1.v6: purge stale fixture articles from BOTH test users before",
548
+ " the suite runs. A test that crashed mid-it() leaves an article",
549
+ " behind; the next run hits 'slug already taken' and the fixture",
550
+ " captures undefined as the slug -> downstream tests fail with",
551
+ " qualifier=undefined. The purge is a one-time sweep over both",
552
+ " users' authored articles via cy.request to /api/articles. */",
553
+ "function purgeFixtureArticles(user) {",
554
+ " return cy.request({",
555
+ " method: 'POST',",
556
+ " url: BASE_URL + '/api/users/login',",
557
+ " body: { user: { email: user.email, password: user.password } },",
558
+ " failOnStatusCode: false,",
559
+ " }).then((loginResp) => {",
560
+ " if (loginResp.status !== 200) return;",
561
+ " const tok = loginResp.body.user.token;",
562
+ " return cy.request({",
563
+ " method: 'GET',",
564
+ " url: BASE_URL + '/api/articles?author=' + encodeURIComponent(user.username) + '&limit=100',",
565
+ " headers: { Authorization: 'Token ' + tok },",
566
+ " failOnStatusCode: false,",
567
+ " }).then((listResp) => {",
568
+ " const arts = (listResp.body && listResp.body.articles) || [];",
569
+ " arts.forEach((a) => {",
570
+ " cy.request({",
571
+ " method: 'DELETE',",
572
+ " url: BASE_URL + '/api/articles/' + a.slug,",
573
+ " headers: { Authorization: 'Token ' + tok },",
574
+ " failOnStatusCode: false,",
575
+ " });",
576
+ " });",
577
+ " });",
578
+ " });",
579
+ "}",
580
+ "/* H1.v6 item 5 -- minimal seed data for pagination/filter tests.",
581
+ " Creates a small set of articles under FORGE_TEST_USER so the",
582
+ " home feed has > 1 page and tags exist. Without this seed,",
583
+ " ArticlesPagination doesn't mount (no second page) and the tag",
584
+ " list is empty. Deterministic titles -> deterministic slugs. */",
585
+ "function seedHomeFeed() {",
586
+ " return cy.request({",
587
+ " method: 'POST',",
588
+ " url: BASE_URL + '/api/users/login',",
589
+ " body: { user: { email: FORGE_TEST_USER.email, password: FORGE_TEST_USER.password } },",
590
+ " failOnStatusCode: false,",
591
+ " }).then((loginResp) => {",
592
+ " if (loginResp.status !== 200) return;",
593
+ " const tok = loginResp.body.user.token;",
594
+ " /* 12 articles to guarantee pagination (default 10/page). */",
595
+ " for (let i = 1; i <= 12; i++) {",
596
+ " cy.request({",
597
+ " method: 'POST',",
598
+ " url: BASE_URL + '/api/articles',",
599
+ " headers: { Authorization: 'Token ' + tok },",
600
+ " body: { article: { title: 'Forge seed ' + i, description: 'seed-' + i,",
601
+ " body: 'Seed article body ' + i, tagList: i % 2 === 0 ? ['forge', 'seed'] : ['forge'] } },",
602
+ " failOnStatusCode: false,",
603
+ " });",
604
+ " }",
605
+ " });",
606
+ "}",
607
+ "/* H1.v6 stabilizer: warm the token cache for both users up-front.",
608
+ " Subsequent loginAs / loginViaApi calls are instant env swaps,",
609
+ " eliminating the per-test login race that drove ±2-test variance",
610
+ " between runs. */",
611
+ "function warmTokenCache(user) {",
612
+ " return cy.request({",
613
+ " method: 'POST',",
614
+ " url: BASE_URL + '/api/users/login',",
615
+ " body: { user: { email: user.email, password: user.password } },",
616
+ " failOnStatusCode: false,",
617
+ " }).then((resp) => {",
618
+ " if (resp.status === 200 && resp.body && resp.body.user) {",
619
+ " Cypress.env('FORGE_AUTH_TOKEN_' + user.email, resp.body.user.token);",
620
+ " }",
621
+ " });",
622
+ "}",
623
+ "before(() => {",
624
+ " ensureTestUser(FORGE_TEST_USER);",
625
+ " ensureTestUser(FORGE_TEST_USER2);",
626
+ " warmTokenCache(FORGE_TEST_USER);",
627
+ " warmTokenCache(FORGE_TEST_USER2);",
628
+ " purgeFixtureArticles(FORGE_TEST_USER);",
629
+ " purgeFixtureArticles(FORGE_TEST_USER2);",
630
+ " seedHomeFeed();",
631
+ "});",
632
+ "/* H1.v6: Cypress.env persists across tests. Tests that visit auth",
633
+ " screens (/login, /register) should NOT inherit a prior test's",
634
+ " token, since the page would render the logged-in redirect. The",
635
+ " gotoAndWait function skips localStorage seeding for these routes",
636
+ " even when Cypress.env still has a token. */",
637
+ "",
638
+ "describe('" + plan.plugin + " -- Forge e2e corpus', () => {",
639
+ ];
640
+ /* Emit each test. Tests with fixture_params get a per-test
641
+ beforeEach (create) + afterEach (delete). Tests without
642
+ fixture_params keep the simple shape from H1.v2. */
643
+ let testIndex = 0;
644
+ for (const tc of plan.cases) {
645
+ testIndex++;
646
+ const route = tc.visit_route || '/';
647
+ const params = tc.fixture_params || [];
648
+ const hasFixtures = params.length > 0;
649
+ if (hasFixtures) {
650
+ /* Open a context() to scope beforeEach / afterEach. */
651
+ body.push(" context('" + tc.name + " (with fixtures)', () => {");
652
+ /* Per-test deterministic seed: stable hash of test index. */
653
+ body.push(" const SEED = " + testIndex + ";");
654
+ /* beforeEach: create one resource per :param.
655
+ H1.v6 item 4: for viewer_role='secondary_author', the fixture
656
+ is created by FORGE_TEST_USER2 so the primary test user can
657
+ interact with it (follow/favorite/unfollow logic requires
658
+ viewer != author). After the fixture is created, swap back
659
+ to the primary user. */
660
+ const secondaryAuthor = tc.viewer_role === 'secondary_author';
661
+ body.push(" beforeEach(() => {");
662
+ if (secondaryAuthor) {
663
+ body.push(" loginAs(FORGE_TEST_USER2);");
664
+ }
665
+ else if (tc.requires_auth || params.some(p => fixtures[p] && fixtures[p].requires_auth)) {
666
+ body.push(" loginViaApi();");
667
+ }
668
+ for (const p of params) {
669
+ const spec = fixtures[p];
670
+ if (!spec)
671
+ continue;
672
+ const alias = p.replace(/^:/, '');
673
+ const useAuth = !!spec.requires_auth;
674
+ /* H1.v3: wrap inside cy.then() so authHeaders() is evaluated
675
+ AFTER loginViaApi() has populated Cypress.env -- not at
676
+ the synchronous emit-time of the test body. */
677
+ body.push(" cy.then(() => {");
678
+ body.push(" const spec = FIXTURES['" + p + "'];");
679
+ body.push(" const mp = splitMethodPath(spec.create_endpoint);");
680
+ body.push(" const payload = applySeedToPayload(spec.create_payload, spec.unique_suffix_field, SEED);");
681
+ body.push(" return cy.request({ method: mp.method, url: BASE_URL + mp.path, body: payload, headers: " + (useAuth ? "authHeaders()" : "{}") + ", failOnStatusCode: false })");
682
+ body.push(" .then((resp) => { const v = getByPath(resp.body, spec.response_path); cy.wrap(v).as('" + alias + "'); });");
683
+ body.push(" });");
684
+ }
685
+ if (secondaryAuthor) {
686
+ /* Swap back to primary so the it() body runs as the viewer. */
687
+ body.push(" loginViaApi();");
688
+ }
689
+ body.push(" });");
690
+ /* afterEach: delete each resource (idempotent). */
691
+ const anyDeletable = params.some(p => fixtures[p] && fixtures[p].delete_endpoint);
692
+ if (anyDeletable) {
693
+ body.push(" afterEach(() => {");
694
+ for (const p of params) {
695
+ const spec = fixtures[p];
696
+ if (!spec || !spec.delete_endpoint)
697
+ continue;
698
+ const alias = p.replace(/^:/, '');
699
+ const useAuth = !!spec.requires_auth;
700
+ /* afterEach: also defer authHeaders() into the cy.then closure. */
701
+ body.push(" cy.get('@" + alias + "').then((val) => {");
702
+ body.push(" const spec = FIXTURES['" + p + "'];");
703
+ body.push(" const mp = splitMethodPath(spec.delete_endpoint.replace(/\\{value\\}/g, String(val)));");
704
+ body.push(" const hdrs = " + (useAuth ? "authHeaders()" : "{}") + ";");
705
+ body.push(" cy.request({ method: mp.method, url: BASE_URL + mp.path, headers: hdrs, failOnStatusCode: false });");
706
+ body.push(" });");
707
+ }
708
+ body.push(" });");
709
+ }
710
+ /* The it() body: visit interpolated route + dispatch. */
711
+ body.push(" it('" + tc.name + "', () => {");
712
+ /* Build a sequence that resolves each alias into a local var
713
+ then visits the route with placeholders substituted. */
714
+ const aliases = params.map(p => p.replace(/^:/, ''));
715
+ if (aliases.length === 1) {
716
+ const a = aliases[0];
717
+ const pName = params[0];
718
+ body.push(" cy.get('@" + a + "').then((" + a + ") => {");
719
+ body.push(" const route = '" + route + "'.replace('" + pName + "', String(" + a + "));");
720
+ body.push(" gotoAndWait(route);");
721
+ emitBodyDispatch(body, tc, ' ');
722
+ body.push(" });");
723
+ }
724
+ else {
725
+ body.push(" cy.then(function () {");
726
+ body.push(" const route = " + JSON.stringify(route) + "");
727
+ for (const p of params) {
728
+ const a = p.replace(/^:/, '');
729
+ body.push(" .replace('" + p + "', String(this." + a + "))");
730
+ }
731
+ body.push(" ; gotoAndWait(route);");
732
+ emitBodyDispatch(body, tc, ' ');
733
+ body.push(" });");
734
+ }
735
+ body.push(" });");
736
+ body.push(" });");
737
+ }
738
+ else {
739
+ /* No fixtures needed. Single it(). */
740
+ body.push(" it('" + tc.name + "', () => {");
741
+ if (tc.requires_auth)
742
+ body.push(" loginViaApi();");
743
+ body.push(" gotoAndWait('" + route + "');");
744
+ emitBodyDispatch(body, tc, ' ');
745
+ body.push(" });");
746
+ }
747
+ }
748
+ body.push("});");
749
+ return [{ relpath: 'cypress/e2e/forge/' + plan.plugin + '.cy.js', content: body.join('\n') }];
750
+ }
751
+ /* Helper -- emit the dispatch portion of a test's it() body. */
752
+ function emitBodyDispatch(body, tc, indent) {
753
+ /* H1.v6: when this test carries a qualifier_alias (template-id action
754
+ with a per-test fixture value), forward that value to NAC as
755
+ opts.qualifier so click_by_verb resolves to the right
756
+ dynamically-qualified DOM instance (e.g. distinguishing
757
+ delete_article from delete_comment, follow_author from follow_user). */
758
+ const q = tc.qualifier_alias;
759
+ /* H1.v6 item 4: for secondary-author fixtures the qualifier alias
760
+ is the article slug, but the actual DOM qualifier on follow_author
761
+ / favorite_article is author.username -- the alias doesn't match
762
+ the runtime suffix. Skip the qualifier; click_by_verb picks the
763
+ first DOM match, which is the only follow button on the page
764
+ (the secondary author's). */
765
+ const useQualifier = q && tc.viewer_role !== 'secondary_author';
766
+ if (tc.kind === 'action_dispatch') {
767
+ if (q || tc.viewer_role === 'secondary_author') {
768
+ /* Wait for the action target to render before clicking. */
769
+ body.push(indent + "cy.get('[data-nac-action=\"" + tc.verb + "\"]', { timeout: 8000 }).should('exist');");
770
+ }
771
+ body.push(indent + "cy.window().then(async (win) => {");
772
+ if (useQualifier) {
773
+ body.push(indent + " const qval = String(" + q + ");");
774
+ body.push(indent + " await win.NAC.click_by_verb('" + tc.plugin + "', '" + tc.verb + "', { qualifier: qval });");
775
+ }
776
+ else {
777
+ body.push(indent + " await win.NAC.click_by_verb('" + tc.plugin + "', '" + tc.verb + "');");
778
+ }
779
+ body.push(indent + "});");
780
+ }
781
+ else if (tc.kind === 'field_read' || tc.kind === 'region_present') {
782
+ body.push(indent + "cy.get('[data-nac-id=\"" + tc.nac_id + "\"]').should('exist');");
783
+ }
784
+ else if (tc.kind === 'field_write') {
785
+ body.push(indent + "cy.window().then(async (win) => {");
786
+ body.push(indent + " await win.NAC.fill('" + tc.nac_id + "', '" + tc.sample_input + "');");
787
+ body.push(indent + "});");
788
+ }
789
+ else if (tc.kind === 'tab_switch') {
790
+ body.push(indent + "cy.window().then(async (win) => {");
791
+ body.push(indent + " await win.NAC.tab('" + tc.nac_id + "');");
792
+ body.push(indent + "});");
793
+ }
794
+ }
795
+ export function emitVitest(plan, opts) {
796
+ const baseUrl = opts?.baseUrl || plan.page_url_token;
797
+ const body = [
798
+ '/* Auto-generated by Yujin Forge -- e2e test corpus.',
799
+ ' * Plugin: ' + plan.plugin,
800
+ ' * Surface coverage: ' + plan.exercised_elements + ' / ' + plan.total_elements + ' manifest elements.',
801
+ ' * Framework: Vitest + jsdom. Re-generate with: yf gen-tests <dir> --framework vitest',
802
+ ' *',
803
+ ' * Vitest tests run in jsdom -- they validate the manifest + DOM',
804
+ ' * structure but do not exercise a real browser. For browser-level',
805
+ ' * dispatch tests, use the Playwright corpus.',
806
+ ' */',
807
+ "import { describe, it, expect, beforeAll } from 'vitest';",
808
+ "import fs from 'node:fs';",
809
+ "import path from 'node:path';",
810
+ "import { JSDOM } from 'jsdom';",
811
+ "",
812
+ "const BASE_HTML = path.resolve(__dirname, '..', 'index.html');",
813
+ "const MANIFEST = path.resolve(__dirname, '..', 'manifest.json');",
814
+ "",
815
+ "let dom;",
816
+ "let manifest;",
817
+ "",
818
+ "beforeAll(() => {",
819
+ " const html = fs.readFileSync(BASE_HTML, 'utf-8');",
820
+ " manifest = JSON.parse(fs.readFileSync(MANIFEST, 'utf-8'));",
821
+ " dom = new JSDOM(html);",
822
+ "});",
823
+ "",
824
+ "describe('" + plan.plugin + " -- Forge e2e corpus (structural)', () => {",
825
+ " it('manifest plugin_slug matches', () => {",
826
+ " expect(manifest.plugin_slug).toBe('" + plan.plugin + "');",
827
+ " });",
828
+ ];
829
+ for (const tc of plan.cases) {
830
+ body.push(" it('" + tc.name + "', () => {");
831
+ if (tc.kind === 'action_dispatch') {
832
+ body.push(" const els = manifest.elements.filter((e) => e.id === '" + tc.nac_id + "');");
833
+ body.push(" expect(els.length, 'manifest element ' + '" + tc.nac_id + "' + ' missing').toBe(1);");
834
+ body.push(" const verbs = (els[0].actions || []).map((a) => a.verb);");
835
+ body.push(" expect(verbs).toContain('" + tc.verb + "');");
836
+ }
837
+ else {
838
+ body.push(" const els = manifest.elements.filter((e) => e.id === '" + tc.nac_id + "');");
839
+ body.push(" expect(els.length, 'manifest element ' + '" + tc.nac_id + "' + ' missing').toBe(1);");
840
+ body.push(" const el = dom.window.document.querySelector('[data-nac-id=\"" + tc.nac_id + "\"]');");
841
+ body.push(" expect(el, 'DOM element ' + '" + tc.nac_id + "' + ' missing').not.toBeNull();");
842
+ }
843
+ body.push(" });");
844
+ }
845
+ body.push("});");
846
+ return [{ relpath: 'tests/forge-e2e/' + plan.plugin + '.test.ts', content: body.join('\n') }];
847
+ }
848
+ export function emitWdio(plan, opts) {
849
+ const baseUrl = opts?.baseUrl || plan.page_url_token;
850
+ const body = [
851
+ '/* Auto-generated by Yujin Forge -- e2e test corpus.',
852
+ ' * Plugin: ' + plan.plugin,
853
+ ' * Surface coverage: ' + plan.exercised_elements + ' / ' + plan.total_elements + ' manifest elements.',
854
+ ' * Framework: WebdriverIO. Re-generate with: yf gen-tests <dir> --framework wdio',
855
+ ' */',
856
+ "const BASE_URL = process.env.FORGE_E2E_BASE_URL || '" + baseUrl + "';",
857
+ "",
858
+ "async function gotoAndWait() {",
859
+ " await browser.url(BASE_URL);",
860
+ " await browser.waitUntil(async () => {",
861
+ " return await browser.execute((slug) => {",
862
+ " return !!(window.NAC && window.NAC.list_registered_plugins().includes(slug));",
863
+ " }, '" + plan.plugin + "');",
864
+ " }, { timeout: 10000, timeoutMsg: 'NAC manifest never registered' });",
865
+ "}",
866
+ "",
867
+ "describe('" + plan.plugin + " -- Forge e2e corpus', () => {",
868
+ ];
869
+ for (const tc of plan.cases) {
870
+ body.push(" it('" + tc.name + "', async () => {");
871
+ body.push(" await gotoAndWait();");
872
+ if (tc.kind === 'action_dispatch') {
873
+ body.push(" await browser.executeAsync(async (plugin, verb, done) => {");
874
+ body.push(" try { await window.NAC.click_by_verb(plugin, verb); done({ ok: true }); }");
875
+ body.push(" catch (e) { done({ ok: false, error: String(e.message || e) }); }");
876
+ body.push(" }, '" + tc.plugin + "', '" + tc.verb + "');");
877
+ }
878
+ else if (tc.kind === 'field_read' || tc.kind === 'region_present') {
879
+ body.push(" const el = await $('[data-nac-id=\"" + tc.nac_id + "\"]');");
880
+ body.push(" await expect(el).toBeExisting();");
881
+ }
882
+ else if (tc.kind === 'field_write') {
883
+ body.push(" await browser.executeAsync(async (id, val, done) => {");
884
+ body.push(" try { await window.NAC.fill(id, val); done({ ok: true }); }");
885
+ body.push(" catch (e) { done({ ok: false, error: String(e.message || e) }); }");
886
+ body.push(" }, '" + tc.nac_id + "', '" + tc.sample_input + "');");
887
+ }
888
+ else if (tc.kind === 'tab_switch') {
889
+ body.push(" await browser.executeAsync(async (id, done) => {");
890
+ body.push(" try { await window.NAC.tab(id); done({ ok: true }); }");
891
+ body.push(" catch (e) { done({ ok: false, error: String(e.message || e) }); }");
892
+ body.push(" }, '" + tc.nac_id + "');");
893
+ }
894
+ body.push(" });");
895
+ }
896
+ body.push("});");
897
+ return [{ relpath: 'test/specs/forge-e2e/' + plan.plugin + '.test.ts', content: body.join('\n') }];
898
+ }
899
+ export async function generateTests(plan, opts) {
900
+ const frameworks = Array.isArray(opts.framework) ? opts.framework : [opts.framework];
901
+ const results = [];
902
+ for (const fw of frameworks) {
903
+ const emit = (fw === 'playwright') ? emitPlaywright
904
+ : (fw === 'cypress') ? emitCypress
905
+ : (fw === 'vitest') ? emitVitest
906
+ : (fw === 'wdio') ? emitWdio
907
+ : null;
908
+ if (!emit)
909
+ throw new Error('Unknown framework: ' + fw);
910
+ const files = emit(plan, { baseUrl: opts.baseUrl });
911
+ const written = [];
912
+ const skipped = [];
913
+ for (const f of files) {
914
+ const abs = path.join(opts.outDir, f.relpath);
915
+ await fs.mkdir(path.dirname(abs), { recursive: true });
916
+ const exists = await fs.stat(abs).then(() => true, () => false);
917
+ if (exists && !opts.overwrite) {
918
+ skipped.push(f.relpath);
919
+ continue;
920
+ }
921
+ await fs.writeFile(abs, f.content, 'utf-8');
922
+ written.push(f.relpath);
923
+ }
924
+ results.push({
925
+ framework: fw, filesWritten: written, filesSkipped: skipped,
926
+ cases: plan.cases.length, exercised_elements: plan.exercised_elements, total_elements: plan.total_elements,
927
+ });
928
+ }
929
+ return results;
930
+ }
931
+ //# sourceMappingURL=e2e.js.map