@aglyn/plugins-ai 1.0.0-beta.143

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 (717) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +25 -0
  3. package/package.json +60 -0
  4. package/src/index.d.ts +21 -0
  5. package/src/index.js +22 -0
  6. package/src/index.js.map +1 -0
  7. package/src/lib/activity/ai-activity-actions.d.ts +106 -0
  8. package/src/lib/activity/ai-activity-actions.js +209 -0
  9. package/src/lib/activity/ai-activity-actions.js.map +1 -0
  10. package/src/lib/activity/ai-activity.d.ts +219 -0
  11. package/src/lib/activity/ai-activity.js +251 -0
  12. package/src/lib/activity/ai-activity.js.map +1 -0
  13. package/src/lib/billing/ai-overage-charge.d.ts +102 -0
  14. package/src/lib/billing/ai-overage-charge.js +258 -0
  15. package/src/lib/billing/ai-overage-charge.js.map +1 -0
  16. package/src/lib/billing/ai-overage-close.d.ts +62 -0
  17. package/src/lib/billing/ai-overage-close.js +129 -0
  18. package/src/lib/billing/ai-overage-close.js.map +1 -0
  19. package/src/lib/billing/ai-overage-cutover.d.ts +63 -0
  20. package/src/lib/billing/ai-overage-cutover.js +114 -0
  21. package/src/lib/billing/ai-overage-cutover.js.map +1 -0
  22. package/src/lib/billing/ai-overage-events.d.ts +77 -0
  23. package/src/lib/billing/ai-overage-events.js +174 -0
  24. package/src/lib/billing/ai-overage-events.js.map +1 -0
  25. package/src/lib/billing/ai-overage-gate.d.ts +147 -0
  26. package/src/lib/billing/ai-overage-gate.js +177 -0
  27. package/src/lib/billing/ai-overage-gate.js.map +1 -0
  28. package/src/lib/billing/ai-overage-ledger.d.ts +135 -0
  29. package/src/lib/billing/ai-overage-ledger.js +278 -0
  30. package/src/lib/billing/ai-overage-ledger.js.map +1 -0
  31. package/src/lib/billing/ai-overage-standing-writes.d.ts +110 -0
  32. package/src/lib/billing/ai-overage-standing-writes.js +205 -0
  33. package/src/lib/billing/ai-overage-standing-writes.js.map +1 -0
  34. package/src/lib/billing/ai-overage-standing.d.ts +168 -0
  35. package/src/lib/billing/ai-overage-standing.js +215 -0
  36. package/src/lib/billing/ai-overage-standing.js.map +1 -0
  37. package/src/lib/billing/ai-overage-trigger.d.ts +30 -0
  38. package/src/lib/billing/ai-overage-trigger.js +76 -0
  39. package/src/lib/billing/ai-overage-trigger.js.map +1 -0
  40. package/src/lib/components/ai-allotment-editor.component.d.ts +40 -0
  41. package/src/lib/components/ai-allotment-editor.component.js +213 -0
  42. package/src/lib/components/ai-allotment-editor.component.js.map +1 -0
  43. package/src/lib/components/ai-assist-actions-context.d.ts +13 -0
  44. package/src/lib/components/ai-assist-actions-context.js +21 -0
  45. package/src/lib/components/ai-assist-actions-context.js.map +1 -0
  46. package/src/lib/components/ai-assist-provider.component.d.ts +51 -0
  47. package/src/lib/components/ai-assist-provider.component.js +519 -0
  48. package/src/lib/components/ai-assist-provider.component.js.map +1 -0
  49. package/src/lib/components/ai-brief-dialog.component.d.ts +56 -0
  50. package/src/lib/components/ai-brief-dialog.component.js +319 -0
  51. package/src/lib/components/ai-brief-dialog.component.js.map +1 -0
  52. package/src/lib/components/ai-collaborator-permissions-column.component.d.ts +32 -0
  53. package/src/lib/components/ai-collaborator-permissions-column.component.js +132 -0
  54. package/src/lib/components/ai-collaborator-permissions-column.component.js.map +1 -0
  55. package/src/lib/components/ai-credits-card.component.d.ts +34 -0
  56. package/src/lib/components/ai-credits-card.component.js +131 -0
  57. package/src/lib/components/ai-credits-card.component.js.map +1 -0
  58. package/src/lib/components/ai-credits-columns.component.d.ts +56 -0
  59. package/src/lib/components/ai-credits-columns.component.js +118 -0
  60. package/src/lib/components/ai-credits-columns.component.js.map +1 -0
  61. package/src/lib/components/ai-crm-email-draft.component.d.ts +17 -0
  62. package/src/lib/components/ai-crm-email-draft.component.js +159 -0
  63. package/src/lib/components/ai-crm-email-draft.component.js.map +1 -0
  64. package/src/lib/components/ai-crm-import-mapping.component.d.ts +20 -0
  65. package/src/lib/components/ai-crm-import-mapping.component.js +167 -0
  66. package/src/lib/components/ai-crm-import-mapping.component.js.map +1 -0
  67. package/src/lib/components/ai-crm-record-card.component.d.ts +27 -0
  68. package/src/lib/components/ai-crm-record-card.component.js +263 -0
  69. package/src/lib/components/ai-crm-record-card.component.js.map +1 -0
  70. package/src/lib/components/ai-describe-automation.component.d.ts +31 -0
  71. package/src/lib/components/ai-describe-automation.component.js +226 -0
  72. package/src/lib/components/ai-describe-automation.component.js.map +1 -0
  73. package/src/lib/components/ai-describe-button.component.d.ts +39 -0
  74. package/src/lib/components/ai-describe-button.component.js +87 -0
  75. package/src/lib/components/ai-describe-button.component.js.map +1 -0
  76. package/src/lib/components/ai-describe-page.component.d.ts +26 -0
  77. package/src/lib/components/ai-describe-page.component.js +18 -0
  78. package/src/lib/components/ai-describe-page.component.js.map +1 -0
  79. package/src/lib/components/ai-experiment-cards.component.d.ts +75 -0
  80. package/src/lib/components/ai-experiment-cards.component.js +573 -0
  81. package/src/lib/components/ai-experiment-cards.component.js.map +1 -0
  82. package/src/lib/components/ai-explain-automation.component.d.ts +31 -0
  83. package/src/lib/components/ai-explain-automation.component.js +265 -0
  84. package/src/lib/components/ai-explain-automation.component.js.map +1 -0
  85. package/src/lib/components/ai-insight-dialog.component.d.ts +37 -0
  86. package/src/lib/components/ai-insight-dialog.component.js +441 -0
  87. package/src/lib/components/ai-insight-dialog.component.js.map +1 -0
  88. package/src/lib/components/ai-job-plan.component.d.ts +46 -0
  89. package/src/lib/components/ai-job-plan.component.js +192 -0
  90. package/src/lib/components/ai-job-plan.component.js.map +1 -0
  91. package/src/lib/components/ai-model-selector.component.d.ts +21 -0
  92. package/src/lib/components/ai-model-selector.component.js +109 -0
  93. package/src/lib/components/ai-model-selector.component.js.map +1 -0
  94. package/src/lib/components/ai-page-brief-dialog.component.d.ts +29 -0
  95. package/src/lib/components/ai-page-brief-dialog.component.js +27 -0
  96. package/src/lib/components/ai-page-brief-dialog.component.js.map +1 -0
  97. package/src/lib/components/ai-permissions-on-host.component.d.ts +55 -0
  98. package/src/lib/components/ai-permissions-on-host.component.js +91 -0
  99. package/src/lib/components/ai-permissions-on-host.component.js.map +1 -0
  100. package/src/lib/components/ai-product-copy-card.component.d.ts +25 -0
  101. package/src/lib/components/ai-product-copy-card.component.js +328 -0
  102. package/src/lib/components/ai-product-copy-card.component.js.map +1 -0
  103. package/src/lib/components/ai-product-import-option.component.d.ts +14 -0
  104. package/src/lib/components/ai-product-import-option.component.js +58 -0
  105. package/src/lib/components/ai-product-import-option.component.js.map +1 -0
  106. package/src/lib/components/ai-products-hub-card.component.d.ts +15 -0
  107. package/src/lib/components/ai-products-hub-card.component.js +418 -0
  108. package/src/lib/components/ai-products-hub-card.component.js.map +1 -0
  109. package/src/lib/components/ai-products-proposals.component.d.ts +19 -0
  110. package/src/lib/components/ai-products-proposals.component.js +547 -0
  111. package/src/lib/components/ai-products-proposals.component.js.map +1 -0
  112. package/src/lib/components/ai-save-as-component.component.d.ts +38 -0
  113. package/src/lib/components/ai-save-as-component.component.js +231 -0
  114. package/src/lib/components/ai-save-as-component.component.js.map +1 -0
  115. package/src/lib/components/ai-seo-audit-card.component.d.ts +9 -0
  116. package/src/lib/components/ai-seo-audit-card.component.js +620 -0
  117. package/src/lib/components/ai-seo-audit-card.component.js.map +1 -0
  118. package/src/lib/components/ai-seo-fields-card.component.d.ts +10 -0
  119. package/src/lib/components/ai-seo-fields-card.component.js +453 -0
  120. package/src/lib/components/ai-seo-fields-card.component.js.map +1 -0
  121. package/src/lib/components/ai-site-batch-card.component.d.ts +32 -0
  122. package/src/lib/components/ai-site-batch-card.component.js +531 -0
  123. package/src/lib/components/ai-site-batch-card.component.js.map +1 -0
  124. package/src/lib/components/ai-site-seo-start-card.component.d.ts +26 -0
  125. package/src/lib/components/ai-site-seo-start-card.component.js +178 -0
  126. package/src/lib/components/ai-site-seo-start-card.component.js.map +1 -0
  127. package/src/lib/components/ai-site-start-card.component.d.ts +19 -0
  128. package/src/lib/components/ai-site-start-card.component.js +348 -0
  129. package/src/lib/components/ai-site-start-card.component.js.map +1 -0
  130. package/src/lib/components/ai-template-collection-field.component.d.ts +32 -0
  131. package/src/lib/components/ai-template-collection-field.component.js +63 -0
  132. package/src/lib/components/ai-template-collection-field.component.js.map +1 -0
  133. package/src/lib/components/ai-theme-proposal-card.component.d.ts +6 -0
  134. package/src/lib/components/ai-theme-proposal-card.component.js +619 -0
  135. package/src/lib/components/ai-theme-proposal-card.component.js.map +1 -0
  136. package/src/lib/components/ai-usage-strip.component.d.ts +27 -0
  137. package/src/lib/components/ai-usage-strip.component.js +145 -0
  138. package/src/lib/components/ai-usage-strip.component.js.map +1 -0
  139. package/src/lib/components/assist-edit-canvas.d.ts +169 -0
  140. package/src/lib/components/assist-edit-canvas.js +636 -0
  141. package/src/lib/components/assist-edit-canvas.js.map +1 -0
  142. package/src/lib/components/assist-edit-card.component.d.ts +16 -0
  143. package/src/lib/components/assist-edit-card.component.js +181 -0
  144. package/src/lib/components/assist-edit-card.component.js.map +1 -0
  145. package/src/lib/components/assist-jobs-drawer.component.d.ts +72 -0
  146. package/src/lib/components/assist-jobs-drawer.component.js +626 -0
  147. package/src/lib/components/assist-jobs-drawer.component.js.map +1 -0
  148. package/src/lib/components/assist-panel.component.d.ts +136 -0
  149. package/src/lib/components/assist-panel.component.js +1099 -0
  150. package/src/lib/components/assist-panel.component.js.map +1 -0
  151. package/src/lib/components/assist-signals-page.component.d.ts +3 -0
  152. package/src/lib/components/assist-signals-page.component.js +1017 -0
  153. package/src/lib/components/assist-signals-page.component.js.map +1 -0
  154. package/src/lib/components/besigner-ai-controls.component.d.ts +29 -0
  155. package/src/lib/components/besigner-ai-controls.component.js +81 -0
  156. package/src/lib/components/besigner-ai-controls.component.js.map +1 -0
  157. package/src/lib/components/billing-ai-allotments.component.d.ts +36 -0
  158. package/src/lib/components/billing-ai-allotments.component.js +486 -0
  159. package/src/lib/components/billing-ai-allotments.component.js.map +1 -0
  160. package/src/lib/components/billing-ai-top-users.component.d.ts +44 -0
  161. package/src/lib/components/billing-ai-top-users.component.js +304 -0
  162. package/src/lib/components/billing-ai-top-users.component.js.map +1 -0
  163. package/src/lib/components/billing-assist-overage-card.component.d.ts +69 -0
  164. package/src/lib/components/billing-assist-overage-card.component.js +437 -0
  165. package/src/lib/components/billing-assist-overage-card.component.js.map +1 -0
  166. package/src/lib/components/host-ai-allotments.component.d.ts +40 -0
  167. package/src/lib/components/host-ai-allotments.component.js +239 -0
  168. package/src/lib/components/host-ai-allotments.component.js.map +1 -0
  169. package/src/lib/components/member-ai-allotment-card.component.d.ts +28 -0
  170. package/src/lib/components/member-ai-allotment-card.component.js +172 -0
  171. package/src/lib/components/member-ai-allotment-card.component.js.map +1 -0
  172. package/src/lib/components/member-ai-usage-card.component.d.ts +28 -0
  173. package/src/lib/components/member-ai-usage-card.component.js +240 -0
  174. package/src/lib/components/member-ai-usage-card.component.js.map +1 -0
  175. package/src/lib/components/staff-org-ai-card.component.d.ts +17 -0
  176. package/src/lib/components/staff-org-ai-card.component.js +739 -0
  177. package/src/lib/components/staff-org-ai-card.component.js.map +1 -0
  178. package/src/lib/components/staff-org-usage-ai-columns.component.d.ts +41 -0
  179. package/src/lib/components/staff-org-usage-ai-columns.component.js +65 -0
  180. package/src/lib/components/staff-org-usage-ai-columns.component.js.map +1 -0
  181. package/src/lib/components/staff-orgs-ai-spend-column.component.d.ts +28 -0
  182. package/src/lib/components/staff-orgs-ai-spend-column.component.js +89 -0
  183. package/src/lib/components/staff-orgs-ai-spend-column.component.js.map +1 -0
  184. package/src/lib/components/staff-user-ai-usage-card.component.d.ts +27 -0
  185. package/src/lib/components/staff-user-ai-usage-card.component.js +183 -0
  186. package/src/lib/components/staff-user-ai-usage-card.component.js.map +1 -0
  187. package/src/lib/components/use-ai-allotments.d.ts +49 -0
  188. package/src/lib/components/use-ai-allotments.js +171 -0
  189. package/src/lib/components/use-ai-allotments.js.map +1 -0
  190. package/src/lib/components/use-ai-crm-answer.d.ts +50 -0
  191. package/src/lib/components/use-ai-crm-answer.js +220 -0
  192. package/src/lib/components/use-ai-crm-answer.js.map +1 -0
  193. package/src/lib/components/use-ai-job-run.d.ts +51 -0
  194. package/src/lib/components/use-ai-job-run.js +176 -0
  195. package/src/lib/components/use-ai-job-run.js.map +1 -0
  196. package/src/lib/components/use-ai-model-choice.d.ts +47 -0
  197. package/src/lib/components/use-ai-model-choice.js +150 -0
  198. package/src/lib/components/use-ai-model-choice.js.map +1 -0
  199. package/src/lib/components/use-ai-products-jobs.d.ts +36 -0
  200. package/src/lib/components/use-ai-products-jobs.js +190 -0
  201. package/src/lib/components/use-ai-products-jobs.js.map +1 -0
  202. package/src/lib/components/use-ai-usage-meter.d.ts +11 -0
  203. package/src/lib/components/use-ai-usage-meter.js +96 -0
  204. package/src/lib/components/use-ai-usage-meter.js.map +1 -0
  205. package/src/lib/components/use-org-ai-usage.d.ts +42 -0
  206. package/src/lib/components/use-org-ai-usage.js +181 -0
  207. package/src/lib/components/use-org-ai-usage.js.map +1 -0
  208. package/src/lib/components/use-staff-orgs-ai-spend.d.ts +18 -0
  209. package/src/lib/components/use-staff-orgs-ai-spend.js +127 -0
  210. package/src/lib/components/use-staff-orgs-ai-spend.js.map +1 -0
  211. package/src/lib/constants.d.ts +24 -0
  212. package/src/lib/constants.js +23 -0
  213. package/src/lib/constants.js.map +1 -0
  214. package/src/lib/declarations.d.ts +48 -0
  215. package/src/lib/declarations.js +129 -0
  216. package/src/lib/declarations.js.map +1 -0
  217. package/src/lib/declarations.server.d.ts +35 -0
  218. package/src/lib/declarations.server.js +138 -0
  219. package/src/lib/declarations.server.js.map +1 -0
  220. package/src/lib/insights/ai-figure-readers.d.ts +90 -0
  221. package/src/lib/insights/ai-figure-readers.js +863 -0
  222. package/src/lib/insights/ai-figure-readers.js.map +1 -0
  223. package/src/lib/insights/ai-insight-digest.d.ts +165 -0
  224. package/src/lib/insights/ai-insight-digest.js +363 -0
  225. package/src/lib/insights/ai-insight-digest.js.map +1 -0
  226. package/src/lib/insights/ai-insight-readers.d.ts +71 -0
  227. package/src/lib/insights/ai-insight-readers.js +141 -0
  228. package/src/lib/insights/ai-insight-readers.js.map +1 -0
  229. package/src/lib/jobs/ai-crm-access.d.ts +61 -0
  230. package/src/lib/jobs/ai-crm-access.js +94 -0
  231. package/src/lib/jobs/ai-crm-access.js.map +1 -0
  232. package/src/lib/jobs/ai-email-bindings.d.ts +97 -0
  233. package/src/lib/jobs/ai-email-bindings.js +147 -0
  234. package/src/lib/jobs/ai-email-bindings.js.map +1 -0
  235. package/src/lib/jobs/ai-job-admission.d.ts +82 -0
  236. package/src/lib/jobs/ai-job-admission.js +45 -0
  237. package/src/lib/jobs/ai-job-admission.js.map +1 -0
  238. package/src/lib/jobs/ai-job-budget.d.ts +167 -0
  239. package/src/lib/jobs/ai-job-budget.js +161 -0
  240. package/src/lib/jobs/ai-job-budget.js.map +1 -0
  241. package/src/lib/jobs/ai-job-campaign-step.d.ts +108 -0
  242. package/src/lib/jobs/ai-job-campaign-step.js +340 -0
  243. package/src/lib/jobs/ai-job-campaign-step.js.map +1 -0
  244. package/src/lib/jobs/ai-job-component-checks.d.ts +74 -0
  245. package/src/lib/jobs/ai-job-component-checks.js +442 -0
  246. package/src/lib/jobs/ai-job-component-checks.js.map +1 -0
  247. package/src/lib/jobs/ai-job-component-step.d.ts +75 -0
  248. package/src/lib/jobs/ai-job-component-step.js +262 -0
  249. package/src/lib/jobs/ai-job-component-step.js.map +1 -0
  250. package/src/lib/jobs/ai-job-crm-step.d.ts +192 -0
  251. package/src/lib/jobs/ai-job-crm-step.js +734 -0
  252. package/src/lib/jobs/ai-job-crm-step.js.map +1 -0
  253. package/src/lib/jobs/ai-job-draft-ids.d.ts +62 -0
  254. package/src/lib/jobs/ai-job-draft-ids.js +132 -0
  255. package/src/lib/jobs/ai-job-draft-ids.js.map +1 -0
  256. package/src/lib/jobs/ai-job-drafts.d.ts +304 -0
  257. package/src/lib/jobs/ai-job-drafts.js +557 -0
  258. package/src/lib/jobs/ai-job-drafts.js.map +1 -0
  259. package/src/lib/jobs/ai-job-email-step.d.ts +195 -0
  260. package/src/lib/jobs/ai-job-email-step.js +547 -0
  261. package/src/lib/jobs/ai-job-email-step.js.map +1 -0
  262. package/src/lib/jobs/ai-job-experiment-step.d.ts +105 -0
  263. package/src/lib/jobs/ai-job-experiment-step.js +445 -0
  264. package/src/lib/jobs/ai-job-experiment-step.js.map +1 -0
  265. package/src/lib/jobs/ai-job-form-step.d.ts +250 -0
  266. package/src/lib/jobs/ai-job-form-step.js +634 -0
  267. package/src/lib/jobs/ai-job-form-step.js.map +1 -0
  268. package/src/lib/jobs/ai-job-generation.d.ts +114 -0
  269. package/src/lib/jobs/ai-job-generation.js +384 -0
  270. package/src/lib/jobs/ai-job-generation.js.map +1 -0
  271. package/src/lib/jobs/ai-job-insight-budget.d.ts +41 -0
  272. package/src/lib/jobs/ai-job-insight-budget.js +46 -0
  273. package/src/lib/jobs/ai-job-insight-budget.js.map +1 -0
  274. package/src/lib/jobs/ai-job-insight-step.d.ts +58 -0
  275. package/src/lib/jobs/ai-job-insight-step.js +350 -0
  276. package/src/lib/jobs/ai-job-insight-step.js.map +1 -0
  277. package/src/lib/jobs/ai-job-layout-step.d.ts +103 -0
  278. package/src/lib/jobs/ai-job-layout-step.js +340 -0
  279. package/src/lib/jobs/ai-job-layout-step.js.map +1 -0
  280. package/src/lib/jobs/ai-job-page-budget.d.ts +57 -0
  281. package/src/lib/jobs/ai-job-page-budget.js +60 -0
  282. package/src/lib/jobs/ai-job-page-budget.js.map +1 -0
  283. package/src/lib/jobs/ai-job-page-sections.d.ts +150 -0
  284. package/src/lib/jobs/ai-job-page-sections.js +450 -0
  285. package/src/lib/jobs/ai-job-page-sections.js.map +1 -0
  286. package/src/lib/jobs/ai-job-page-step.d.ts +198 -0
  287. package/src/lib/jobs/ai-job-page-step.js +667 -0
  288. package/src/lib/jobs/ai-job-page-step.js.map +1 -0
  289. package/src/lib/jobs/ai-job-plan-conformance.d.ts +107 -0
  290. package/src/lib/jobs/ai-job-plan-conformance.js +164 -0
  291. package/src/lib/jobs/ai-job-plan-conformance.js.map +1 -0
  292. package/src/lib/jobs/ai-job-plan-step.d.ts +196 -0
  293. package/src/lib/jobs/ai-job-plan-step.js +458 -0
  294. package/src/lib/jobs/ai-job-plan-step.js.map +1 -0
  295. package/src/lib/jobs/ai-job-plugin-drafts.d.ts +76 -0
  296. package/src/lib/jobs/ai-job-plugin-drafts.js +91 -0
  297. package/src/lib/jobs/ai-job-plugin-drafts.js.map +1 -0
  298. package/src/lib/jobs/ai-job-products-step.d.ts +106 -0
  299. package/src/lib/jobs/ai-job-products-step.js +436 -0
  300. package/src/lib/jobs/ai-job-products-step.js.map +1 -0
  301. package/src/lib/jobs/ai-job-seo-budget.d.ts +52 -0
  302. package/src/lib/jobs/ai-job-seo-budget.js +63 -0
  303. package/src/lib/jobs/ai-job-seo-budget.js.map +1 -0
  304. package/src/lib/jobs/ai-job-seo-step.d.ts +145 -0
  305. package/src/lib/jobs/ai-job-seo-step.js +809 -0
  306. package/src/lib/jobs/ai-job-seo-step.js.map +1 -0
  307. package/src/lib/jobs/ai-job-site-step.d.ts +252 -0
  308. package/src/lib/jobs/ai-job-site-step.js +538 -0
  309. package/src/lib/jobs/ai-job-site-step.js.map +1 -0
  310. package/src/lib/jobs/ai-job-template-step.d.ts +105 -0
  311. package/src/lib/jobs/ai-job-template-step.js +391 -0
  312. package/src/lib/jobs/ai-job-template-step.js.map +1 -0
  313. package/src/lib/jobs/ai-job-text-step.d.ts +125 -0
  314. package/src/lib/jobs/ai-job-text-step.js +128 -0
  315. package/src/lib/jobs/ai-job-text-step.js.map +1 -0
  316. package/src/lib/jobs/ai-job-theme-budget.d.ts +38 -0
  317. package/src/lib/jobs/ai-job-theme-budget.js +43 -0
  318. package/src/lib/jobs/ai-job-theme-budget.js.map +1 -0
  319. package/src/lib/jobs/ai-job-theme-step.d.ts +115 -0
  320. package/src/lib/jobs/ai-job-theme-step.js +337 -0
  321. package/src/lib/jobs/ai-job-theme-step.js.map +1 -0
  322. package/src/lib/jobs/ai-job-workflow-step.d.ts +139 -0
  323. package/src/lib/jobs/ai-job-workflow-step.js +530 -0
  324. package/src/lib/jobs/ai-job-workflow-step.js.map +1 -0
  325. package/src/lib/jobs/ai-jobs-beat.d.ts +78 -0
  326. package/src/lib/jobs/ai-jobs-beat.js +93 -0
  327. package/src/lib/jobs/ai-jobs-beat.js.map +1 -0
  328. package/src/lib/jobs/ai-jobs-pause.d.ts +38 -0
  329. package/src/lib/jobs/ai-jobs-pause.js +45 -0
  330. package/src/lib/jobs/ai-jobs-pause.js.map +1 -0
  331. package/src/lib/jobs/ai-jobs.d.ts +558 -0
  332. package/src/lib/jobs/ai-jobs.js +1410 -0
  333. package/src/lib/jobs/ai-jobs.js.map +1 -0
  334. package/src/lib/jobs/ai-media-asset-server.d.ts +34 -0
  335. package/src/lib/jobs/ai-media-asset-server.js +34 -0
  336. package/src/lib/jobs/ai-media-asset-server.js.map +1 -0
  337. package/src/lib/jobs/ai-media-asset.d.ts +59 -0
  338. package/src/lib/jobs/ai-media-asset.js +86 -0
  339. package/src/lib/jobs/ai-media-asset.js.map +1 -0
  340. package/src/lib/jobs/ai-product-image.d.ts +78 -0
  341. package/src/lib/jobs/ai-product-image.js +113 -0
  342. package/src/lib/jobs/ai-product-image.js.map +1 -0
  343. package/src/lib/jobs/ai-theme-brand-inputs.d.ts +125 -0
  344. package/src/lib/jobs/ai-theme-brand-inputs.js +378 -0
  345. package/src/lib/jobs/ai-theme-brand-inputs.js.map +1 -0
  346. package/src/lib/jobs/ai-theme-brand-server.d.ts +33 -0
  347. package/src/lib/jobs/ai-theme-brand-server.js +33 -0
  348. package/src/lib/jobs/ai-theme-brand-server.js.map +1 -0
  349. package/src/lib/jobs/ai-workflow-records.d.ts +84 -0
  350. package/src/lib/jobs/ai-workflow-records.js +158 -0
  351. package/src/lib/jobs/ai-workflow-records.js.map +1 -0
  352. package/src/lib/jobs/fixtures/ai-free-page-built-recording.d.ts +31 -0
  353. package/src/lib/jobs/fixtures/ai-free-page-built-recording.js +367 -0
  354. package/src/lib/jobs/fixtures/ai-free-page-built-recording.js.map +1 -0
  355. package/src/lib/jobs/fixtures/ai-free-page-recording.d.ts +69 -0
  356. package/src/lib/jobs/fixtures/ai-free-page-recording.js +442 -0
  357. package/src/lib/jobs/fixtures/ai-free-page-recording.js.map +1 -0
  358. package/src/lib/jobs/fixtures/ai-page-briefs.d.ts +146 -0
  359. package/src/lib/jobs/fixtures/ai-page-briefs.js +1850 -0
  360. package/src/lib/jobs/fixtures/ai-page-briefs.js.map +1 -0
  361. package/src/lib/model/ai-allotments.d.ts +208 -0
  362. package/src/lib/model/ai-allotments.js +269 -0
  363. package/src/lib/model/ai-allotments.js.map +1 -0
  364. package/src/lib/model/ai-automation-draft.d.ts +74 -0
  365. package/src/lib/model/ai-automation-draft.js +310 -0
  366. package/src/lib/model/ai-automation-draft.js.map +1 -0
  367. package/src/lib/model/ai-automation-outline.d.ts +50 -0
  368. package/src/lib/model/ai-automation-outline.js +262 -0
  369. package/src/lib/model/ai-automation-outline.js.map +1 -0
  370. package/src/lib/model/ai-build-plan.d.ts +197 -0
  371. package/src/lib/model/ai-build-plan.js +438 -0
  372. package/src/lib/model/ai-build-plan.js.map +1 -0
  373. package/src/lib/model/ai-crm.d.ts +237 -0
  374. package/src/lib/model/ai-crm.js +225 -0
  375. package/src/lib/model/ai-crm.js.map +1 -0
  376. package/src/lib/model/ai-experiment-proposal.d.ts +68 -0
  377. package/src/lib/model/ai-experiment-proposal.js +146 -0
  378. package/src/lib/model/ai-experiment-proposal.js.map +1 -0
  379. package/src/lib/model/ai-experiment.d.ts +309 -0
  380. package/src/lib/model/ai-experiment.js +395 -0
  381. package/src/lib/model/ai-experiment.js.map +1 -0
  382. package/src/lib/model/ai-insight.d.ts +178 -0
  383. package/src/lib/model/ai-insight.js +201 -0
  384. package/src/lib/model/ai-insight.js.map +1 -0
  385. package/src/lib/model/ai-jobs.types.d.ts +412 -0
  386. package/src/lib/model/ai-jobs.types.js +42 -0
  387. package/src/lib/model/ai-jobs.types.js.map +1 -0
  388. package/src/lib/model/ai-page-job.d.ts +92 -0
  389. package/src/lib/model/ai-page-job.js +159 -0
  390. package/src/lib/model/ai-page-job.js.map +1 -0
  391. package/src/lib/model/ai-permissions.d.ts +49 -0
  392. package/src/lib/model/ai-permissions.js +97 -0
  393. package/src/lib/model/ai-permissions.js.map +1 -0
  394. package/src/lib/model/ai-plan-capabilities.d.ts +139 -0
  395. package/src/lib/model/ai-plan-capabilities.js +149 -0
  396. package/src/lib/model/ai-plan-capabilities.js.map +1 -0
  397. package/src/lib/model/ai-products.d.ts +251 -0
  398. package/src/lib/model/ai-products.js +253 -0
  399. package/src/lib/model/ai-products.js.map +1 -0
  400. package/src/lib/model/ai-seo.d.ts +208 -0
  401. package/src/lib/model/ai-seo.js +150 -0
  402. package/src/lib/model/ai-seo.js.map +1 -0
  403. package/src/lib/model/ai-site-inventory.d.ts +164 -0
  404. package/src/lib/model/ai-site-inventory.js +145 -0
  405. package/src/lib/model/ai-site-inventory.js.map +1 -0
  406. package/src/lib/model/ai-site-job.d.ts +247 -0
  407. package/src/lib/model/ai-site-job.js +314 -0
  408. package/src/lib/model/ai-site-job.js.map +1 -0
  409. package/src/lib/model/ai-site-start-seo.d.ts +103 -0
  410. package/src/lib/model/ai-site-start-seo.js +154 -0
  411. package/src/lib/model/ai-site-start-seo.js.map +1 -0
  412. package/src/lib/model/ai-site-start.d.ts +122 -0
  413. package/src/lib/model/ai-site-start.js +159 -0
  414. package/src/lib/model/ai-site-start.js.map +1 -0
  415. package/src/lib/model/ai-site-switch.d.ts +56 -0
  416. package/src/lib/model/ai-site-switch.js +73 -0
  417. package/src/lib/model/ai-site-switch.js.map +1 -0
  418. package/src/lib/model/ai-storefront-claims.d.ts +70 -0
  419. package/src/lib/model/ai-storefront-claims.js +130 -0
  420. package/src/lib/model/ai-storefront-claims.js.map +1 -0
  421. package/src/lib/model/ai-template-subjects.d.ts +82 -0
  422. package/src/lib/model/ai-template-subjects.js +136 -0
  423. package/src/lib/model/ai-template-subjects.js.map +1 -0
  424. package/src/lib/model/ai-theme-proposal.d.ts +210 -0
  425. package/src/lib/model/ai-theme-proposal.js +506 -0
  426. package/src/lib/model/ai-theme-proposal.js.map +1 -0
  427. package/src/lib/model/ai-tokens.d.ts +96 -0
  428. package/src/lib/model/ai-tokens.js +117 -0
  429. package/src/lib/model/ai-tokens.js.map +1 -0
  430. package/src/lib/model/ai-usage-by-user.d.ts +120 -0
  431. package/src/lib/model/ai-usage-by-user.js +139 -0
  432. package/src/lib/model/ai-usage-by-user.js.map +1 -0
  433. package/src/lib/model/ai-workflow-job.d.ts +111 -0
  434. package/src/lib/model/ai-workflow-job.js +183 -0
  435. package/src/lib/model/ai-workflow-job.js.map +1 -0
  436. package/src/lib/model/assist-edit.d.ts +261 -0
  437. package/src/lib/model/assist-edit.js +175 -0
  438. package/src/lib/model/assist-edit.js.map +1 -0
  439. package/src/lib/plugin-config.d.ts +33 -0
  440. package/src/lib/plugin-config.js +118 -0
  441. package/src/lib/plugin-config.js.map +1 -0
  442. package/src/lib/plugin.d.ts +30 -0
  443. package/src/lib/plugin.js +525 -0
  444. package/src/lib/plugin.js.map +1 -0
  445. package/src/lib/providers/anthropic.d.ts +84 -0
  446. package/src/lib/providers/anthropic.js +432 -0
  447. package/src/lib/providers/anthropic.js.map +1 -0
  448. package/src/lib/providers/catalog.d.ts +197 -0
  449. package/src/lib/providers/catalog.js +312 -0
  450. package/src/lib/providers/catalog.js.map +1 -0
  451. package/src/lib/providers/contract.d.ts +464 -0
  452. package/src/lib/providers/contract.js +274 -0
  453. package/src/lib/providers/contract.js.map +1 -0
  454. package/src/lib/providers/model-choice.d.ts +119 -0
  455. package/src/lib/providers/model-choice.js +416 -0
  456. package/src/lib/providers/model-choice.js.map +1 -0
  457. package/src/lib/providers/openai-compatible.d.ts +58 -0
  458. package/src/lib/providers/openai-compatible.js +361 -0
  459. package/src/lib/providers/openai-compatible.js.map +1 -0
  460. package/src/lib/providers/registry.d.ts +49 -0
  461. package/src/lib/providers/registry.js +71 -0
  462. package/src/lib/providers/registry.js.map +1 -0
  463. package/src/lib/providers/routing.d.ts +98 -0
  464. package/src/lib/providers/routing.js +450 -0
  465. package/src/lib/providers/routing.js.map +1 -0
  466. package/src/lib/runtime/ai-abuse-guards.d.ts +113 -0
  467. package/src/lib/runtime/ai-abuse-guards.js +148 -0
  468. package/src/lib/runtime/ai-abuse-guards.js.map +1 -0
  469. package/src/lib/runtime/ai-component-bindings.d.ts +67 -0
  470. package/src/lib/runtime/ai-component-bindings.js +106 -0
  471. package/src/lib/runtime/ai-component-bindings.js.map +1 -0
  472. package/src/lib/runtime/ai-device-audit.d.ts +109 -0
  473. package/src/lib/runtime/ai-device-audit.js +97 -0
  474. package/src/lib/runtime/ai-device-audit.js.map +1 -0
  475. package/src/lib/runtime/ai-doctrine-validators.d.ts +720 -0
  476. package/src/lib/runtime/ai-doctrine-validators.js +3133 -0
  477. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -0
  478. package/src/lib/runtime/ai-doctrine.d.ts +372 -0
  479. package/src/lib/runtime/ai-doctrine.js +944 -0
  480. package/src/lib/runtime/ai-doctrine.js.map +1 -0
  481. package/src/lib/runtime/ai-eval-live.d.ts +321 -0
  482. package/src/lib/runtime/ai-eval-live.js +1257 -0
  483. package/src/lib/runtime/ai-eval-live.js.map +1 -0
  484. package/src/lib/runtime/ai-eval-memory-firestore.d.ts +51 -0
  485. package/src/lib/runtime/ai-eval-memory-firestore.js +183 -0
  486. package/src/lib/runtime/ai-eval-memory-firestore.js.map +1 -0
  487. package/src/lib/runtime/ai-eval.d.ts +441 -0
  488. package/src/lib/runtime/ai-eval.js +1151 -0
  489. package/src/lib/runtime/ai-eval.js.map +1 -0
  490. package/src/lib/runtime/ai-gate.d.ts +130 -0
  491. package/src/lib/runtime/ai-gate.js +272 -0
  492. package/src/lib/runtime/ai-gate.js.map +1 -0
  493. package/src/lib/runtime/ai-generation-bounds.d.ts +24 -0
  494. package/src/lib/runtime/ai-generation-bounds.js +23 -0
  495. package/src/lib/runtime/ai-generation-bounds.js.map +1 -0
  496. package/src/lib/runtime/ai-insight-check.d.ts +76 -0
  497. package/src/lib/runtime/ai-insight-check.js +281 -0
  498. package/src/lib/runtime/ai-insight-check.js.map +1 -0
  499. package/src/lib/runtime/ai-node-tree.d.ts +181 -0
  500. package/src/lib/runtime/ai-node-tree.js +931 -0
  501. package/src/lib/runtime/ai-node-tree.js.map +1 -0
  502. package/src/lib/runtime/ai-page-links.d.ts +79 -0
  503. package/src/lib/runtime/ai-page-links.js +135 -0
  504. package/src/lib/runtime/ai-page-links.js.map +1 -0
  505. package/src/lib/runtime/ai-palette.d.ts +252 -0
  506. package/src/lib/runtime/ai-palette.generated.d.ts +34 -0
  507. package/src/lib/runtime/ai-palette.generated.js +5263 -0
  508. package/src/lib/runtime/ai-palette.generated.js.map +1 -0
  509. package/src/lib/runtime/ai-palette.js +187 -0
  510. package/src/lib/runtime/ai-palette.js.map +1 -0
  511. package/src/lib/runtime/ai-products-generation.d.ts +107 -0
  512. package/src/lib/runtime/ai-products-generation.js +250 -0
  513. package/src/lib/runtime/ai-products-generation.js.map +1 -0
  514. package/src/lib/runtime/ai-repeated-items.d.ts +95 -0
  515. package/src/lib/runtime/ai-repeated-items.js +397 -0
  516. package/src/lib/runtime/ai-repeated-items.js.map +1 -0
  517. package/src/lib/runtime/ai-runtime.d.ts +148 -0
  518. package/src/lib/runtime/ai-runtime.js +237 -0
  519. package/src/lib/runtime/ai-runtime.js.map +1 -0
  520. package/src/lib/runtime/ai-template-examples.d.ts +71 -0
  521. package/src/lib/runtime/ai-template-examples.js +135 -0
  522. package/src/lib/runtime/ai-template-examples.js.map +1 -0
  523. package/src/lib/runtime/assist-answer-cache.d.ts +80 -0
  524. package/src/lib/runtime/assist-answer-cache.js +184 -0
  525. package/src/lib/runtime/assist-answer-cache.js.map +1 -0
  526. package/src/lib/runtime/seo-audit.d.ts +93 -0
  527. package/src/lib/runtime/seo-audit.js +260 -0
  528. package/src/lib/runtime/seo-audit.js.map +1 -0
  529. package/src/lib/runtime/seo-content-fixes.d.ts +41 -0
  530. package/src/lib/runtime/seo-content-fixes.js +168 -0
  531. package/src/lib/runtime/seo-content-fixes.js.map +1 -0
  532. package/src/lib/runtime/seo-fields.d.ts +152 -0
  533. package/src/lib/runtime/seo-fields.js +215 -0
  534. package/src/lib/runtime/seo-fields.js.map +1 -0
  535. package/src/lib/runtime/seo-page-facts.d.ts +61 -0
  536. package/src/lib/runtime/seo-page-facts.js +150 -0
  537. package/src/lib/runtime/seo-page-facts.js.map +1 -0
  538. package/src/lib/runtime/site-inventory.d.ts +63 -0
  539. package/src/lib/runtime/site-inventory.js +230 -0
  540. package/src/lib/runtime/site-inventory.js.map +1 -0
  541. package/src/lib/server/ai-admin-org.d.ts +33 -0
  542. package/src/lib/server/ai-admin-org.js +381 -0
  543. package/src/lib/server/ai-admin-org.js.map +1 -0
  544. package/src/lib/server/ai-admin-orgs-spend.d.ts +40 -0
  545. package/src/lib/server/ai-admin-orgs-spend.js +129 -0
  546. package/src/lib/server/ai-admin-orgs-spend.js.map +1 -0
  547. package/src/lib/server/ai-admin-overage.d.ts +19 -0
  548. package/src/lib/server/ai-admin-overage.js +246 -0
  549. package/src/lib/server/ai-admin-overage.js.map +1 -0
  550. package/src/lib/server/ai-admin-signals.d.ts +34 -0
  551. package/src/lib/server/ai-admin-signals.js +330 -0
  552. package/src/lib/server/ai-admin-signals.js.map +1 -0
  553. package/src/lib/server/ai-admin-user.d.ts +27 -0
  554. package/src/lib/server/ai-admin-user.js +128 -0
  555. package/src/lib/server/ai-admin-user.js.map +1 -0
  556. package/src/lib/server/ai-allotments.d.ts +27 -0
  557. package/src/lib/server/ai-allotments.js +544 -0
  558. package/src/lib/server/ai-allotments.js.map +1 -0
  559. package/src/lib/server/ai-assist-prompts.d.ts +102 -0
  560. package/src/lib/server/ai-assist-prompts.js +207 -0
  561. package/src/lib/server/ai-assist-prompts.js.map +1 -0
  562. package/src/lib/server/ai-assist.d.ts +119 -0
  563. package/src/lib/server/ai-assist.js +517 -0
  564. package/src/lib/server/ai-assist.js.map +1 -0
  565. package/src/lib/server/ai-crm-answer.d.ts +30 -0
  566. package/src/lib/server/ai-crm-answer.js +98 -0
  567. package/src/lib/server/ai-crm-answer.js.map +1 -0
  568. package/src/lib/server/ai-generate-component.d.ts +111 -0
  569. package/src/lib/server/ai-generate-component.js +643 -0
  570. package/src/lib/server/ai-generate-component.js.map +1 -0
  571. package/src/lib/server/ai-host-permissions.d.ts +46 -0
  572. package/src/lib/server/ai-host-permissions.js +204 -0
  573. package/src/lib/server/ai-host-permissions.js.map +1 -0
  574. package/src/lib/server/ai-insight-answer.d.ts +30 -0
  575. package/src/lib/server/ai-insight-answer.js +83 -0
  576. package/src/lib/server/ai-insight-answer.js.map +1 -0
  577. package/src/lib/server/ai-insight-digest-route.d.ts +30 -0
  578. package/src/lib/server/ai-insight-digest-route.js +156 -0
  579. package/src/lib/server/ai-insight-digest-route.js.map +1 -0
  580. package/src/lib/server/ai-jobs-batch.d.ts +48 -0
  581. package/src/lib/server/ai-jobs-batch.js +306 -0
  582. package/src/lib/server/ai-jobs-batch.js.map +1 -0
  583. package/src/lib/server/ai-jobs-beat-route.d.ts +36 -0
  584. package/src/lib/server/ai-jobs-beat-route.js +86 -0
  585. package/src/lib/server/ai-jobs-beat-route.js.map +1 -0
  586. package/src/lib/server/ai-jobs-cancel.d.ts +30 -0
  587. package/src/lib/server/ai-jobs-cancel.js +74 -0
  588. package/src/lib/server/ai-jobs-cancel.js.map +1 -0
  589. package/src/lib/server/ai-jobs-events-route.d.ts +31 -0
  590. package/src/lib/server/ai-jobs-events-route.js +51 -0
  591. package/src/lib/server/ai-jobs-events-route.js.map +1 -0
  592. package/src/lib/server/ai-jobs-events.d.ts +55 -0
  593. package/src/lib/server/ai-jobs-events.js +144 -0
  594. package/src/lib/server/ai-jobs-events.js.map +1 -0
  595. package/src/lib/server/ai-jobs-gate.d.ts +47 -0
  596. package/src/lib/server/ai-jobs-gate.js +106 -0
  597. package/src/lib/server/ai-jobs-gate.js.map +1 -0
  598. package/src/lib/server/ai-jobs-resume.d.ts +23 -0
  599. package/src/lib/server/ai-jobs-resume.js +209 -0
  600. package/src/lib/server/ai-jobs-resume.js.map +1 -0
  601. package/src/lib/server/ai-jobs-route.d.ts +32 -0
  602. package/src/lib/server/ai-jobs-route.js +307 -0
  603. package/src/lib/server/ai-jobs-route.js.map +1 -0
  604. package/src/lib/server/ai-models.d.ts +18 -0
  605. package/src/lib/server/ai-models.js +134 -0
  606. package/src/lib/server/ai-models.js.map +1 -0
  607. package/src/lib/server/ai-seo-apply.d.ts +53 -0
  608. package/src/lib/server/ai-seo-apply.js +294 -0
  609. package/src/lib/server/ai-seo-apply.js.map +1 -0
  610. package/src/lib/server/ai-usage.d.ts +18 -0
  611. package/src/lib/server/ai-usage.js +220 -0
  612. package/src/lib/server/ai-usage.js.map +1 -0
  613. package/src/lib/server/assist-chat.d.ts +234 -0
  614. package/src/lib/server/assist-chat.js +1446 -0
  615. package/src/lib/server/assist-chat.js.map +1 -0
  616. package/src/lib/server/assist-edit-applied.d.ts +22 -0
  617. package/src/lib/server/assist-edit-applied.js +239 -0
  618. package/src/lib/server/assist-edit-applied.js.map +1 -0
  619. package/src/lib/server/assist-edit.d.ts +124 -0
  620. package/src/lib/server/assist-edit.js +1000 -0
  621. package/src/lib/server/assist-edit.js.map +1 -0
  622. package/src/lib/server/assist-feedback.d.ts +31 -0
  623. package/src/lib/server/assist-feedback.js +115 -0
  624. package/src/lib/server/assist-feedback.js.map +1 -0
  625. package/src/lib/server/assist-view-context.d.ts +343 -0
  626. package/src/lib/server/assist-view-context.js +690 -0
  627. package/src/lib/server/assist-view-context.js.map +1 -0
  628. package/src/lib/server/billing-credits.d.ts +59 -0
  629. package/src/lib/server/billing-credits.js +146 -0
  630. package/src/lib/server/billing-credits.js.map +1 -0
  631. package/src/lib/server/billing-overage.d.ts +19 -0
  632. package/src/lib/server/billing-overage.js +337 -0
  633. package/src/lib/server/billing-overage.js.map +1 -0
  634. package/src/lib/server.d.ts +32 -0
  635. package/src/lib/server.js +265 -0
  636. package/src/lib/server.js.map +1 -0
  637. package/src/lib/subprocessors.d.ts +18 -0
  638. package/src/lib/subprocessors.js +52 -0
  639. package/src/lib/subprocessors.js.map +1 -0
  640. package/src/lib/tools/ai-component-tool.d.ts +99 -0
  641. package/src/lib/tools/ai-component-tool.js +369 -0
  642. package/src/lib/tools/ai-component-tool.js.map +1 -0
  643. package/src/lib/tools/ai-crm-tool.d.ts +106 -0
  644. package/src/lib/tools/ai-crm-tool.js +488 -0
  645. package/src/lib/tools/ai-crm-tool.js.map +1 -0
  646. package/src/lib/tools/ai-duplicate-resource-tool.d.ts +71 -0
  647. package/src/lib/tools/ai-duplicate-resource-tool.js +164 -0
  648. package/src/lib/tools/ai-duplicate-resource-tool.js.map +1 -0
  649. package/src/lib/tools/ai-experiment-tool.d.ts +40 -0
  650. package/src/lib/tools/ai-experiment-tool.js +155 -0
  651. package/src/lib/tools/ai-experiment-tool.js.map +1 -0
  652. package/src/lib/tools/ai-insight-tool.d.ts +46 -0
  653. package/src/lib/tools/ai-insight-tool.js +200 -0
  654. package/src/lib/tools/ai-insight-tool.js.map +1 -0
  655. package/src/lib/tools/ai-inventory-lookup-tool.d.ts +86 -0
  656. package/src/lib/tools/ai-inventory-lookup-tool.js +164 -0
  657. package/src/lib/tools/ai-inventory-lookup-tool.js.map +1 -0
  658. package/src/lib/tools/ai-products-tool.d.ts +65 -0
  659. package/src/lib/tools/ai-products-tool.js +644 -0
  660. package/src/lib/tools/ai-products-tool.js.map +1 -0
  661. package/src/lib/tools/ai-seo-tool.d.ts +162 -0
  662. package/src/lib/tools/ai-seo-tool.js +546 -0
  663. package/src/lib/tools/ai-seo-tool.js.map +1 -0
  664. package/src/lib/tools/ai-theme-tool.d.ts +101 -0
  665. package/src/lib/tools/ai-theme-tool.js +562 -0
  666. package/src/lib/tools/ai-theme-tool.js.map +1 -0
  667. package/src/lib/tools/ai-workflow-tool.d.ts +166 -0
  668. package/src/lib/tools/ai-workflow-tool.js +860 -0
  669. package/src/lib/tools/ai-workflow-tool.js.map +1 -0
  670. package/src/lib/usage/ai-allotment-alerts.d.ts +72 -0
  671. package/src/lib/usage/ai-allotment-alerts.js +157 -0
  672. package/src/lib/usage/ai-allotment-alerts.js.map +1 -0
  673. package/src/lib/usage/ai-allotments.d.ts +125 -0
  674. package/src/lib/usage/ai-allotments.js +239 -0
  675. package/src/lib/usage/ai-allotments.js.map +1 -0
  676. package/src/lib/usage/ai-model-sample.d.ts +36 -0
  677. package/src/lib/usage/ai-model-sample.js +68 -0
  678. package/src/lib/usage/ai-model-sample.js.map +1 -0
  679. package/src/lib/usage/ai-usage-alerts.d.ts +55 -0
  680. package/src/lib/usage/ai-usage-alerts.js +114 -0
  681. package/src/lib/usage/ai-usage-alerts.js.map +1 -0
  682. package/src/lib/usage/ai-usage-by-user.d.ts +161 -0
  683. package/src/lib/usage/ai-usage-by-user.js +211 -0
  684. package/src/lib/usage/ai-usage-by-user.js.map +1 -0
  685. package/src/lib/usage/ai-usage-eraser.d.ts +29 -0
  686. package/src/lib/usage/ai-usage-eraser.js +40 -0
  687. package/src/lib/usage/ai-usage-eraser.js.map +1 -0
  688. package/src/lib/usage/ai-usage-meter.d.ts +39 -0
  689. package/src/lib/usage/ai-usage-meter.js +85 -0
  690. package/src/lib/usage/ai-usage-meter.js.map +1 -0
  691. package/src/lib/usage/ai-usage-wire.d.ts +184 -0
  692. package/src/lib/usage/ai-usage-wire.js +98 -0
  693. package/src/lib/usage/ai-usage-wire.js.map +1 -0
  694. package/src/lib/usage/assist-ceiling.d.ts +87 -0
  695. package/src/lib/usage/assist-ceiling.js +110 -0
  696. package/src/lib/usage/assist-ceiling.js.map +1 -0
  697. package/src/lib/usage/assist-free-taste.d.ts +225 -0
  698. package/src/lib/usage/assist-free-taste.js +308 -0
  699. package/src/lib/usage/assist-free-taste.js.map +1 -0
  700. package/src/lib/usage/assist-refusals.d.ts +69 -0
  701. package/src/lib/usage/assist-refusals.js +79 -0
  702. package/src/lib/usage/assist-refusals.js.map +1 -0
  703. package/src/lib/usage/assist-signal-mining.d.ts +375 -0
  704. package/src/lib/usage/assist-signal-mining.js +344 -0
  705. package/src/lib/usage/assist-signal-mining.js.map +1 -0
  706. package/src/lib/usage/assist-usage.d.ts +571 -0
  707. package/src/lib/usage/assist-usage.js +978 -0
  708. package/src/lib/usage/assist-usage.js.map +1 -0
  709. package/src/lib/usage/staff-org-ai.d.ts +242 -0
  710. package/src/lib/usage/staff-org-ai.js +173 -0
  711. package/src/lib/usage/staff-org-ai.js.map +1 -0
  712. package/src/lib/usage/staff-org-usage-ai.d.ts +83 -0
  713. package/src/lib/usage/staff-org-usage-ai.js +59 -0
  714. package/src/lib/usage/staff-org-usage-ai.js.map +1 -0
  715. package/src/lib/usage/staff-orgs-ai-spend.d.ts +83 -0
  716. package/src/lib/usage/staff-orgs-ai-spend.js +114 -0
  717. package/src/lib/usage/staff-orgs-ai-spend.js.map +1 -0
@@ -0,0 +1,3133 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ /**
3
+ * @license
4
+ * Copyright 2026 Aglyn LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */ import { sanitizeAuthorHtml } from "@aglyn/aglyn/app-utils/author-html";
18
+ import { formatBytes, measureNodeMap, nodeMapBytes } from "@aglyn/aglyn/app-utils/measure-node-map";
19
+ import { MEDIA_CDN_VARIANT_WIDTHS, parseMediaRef } from "@aglyn/aglyn/app-utils/media-ref";
20
+ import { ESTIMATED_PAGE_TRANSFER_BYTES, FREE_AI_TASTE_CREDITS_PER_MONTH } from "@aglyn/aglyn/app-utils/plan-entitlements";
21
+ import { REUSABLE_INSTANCE_COMPONENT_ID, REUSABLE_INSTANCE_PROP_VALUES_KEY } from "@aglyn/aglyn/app-utils/reusable-component-keys";
22
+ import { parseBreakpointSpan } from "@aglyn/shared-data-enums/breakpoint-span";
23
+ import { renderEmailHtml } from "@aglyn/shared-util-email/email-render";
24
+ import { aiPlanCreateFor, aiPlanUndeclaredRefs, isAiPlanNewRef } from "../model/ai-build-plan.js";
25
+ import { aiCreationNoun, aiPlanUncreatable, aiPlanUncreatableKind } from "../model/ai-plan-capabilities.js";
26
+ import { AI_INSTANCE_REF_PROP, validateAiNodeTree } from "./ai-node-tree.js";
27
+ import { AI_PAGE_SCROLL_TO_PROP, aiPageLinkTarget, aiStoredScrollTargets } from "./ai-page-links.js";
28
+ import { AI_EMAIL_CLIP_BYTES, AI_OUTPUT_BUDGETS, AI_OUTPUT_SURFACE, isHeadlineVariant } from "./ai-palette.js";
29
+ import { AI_PALETTE, AI_SX_TOKENS } from "./ai-palette.generated.js";
30
+ /** Each rule's title, in the words the proposal and the docs page use. */ export const AI_DOCTRINE_RULES = {
31
+ 1: 'Repeats become one reusable component',
32
+ 2: 'Site-wide regions live in the layout',
33
+ 3: 'Forms are built on the Forms page, then placed',
34
+ 4: 'Similar pages share one template',
35
+ 5: 'Colors, spacing and type come from the theme',
36
+ 6: 'Emails use the brand',
37
+ 7: 'Reuse before creating',
38
+ 8: 'Data is bound, not typed',
39
+ 9: 'Images come from the media library, with alt text',
40
+ 10: 'Navigation and SEO travel with a page',
41
+ 11: 'One main landmark and an ordered outline',
42
+ 12: "Responsive by the theme's breakpoints",
43
+ 13: 'Drafts only',
44
+ 14: 'Your voice, with no filler copy',
45
+ 15: 'Start from a duplicate of the nearest thing',
46
+ 16: 'The smallest document that does the job',
47
+ 17: 'A measured budget for every output'
48
+ };
49
+ export const AI_DOCTRINE_RULE_NUMBERS = Object.keys(AI_DOCTRINE_RULES).map(Number);
50
+ /** A violation as one line: the rule's number and title, then its sentence. */ export function aiDoctrineViolationText(violation) {
51
+ return violation.rule === null ? violation.message : `Rule ${violation.rule} (${AI_DOCTRINE_RULES[violation.rule]}): ${violation.message}`;
52
+ }
53
+ function isRecord(value) {
54
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
55
+ }
56
+ /**
57
+ * Every reachable node in document order, each once. Exported for a door's
58
+ * own checks (`extend`), which read a tree the way these detectors do.
59
+ */ export function walkTree(tree) {
60
+ const visits = [];
61
+ const seen = new Set();
62
+ const stack = [
63
+ {
64
+ id: tree.rootId,
65
+ depth: 0,
66
+ ancestors: []
67
+ }
68
+ ];
69
+ while(stack.length){
70
+ var _node_nodes;
71
+ const { id, depth, ancestors } = stack.pop();
72
+ const node = tree.nodes[id];
73
+ if (!node || seen.has(id)) continue;
74
+ seen.add(id);
75
+ visits.push({
76
+ id,
77
+ node,
78
+ depth,
79
+ ancestors
80
+ });
81
+ const children = (_node_nodes = node.nodes) != null ? _node_nodes : [];
82
+ for(let index = children.length - 1; index >= 0; index -= 1){
83
+ stack.push({
84
+ id: children[index],
85
+ depth: depth + 1,
86
+ ancestors: [
87
+ ...ancestors,
88
+ id
89
+ ]
90
+ });
91
+ }
92
+ }
93
+ return visits;
94
+ }
95
+ function displayName(componentId) {
96
+ var _ref;
97
+ var _AI_PALETTE_componentId;
98
+ return (_ref = (_AI_PALETTE_componentId = AI_PALETTE[componentId]) == null ? void 0 : _AI_PALETTE_componentId.displayName) != null ? _ref : componentId;
99
+ }
100
+ /** The HTML element a node was told to render as. */ function elementOf(node) {
101
+ var _ref;
102
+ var _node_props, _node_props1;
103
+ const value = (_ref = node == null ? void 0 : (_node_props = node.props) == null ? void 0 : _node_props['component']) != null ? _ref : node == null ? void 0 : (_node_props1 = node.props) == null ? void 0 : _node_props1['element'];
104
+ return typeof value === 'string' ? value : '';
105
+ }
106
+ function hasAncestor(tree, visit, test) {
107
+ return visit.ancestors.some((id)=>{
108
+ const node = tree.nodes[id];
109
+ return node ? test(node) : false;
110
+ });
111
+ }
112
+ function unique(values) {
113
+ return [
114
+ ...new Set(values)
115
+ ];
116
+ }
117
+ function listNames(ids, tree) {
118
+ const names = unique(ids.map((id)=>{
119
+ var _ref;
120
+ var _tree_nodes_id;
121
+ return displayName((_ref = (_tree_nodes_id = tree.nodes[id]) == null ? void 0 : _tree_nodes_id.componentId) != null ? _ref : '');
122
+ }));
123
+ return names.length <= 1 ? names.join('') : `${names.slice(0, -1).join(', ')} and ${names[names.length - 1]}`;
124
+ }
125
+ // Rule 1 and rule 8 read the same index: which subtrees share a shape.
126
+ /** Below this, a repeat is a row of buttons, not a block worth a component. */ export const AI_REPEAT_MIN_NODES = 3;
127
+ /** Rule 1: a block on one page this many times is a component. */ export const AI_REPEAT_MIN_COUNT = 3;
128
+ /** Rule 1 across pages: a block this large on two pages is a component. */ export const AI_REPEAT_ACROSS_PAGES_MIN_NODES = 5;
129
+ /** Rule 8: a hand-typed list this long is data. */ export const AI_TYPED_LIST_MIN_ITEMS = 8;
130
+ /**
131
+ * A subtree's shape: element ids, the prop NAMES it sets, and the values of
132
+ * the props that change structure (enums, switches, numbers) — but never the
133
+ * values of copy, links or media, which is what differs between the twelve
134
+ * cards that should have been one component.
135
+ */ function indexShapes(tree) {
136
+ const shapeOf = new Map();
137
+ const sizeOf = new Map();
138
+ const visits = walkTree(tree);
139
+ for(let index = visits.length - 1; index >= 0; index -= 1){
140
+ var _ref, _node_props, _node_nodes;
141
+ var _AI_PALETTE_node_componentId;
142
+ const { id, node } = visits[index];
143
+ const schema = (_ref = (_AI_PALETTE_node_componentId = AI_PALETTE[node.componentId]) == null ? void 0 : _AI_PALETTE_node_componentId.propsSchema.properties) != null ? _ref : {};
144
+ const props = Object.keys((_node_props = node.props) != null ? _node_props : {}).sort().map((key)=>{
145
+ var _node_props;
146
+ const declared = schema[key];
147
+ const structural = Boolean(declared == null ? void 0 : declared.enum) || (declared == null ? void 0 : declared.type) === 'boolean' || (declared == null ? void 0 : declared.type) === 'number' || (declared == null ? void 0 : declared.type) === 'integer';
148
+ return structural ? `${key}=${String((_node_props = node.props) == null ? void 0 : _node_props[key])}` : key;
149
+ }).join(',');
150
+ const children = ((_node_nodes = node.nodes) != null ? _node_nodes : []).filter((child)=>shapeOf.has(child));
151
+ shapeOf.set(id, `${node.componentId}(${props})[${children.map((child)=>shapeOf.get(child)).join('|')}]`);
152
+ sizeOf.set(id, 1 + children.reduce((sum, child)=>{
153
+ var _sizeOf_get;
154
+ return sum + ((_sizeOf_get = sizeOf.get(child)) != null ? _sizeOf_get : 0);
155
+ }, 0));
156
+ }
157
+ return {
158
+ visits,
159
+ shapeOf,
160
+ sizeOf
161
+ };
162
+ }
163
+ /** Whether a node sits inside a collection repeater, whose child is authored once and repeated by data. */ function repeatedByData(tree, visit) {
164
+ return hasAncestor(tree, visit, (node)=>node.componentId === 'collectionEntries');
165
+ }
166
+ /**
167
+ * Shapes that repeat at least `minCount` times, reported only where they are
168
+ * the outermost repeat: the header inside each of twelve cards repeats
169
+ * because the card does, and the fix is the card. `groupOf` counts each
170
+ * group's repeats apart; absent, the whole tree is one group.
171
+ */ function repeatedShapes(tree, index, minNodes, minCount, groupOf = ()=>tree.rootId) {
172
+ const byShape = new Map();
173
+ for (const visit of index.visits){
174
+ var _index_sizeOf_get, _byShape_get;
175
+ if (visit.id === tree.rootId) continue;
176
+ if (((_index_sizeOf_get = index.sizeOf.get(visit.id)) != null ? _index_sizeOf_get : 0) < minNodes) continue;
177
+ if (repeatedByData(tree, visit)) continue;
178
+ const shape = JSON.stringify([
179
+ groupOf(visit),
180
+ index.shapeOf.get(visit.id)
181
+ ]);
182
+ byShape.set(shape, [
183
+ ...(_byShape_get = byShape.get(shape)) != null ? _byShape_get : [],
184
+ visit.id
185
+ ]);
186
+ }
187
+ const repeated = [
188
+ ...byShape.entries()
189
+ ].filter(([, ids])=>ids.length >= minCount);
190
+ const repeatedIds = new Set(repeated.flatMap(([, ids])=>ids));
191
+ const ancestorsOf = new Map(index.visits.map((visit)=>[
192
+ visit.id,
193
+ visit.ancestors
194
+ ]));
195
+ return repeated.filter(([, ids])=>!ids.every((id)=>{
196
+ var _ancestorsOf_get;
197
+ return ((_ancestorsOf_get = ancestorsOf.get(id)) != null ? _ancestorsOf_get : []).some((ancestor)=>repeatedIds.has(ancestor));
198
+ })).map(([shape, ids])=>({
199
+ shape,
200
+ ids
201
+ }));
202
+ }
203
+ /**
204
+ * Rule 1. A block repeated on one page, or a block this page shares with
205
+ * another page generated beside it, is one component placed as instances.
206
+ *
207
+ * A workspace that keeps no reusable components can place no instance, so
208
+ * there a repeated block is drawn where it repeats, and nothing is refused.
209
+ */ export function detectRepeatedSubtrees(tree, otherPages = [], context = {}) {
210
+ if (context.reusableComponents === false) return [];
211
+ const index = indexShapes(tree);
212
+ const violations = repeatedShapes(tree, index, AI_REPEAT_MIN_NODES, AI_REPEAT_MIN_COUNT).map(({ ids })=>{
213
+ const name = displayName(tree.nodes[ids[0]].componentId);
214
+ return {
215
+ rule: 1,
216
+ code: 'repeated-subtree',
217
+ message: `The same ${name} block appears ${ids.length} times. Make it one reusable component with props for what changes, and place it ${ids.length} times.`,
218
+ nodeIds: ids
219
+ };
220
+ });
221
+ if (otherPages.length) {
222
+ const elsewhere = new Set();
223
+ for (const other of otherPages){
224
+ const otherIndex = indexShapes(other);
225
+ for (const visit of otherIndex.visits){
226
+ var _otherIndex_sizeOf_get;
227
+ if (visit.id === other.rootId) continue;
228
+ if (((_otherIndex_sizeOf_get = otherIndex.sizeOf.get(visit.id)) != null ? _otherIndex_sizeOf_get : 0) >= AI_REPEAT_ACROSS_PAGES_MIN_NODES) {
229
+ elsewhere.add(otherIndex.shapeOf.get(visit.id));
230
+ }
231
+ }
232
+ }
233
+ const shared = index.visits.filter((visit)=>{
234
+ var _index_sizeOf_get;
235
+ return visit.id !== tree.rootId && ((_index_sizeOf_get = index.sizeOf.get(visit.id)) != null ? _index_sizeOf_get : 0) >= AI_REPEAT_ACROSS_PAGES_MIN_NODES && elsewhere.has(index.shapeOf.get(visit.id));
236
+ });
237
+ const sharedIds = new Set(shared.map((visit)=>visit.id));
238
+ const outermost = shared.filter((visit)=>!visit.ancestors.some((id)=>sharedIds.has(id)));
239
+ for (const visit of outermost){
240
+ violations.push({
241
+ rule: 1,
242
+ code: 'repeated-across-pages',
243
+ message: `This page and another share the same ${displayName(visit.node.componentId)} block. Make it one reusable component and place it on both.`,
244
+ nodeIds: [
245
+ visit.id
246
+ ]
247
+ });
248
+ }
249
+ }
250
+ return violations;
251
+ }
252
+ /**
253
+ * How many copies of one repeated thing a document shows, read as
254
+ * generously as the tree allows (AGL-3024): the largest group of nodes the
255
+ * shape index calls the same shape — the index rule 1 counts repeats with —
256
+ * or, where a node's children outnumber that, its fan-out.
257
+ *
258
+ * GENEROUS ON PURPOSE. The one caller compares this against a count a plan
259
+ * promised and speaks only when the count is SHORT, so every reading that
260
+ * finds more items makes the check quieter, never louder: three cards whose
261
+ * headings a hero's subtitle happens to match read as four and are let
262
+ * through, and four cards laid out as two rows of two read as four either by
263
+ * their shared shape or by the rows' fan-out. What it cannot do is find four
264
+ * items in a document that holds three of everything.
265
+ */ export function aiRepeatedItemCount(tree) {
266
+ const index = indexShapes(tree);
267
+ const byShape = new Map();
268
+ let most = 0;
269
+ for (const visit of index.visits){
270
+ var _visit_node_nodes;
271
+ if (visit.id !== tree.rootId) {
272
+ const shape = index.shapeOf.get(visit.id);
273
+ if (shape) {
274
+ var _byShape_get;
275
+ const count = ((_byShape_get = byShape.get(shape)) != null ? _byShape_get : 0) + 1;
276
+ byShape.set(shape, count);
277
+ if (count > most) most = count;
278
+ }
279
+ }
280
+ const children = ((_visit_node_nodes = visit.node.nodes) != null ? _visit_node_nodes : []).filter((id)=>tree.nodes[id]).length;
281
+ if (children > most) most = children;
282
+ }
283
+ return most;
284
+ }
285
+ /** Whether a document draws its repeated items from a collection, which fills them at render. */ export function aiBindsRepeatedItems(tree) {
286
+ return walkTree(tree).some(({ node })=>node.componentId === 'collectionEntries');
287
+ }
288
+ const REGION_ELEMENTS = new Set([
289
+ 'header',
290
+ 'footer',
291
+ 'nav'
292
+ ]);
293
+ const REGION_WORDS = {
294
+ header: 'header',
295
+ footer: 'footer',
296
+ nav: 'navigation'
297
+ };
298
+ /**
299
+ * The regions a layout is built out of, and how each one is found in a tree
300
+ * (AGL-3024): the landmark elements it may be written as, and the palette
301
+ * elements that ARE one wherever they appear.
302
+ *
303
+ * A CLOSED vocabulary, because it is what a plan's promise of a region is
304
+ * checked against: a planner naming a region outside it has promised
305
+ * something no check can find, which the plan rules refuse rather than let
306
+ * pass as built.
307
+ */ export const AI_LAYOUT_REGIONS = {
308
+ header: {
309
+ elements: [
310
+ 'header'
311
+ ],
312
+ componentIds: [
313
+ 'muiAppBar'
314
+ ]
315
+ },
316
+ nav: {
317
+ elements: [
318
+ 'nav'
319
+ ],
320
+ componentIds: []
321
+ },
322
+ sidebar: {
323
+ elements: [
324
+ 'aside'
325
+ ],
326
+ componentIds: []
327
+ },
328
+ main: {
329
+ elements: [
330
+ 'main'
331
+ ],
332
+ componentIds: [
333
+ 'layoutSlot'
334
+ ]
335
+ },
336
+ footer: {
337
+ elements: [
338
+ 'footer'
339
+ ],
340
+ componentIds: []
341
+ }
342
+ };
343
+ export const AI_LAYOUT_REGION_NAMES = Object.keys(AI_LAYOUT_REGIONS);
344
+ /** The words a plan may name each region by, beyond the region's own name. */ const LAYOUT_REGION_ALIASES = {
345
+ 'app bar': 'header',
346
+ masthead: 'header',
347
+ 'top bar': 'header',
348
+ navigation: 'nav',
349
+ 'nav bar': 'nav',
350
+ menu: 'nav',
351
+ aside: 'sidebar',
352
+ 'side bar': 'sidebar',
353
+ 'side nav': 'sidebar',
354
+ drawer: 'sidebar',
355
+ rail: 'sidebar',
356
+ slot: 'main',
357
+ 'layout slot': 'main',
358
+ content: 'main'
359
+ };
360
+ /**
361
+ * The region a plan's word names, or `null` for a word the vocabulary does
362
+ * not hold.
363
+ *
364
+ * Read the way a recorded plan writes one. A planner names a region as
365
+ * `header`, as `layoutSlot:slot` or as `footer:region (firm name, address)`,
366
+ * so the name is what stands before the first colon, capitals are word
367
+ * breaks, and a trailing "region" or "area" is filler once the whole name has
368
+ * been tried: "Sidebar region", "side-bar" and `sidebar:links` are one thing.
369
+ */ export function aiLayoutRegionOf(word) {
370
+ var _read;
371
+ const cleaned = word.split(':')[0].replace(/([a-z0-9])([A-Z])/g, '$1 $2').toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim();
372
+ const read = (value)=>{
373
+ var _LAYOUT_REGION_ALIASES_value;
374
+ return AI_LAYOUT_REGION_NAMES.includes(value) ? value : (_LAYOUT_REGION_ALIASES_value = LAYOUT_REGION_ALIASES[value]) != null ? _LAYOUT_REGION_ALIASES_value : null;
375
+ };
376
+ if (!cleaned) return null;
377
+ return (_read = read(cleaned)) != null ? _read : read(cleaned.replace(/\s+(?:region|area)$/, ''));
378
+ }
379
+ /** Every region a built tree carries, by the elements and palette elements it holds. */ export function aiTreeLayoutRegions(tree) {
380
+ const found = new Set();
381
+ for (const { node } of walkTree(tree)){
382
+ const element = elementOf(node);
383
+ for (const region of AI_LAYOUT_REGION_NAMES){
384
+ const where = AI_LAYOUT_REGIONS[region];
385
+ if (element && where.elements.includes(element) || where.componentIds.includes(node.componentId)) {
386
+ found.add(region);
387
+ }
388
+ }
389
+ }
390
+ return found;
391
+ }
392
+ /**
393
+ * Rule 2. A page carries no copy of what every page carries: an app bar, or
394
+ * a header, navigation or footer element near the top of the document. The
395
+ * header of an article inside the page is the article's, not the site's.
396
+ */ export function detectLayoutRegions(tree, outputKind) {
397
+ if (outputKind !== 'page' && outputKind !== 'template') return [];
398
+ const found = [];
399
+ for (const visit of walkTree(tree)){
400
+ if (visit.id === tree.rootId) continue;
401
+ if (visit.node.componentId === 'muiAppBar') {
402
+ found.push({
403
+ id: visit.id,
404
+ region: 'header'
405
+ });
406
+ continue;
407
+ }
408
+ const element = elementOf(visit.node);
409
+ if (REGION_ELEMENTS.has(element) && visit.depth <= 3 && !hasAncestor(tree, visit, (node)=>elementOf(node) === 'article')) {
410
+ found.push({
411
+ id: visit.id,
412
+ region: REGION_WORDS[element]
413
+ });
414
+ }
415
+ }
416
+ if (!found.length) return [];
417
+ const regions = unique(found.map((entry)=>entry.region)).join(' and ');
418
+ return [
419
+ {
420
+ rule: 2,
421
+ code: 'layout-region-in-screen',
422
+ message: `This page carries its own site ${regions}. Headers, navigation and footers live in the site's layout; put the page in the layout instead of copying them onto it.`,
423
+ nodeIds: found.map((entry)=>entry.id)
424
+ }
425
+ ];
426
+ }
427
+ /**
428
+ * Rule 3. A form on a page is a Form bound by id to a form built on the
429
+ * Forms page — fields, validation, consent and routing kept in one place.
430
+ * Loose fields, an unbound Form, and fields drawn inside a bound Form (which
431
+ * the page never renders: the placed form's own design replaces them) are
432
+ * refused. A form's own design is the one place fields are drawn.
433
+ *
434
+ * A workspace that keeps no saved forms (AGL-3030) has no Forms page to build
435
+ * one on, so there the form IS the page's: an unbound Form holding its Form
436
+ * Fields, which the site's submit route collects like any other. Loose fields
437
+ * are still refused, and so is an unbound Form with no field to send.
438
+ */ export function detectInlineForms(tree, outputKind, context = {}) {
439
+ if (outputKind === 'form' || outputKind === 'email') return [];
440
+ const inline = context.reusableComponents === false;
441
+ const loose = [];
442
+ const unbound = [];
443
+ const shadowed = [];
444
+ const empty = [];
445
+ const visits = walkTree(tree);
446
+ for (const visit of visits){
447
+ if (visit.node.componentId === 'formField' && !hasAncestor(tree, visit, (node)=>node.componentId === 'form')) {
448
+ loose.push(visit.id);
449
+ }
450
+ if (visit.node.componentId === 'form') {
451
+ var _visit_node_props;
452
+ const bound = (_visit_node_props = visit.node.props) == null ? void 0 : _visit_node_props['formId'];
453
+ if (typeof bound === 'string' && bound) {
454
+ var _visit_node_nodes;
455
+ if (((_visit_node_nodes = visit.node.nodes) != null ? _visit_node_nodes : []).length) shadowed.push(visit.id);
456
+ } else if (!inline) {
457
+ unbound.push(visit.id);
458
+ } else if (!visits.some((inner)=>inner.node.componentId === 'formField' && inner.ancestors.includes(visit.id))) {
459
+ empty.push(visit.id);
460
+ }
461
+ }
462
+ }
463
+ const violations = [];
464
+ if (inline && loose.length) {
465
+ violations.push({
466
+ rule: 3,
467
+ code: 'loose-form-field',
468
+ message: 'This draws form fields outside a form, where nothing sends them. Put every Form Field inside the Form it belongs to.',
469
+ nodeIds: loose
470
+ });
471
+ }
472
+ if (empty.length) {
473
+ violations.push({
474
+ rule: 3,
475
+ code: 'form-without-fields',
476
+ message: 'This form has no field to send. Give it the Form Fields it collects.',
477
+ nodeIds: empty
478
+ });
479
+ }
480
+ if (!inline && (loose.length || unbound.length)) {
481
+ violations.push({
482
+ rule: 3,
483
+ code: 'inline-form',
484
+ message: 'This draws a form inline. Build the form on the Forms page, with its fields, validation, consent and routing, then place it here by its id.',
485
+ nodeIds: [
486
+ ...unbound,
487
+ ...loose
488
+ ]
489
+ });
490
+ }
491
+ if (shadowed.length) {
492
+ violations.push({
493
+ rule: 3,
494
+ code: 'fields-inside-bound-form',
495
+ message: 'This form is placed by its id, so its fields come from the Forms page. Remove the copies of the fields drawn inside it.',
496
+ nodeIds: shadowed
497
+ });
498
+ }
499
+ return violations;
500
+ }
501
+ const COLOR_SX_KEYS = new Set([
502
+ 'color',
503
+ 'bgcolor',
504
+ 'backgroundColor',
505
+ 'borderColor'
506
+ ]);
507
+ const COLOR_IN_VALUE_SX_KEYS = new Set([
508
+ 'border',
509
+ 'borderTop',
510
+ 'borderRight',
511
+ 'borderBottom',
512
+ 'borderLeft',
513
+ 'boxShadow'
514
+ ]);
515
+ const SPACING_SX_KEYS = new Set([
516
+ 'm',
517
+ 'mt',
518
+ 'mr',
519
+ 'mb',
520
+ 'ml',
521
+ 'mx',
522
+ 'my',
523
+ 'margin',
524
+ 'marginTop',
525
+ 'marginRight',
526
+ 'marginBottom',
527
+ 'marginLeft',
528
+ 'p',
529
+ 'pt',
530
+ 'pr',
531
+ 'pb',
532
+ 'pl',
533
+ 'px',
534
+ 'py',
535
+ 'padding',
536
+ 'paddingTop',
537
+ 'paddingRight',
538
+ 'paddingBottom',
539
+ 'paddingLeft',
540
+ 'gap',
541
+ 'rowGap',
542
+ 'columnGap'
543
+ ]);
544
+ const TYPE_SX_KEYS = new Set([
545
+ 'fontSize',
546
+ 'lineHeight',
547
+ 'letterSpacing'
548
+ ]);
549
+ const COLOR_KEYWORDS = new Set([
550
+ 'inherit',
551
+ 'transparent',
552
+ 'currentcolor',
553
+ 'initial',
554
+ 'unset',
555
+ 'none'
556
+ ]);
557
+ const LITERAL_COLOR = /#[0-9a-f]{3,8}\b|\b(?:rgba?|hsla?|hwb|lab|lch|oklab|oklch|color)\s*\(|\b(?:black|white|red|green|blue|yellow|orange|purple|pink|gray|grey|brown|cyan|magenta|navy|teal|maroon|olive|lime|aqua|silver|gold|indigo|violet|beige|coral|crimson|salmon|tomato|turquoise|khaki|lavender|ivory|azure)\b/i;
558
+ const LENGTH_WITH_UNIT = /-?\d*\.?\d+\s*(?:px|rem|em|pt|vh|vw|ch)\b/i;
559
+ const paletteTokens = new Set(AI_SX_TOKENS.palette);
560
+ /** Whether a value names a color by value rather than by theme token. */ function hasLiteralColor(value) {
561
+ if (typeof value !== 'string') return false;
562
+ const parts = value.trim().split(/\s+/).filter((part)=>!paletteTokens.has(part) && !COLOR_KEYWORDS.has(part.toLowerCase()));
563
+ return LITERAL_COLOR.test(parts.join(' '));
564
+ }
565
+ /** An `sx` value and each of its responsive values. */ function sxValues(value) {
566
+ return isRecord(value) ? Object.values(value) : [
567
+ value
568
+ ];
569
+ }
570
+ /**
571
+ * Rule 5. Every color, spacing, radius and type size is a theme token. A
572
+ * color the theme lacks is a theme change for the theme door to draft, not a
573
+ * value written onto one element.
574
+ */ export function detectLiteralStyles(tree, outputKind) {
575
+ if (outputKind === 'email') return [];
576
+ const colors = [];
577
+ const lengths = [];
578
+ for (const { id, node } of walkTree(tree)){
579
+ var _node_sx, _ref, _node_props;
580
+ var _AI_PALETTE_node_componentId;
581
+ for (const [key, raw] of Object.entries((_node_sx = node.sx) != null ? _node_sx : {})){
582
+ for (const value of sxValues(raw)){
583
+ if ((COLOR_SX_KEYS.has(key) || COLOR_IN_VALUE_SX_KEYS.has(key)) && hasLiteralColor(value)) {
584
+ colors.push(id);
585
+ } else if ((SPACING_SX_KEYS.has(key) || key === 'borderRadius' || TYPE_SX_KEYS.has(key)) && typeof value === 'string' && LENGTH_WITH_UNIT.test(value)) {
586
+ lengths.push(id);
587
+ } else if (key === 'fontSize' && typeof value === 'number' && value > 0) {
588
+ lengths.push(id);
589
+ }
590
+ }
591
+ }
592
+ const schema = (_ref = (_AI_PALETTE_node_componentId = AI_PALETTE[node.componentId]) == null ? void 0 : _AI_PALETTE_node_componentId.propsSchema.properties) != null ? _ref : {};
593
+ for (const [name, value] of Object.entries((_node_props = node.props) != null ? _node_props : {})){
594
+ var _schema_name;
595
+ if (/(?:^|[a-z])(?:color|Color|bgcolor)$/.test(name) && !((_schema_name = schema[name]) == null ? void 0 : _schema_name.enum) && hasLiteralColor(value)) {
596
+ colors.push(id);
597
+ }
598
+ }
599
+ }
600
+ const violations = [];
601
+ if (colors.length) {
602
+ violations.push({
603
+ rule: 5,
604
+ code: 'literal-color',
605
+ message: "This sets colors by value. Use the theme's color tokens, and propose a theme change for a color the theme does not have.",
606
+ nodeIds: unique(colors)
607
+ });
608
+ }
609
+ if (lengths.length) {
610
+ violations.push({
611
+ rule: 5,
612
+ code: 'literal-length',
613
+ message: "This sets spacing, corners or type sizes in fixed units. Use the theme's spacing scale, shape and typography styles.",
614
+ nodeIds: unique(lengths)
615
+ });
616
+ }
617
+ return violations;
618
+ }
619
+ function normalizeHex(value) {
620
+ const hex = value.trim().toLowerCase();
621
+ return /^#[0-9a-f]{3}$/.test(hex) ? `#${hex[1]}${hex[1]}${hex[2]}${hex[2]}${hex[3]}${hex[3]}` : hex;
622
+ }
623
+ /**
624
+ * Rule 6. An email is painted with the brand: its colors are the site's
625
+ * brand colors and its fonts the brand's. A mail client cannot resolve a
626
+ * theme token, so the brand's own values are what an email may name — and
627
+ * with no brand recorded, the blocks' own defaults.
628
+ */ export function detectOffBrandEmail(tree, outputKind, brand) {
629
+ var _ref, _ref1;
630
+ if (outputKind !== 'email') return [];
631
+ const allowed = new Set(Object.values((_ref = brand == null ? void 0 : brand.colors) != null ? _ref : {}).map(normalizeHex));
632
+ const fonts = new Set(((_ref1 = brand == null ? void 0 : brand.fonts) != null ? _ref1 : []).map((font)=>font.toLowerCase()));
633
+ const offBrand = [];
634
+ const values = [];
635
+ for (const { id, node } of walkTree(tree)){
636
+ var _node_sx;
637
+ var _node_props;
638
+ const candidates = [];
639
+ for (const name of [
640
+ 'color',
641
+ 'backgroundColor'
642
+ ])candidates.push((_node_props = node.props) == null ? void 0 : _node_props[name]);
643
+ for (const [key, raw] of Object.entries((_node_sx = node.sx) != null ? _node_sx : {})){
644
+ if (COLOR_SX_KEYS.has(key)) candidates.push(...sxValues(raw));
645
+ if (key === 'fontFamily') {
646
+ for (const family of sxValues(raw)){
647
+ if (typeof family === 'string' && !fonts.has(family.trim().toLowerCase())) {
648
+ offBrand.push(id);
649
+ values.push(family.trim());
650
+ }
651
+ }
652
+ }
653
+ }
654
+ for (const candidate of candidates){
655
+ if (typeof candidate !== 'string' || !candidate.trim()) continue;
656
+ if (!allowed.has(normalizeHex(candidate))) {
657
+ offBrand.push(id);
658
+ values.push(candidate.trim());
659
+ }
660
+ }
661
+ }
662
+ if (!offBrand.length) return [];
663
+ return [
664
+ {
665
+ rule: 6,
666
+ code: 'off-brand-email',
667
+ message: `This email uses ${unique(values).slice(0, 3).join(', ')}, outside the brand. Use the brand's colors and fonts so every email reads as the same sender.`,
668
+ nodeIds: unique(offBrand)
669
+ }
670
+ ];
671
+ }
672
+ /**
673
+ * Rule 8 (tree). A long list typed out item by item is data: bound to a
674
+ * dataset or a content collection it stays current; typed, it is a copy that
675
+ * goes stale. Instances of one component filled in side by side are the same
676
+ * list in a component's clothes, and are counted the same way.
677
+ *
678
+ * A list is counted within the Section it sits in, the unit a plan counts a
679
+ * section's items in (AGL-3061): a page's four practice areas and its four
680
+ * steps are two short lists that happen to share a card, not one long one,
681
+ * and a plan that kept them must not build a page this refuses. The same
682
+ * list split across a section's columns is still one list. A tree with no
683
+ * Section is one group.
684
+ */ export function detectTypedData(tree) {
685
+ const index = indexShapes(tree);
686
+ const sectionOf = (visit)=>{
687
+ var _reverse_find;
688
+ return (_reverse_find = [
689
+ ...visit.ancestors
690
+ ].reverse().find((id)=>{
691
+ var _tree_nodes_id;
692
+ return ((_tree_nodes_id = tree.nodes[id]) == null ? void 0 : _tree_nodes_id.componentId) === 'section';
693
+ })) != null ? _reverse_find : tree.rootId;
694
+ };
695
+ const violations = repeatedShapes(tree, index, AI_REPEAT_MIN_NODES - 1, AI_TYPED_LIST_MIN_ITEMS, sectionOf).map(({ ids })=>({
696
+ rule: 8,
697
+ code: 'typed-list',
698
+ message: `${ids.length} ${displayName(tree.nodes[ids[0]].componentId)} items are typed out by hand. Bind the list to a dataset or content collection, or propose a dataset for data the site does not have yet.`,
699
+ nodeIds: ids
700
+ }));
701
+ for (const visit of index.visits){
702
+ var _visit_node_nodes;
703
+ if (repeatedByData(tree, visit) || visit.node.componentId === 'collectionEntries') continue;
704
+ const byComponent = new Map();
705
+ for (const childId of (_visit_node_nodes = visit.node.nodes) != null ? _visit_node_nodes : []){
706
+ var _byComponent_get;
707
+ var _child_props;
708
+ const child = tree.nodes[childId];
709
+ const ref = child == null ? void 0 : (_child_props = child.props) == null ? void 0 : _child_props[AI_INSTANCE_REF_PROP];
710
+ if ((child == null ? void 0 : child.componentId) !== REUSABLE_INSTANCE_COMPONENT_ID || typeof ref !== 'string') continue;
711
+ byComponent.set(ref, [
712
+ ...(_byComponent_get = byComponent.get(ref)) != null ? _byComponent_get : [],
713
+ childId
714
+ ]);
715
+ }
716
+ for (const ids of byComponent.values()){
717
+ if (ids.length < AI_TYPED_LIST_MIN_ITEMS) continue;
718
+ violations.push({
719
+ rule: 8,
720
+ code: 'typed-instances',
721
+ message: `${ids.length} copies of one component are filled in by hand. Bind them to a dataset or content collection, or propose a dataset for data the site does not have yet.`,
722
+ nodeIds: ids
723
+ });
724
+ }
725
+ }
726
+ return violations;
727
+ }
728
+ /** The media-role props of a node, with the declared image props of an instance. */ function mediaValues(node, context) {
729
+ var _ref, _node_props;
730
+ var _AI_PALETTE_node_componentId, _node_props1, _node_props2;
731
+ const out = [];
732
+ const roles = (_ref = (_AI_PALETTE_node_componentId = AI_PALETTE[node.componentId]) == null ? void 0 : _AI_PALETTE_node_componentId.propRoles) != null ? _ref : {};
733
+ for (const [name, value] of Object.entries((_node_props = node.props) != null ? _node_props : {})){
734
+ if (roles[name] === 'media' && typeof value === 'string' && value.trim()) {
735
+ out.push({
736
+ name,
737
+ value: value.trim()
738
+ });
739
+ }
740
+ }
741
+ const refId = (_node_props1 = node.props) == null ? void 0 : _node_props1[AI_INSTANCE_REF_PROP];
742
+ const values = (_node_props2 = node.props) == null ? void 0 : _node_props2[REUSABLE_INSTANCE_PROP_VALUES_KEY];
743
+ if (typeof refId === 'string' && isRecord(values)) {
744
+ var _ref1;
745
+ var _context_componentProps;
746
+ const declared = (_ref1 = (_context_componentProps = context.componentProps) == null ? void 0 : _context_componentProps[refId]) != null ? _ref1 : {};
747
+ for (const [name, value] of Object.entries(values)){
748
+ if (declared[name] === 'image' && typeof value === 'string' && value.trim()) {
749
+ out.push({
750
+ name: `${REUSABLE_INSTANCE_PROP_VALUES_KEY}.${name}`,
751
+ value: value.trim()
752
+ });
753
+ }
754
+ }
755
+ }
756
+ return out;
757
+ }
758
+ /**
759
+ * Rule 9. Images are placed from the media library — an asset, or an empty
760
+ * slot for the author to fill — never hot-linked from another site, and
761
+ * every picture carries alt text (or says it is decorative).
762
+ */ export function detectImageSources(tree, context = {}) {
763
+ const hotlinked = [];
764
+ const missingAlt = [];
765
+ for (const { id, node } of walkTree(tree)){
766
+ if (mediaValues(node, context).some(({ value })=>/^https?:\/\//i.test(value))) {
767
+ hotlinked.push(id);
768
+ }
769
+ if (node.componentId === 'image' || node.componentId === 'emailImage') {
770
+ var _node_props, _node_props1;
771
+ const alt = (_node_props = node.props) == null ? void 0 : _node_props['alt'];
772
+ const decorative = ((_node_props1 = node.props) == null ? void 0 : _node_props1['decorative']) === true;
773
+ if (!decorative && (typeof alt !== 'string' || !alt.trim())) missingAlt.push(id);
774
+ }
775
+ }
776
+ const violations = [];
777
+ if (hotlinked.length) {
778
+ violations.push({
779
+ rule: 9,
780
+ code: 'hotlinked-image',
781
+ message: 'This loads images from another website. Place images from the media library, or leave the slot empty for an upload.',
782
+ nodeIds: hotlinked
783
+ });
784
+ }
785
+ if (missingAlt.length) {
786
+ violations.push({
787
+ rule: 9,
788
+ code: 'missing-alt',
789
+ message: 'An image has no alt text. Describe what each picture shows, or mark a purely decorative one as decorative.',
790
+ nodeIds: missingAlt
791
+ });
792
+ }
793
+ return violations;
794
+ }
795
+ /**
796
+ * A heading's level: the element it renders as, else the variant's own.
797
+ * Exported for a door's own checks, which read an outline the way rule 11
798
+ * reads a page's.
799
+ */ export function aiHeadingLevel(node) {
800
+ var _ref;
801
+ var _node_props, _node_props1;
802
+ if (node.componentId !== 'muiTypography') return null;
803
+ const element = typeof ((_node_props = node.props) == null ? void 0 : _node_props['component']) === 'string' ? String(node.props['component']) : '';
804
+ const match = /^h([1-6])$/.exec(element || String((_ref = (_node_props1 = node.props) == null ? void 0 : _node_props1['variant']) != null ? _ref : ''));
805
+ return match ? Number(match[1]) : null;
806
+ }
807
+ /**
808
+ * Rule 11. One `main` landmark per document, and a heading outline that
809
+ * starts at one `h1` and never skips a level. The platform places the one
810
+ * landmark on the layout's slot, or on a page's root when it has no layout
811
+ * (`stampDocumentLandmark`); a tree that declares two leaves it to pick the
812
+ * wrong one. A component, a form or an email is placed inside pages, so it
813
+ * carries no landmark of its own, and a layout carries no `h1`: every page
814
+ * placed in it brings its own.
815
+ */ export function detectDocumentStructure(tree, outputKind) {
816
+ var _ref;
817
+ var _headings_;
818
+ const visits = walkTree(tree);
819
+ const mains = visits.filter((visit)=>elementOf(visit.node) === 'main').map((visit)=>visit.id);
820
+ const violations = [];
821
+ if (outputKind === 'component' || outputKind === 'email' || outputKind === 'form') {
822
+ if (mains.length) {
823
+ violations.push({
824
+ rule: 11,
825
+ code: 'landmark-in-fragment',
826
+ message: `A ${outputKind} is placed inside pages, so it cannot carry the main landmark. Leave the element unset.`,
827
+ nodeIds: mains
828
+ });
829
+ }
830
+ } else if (mains.length > 1) {
831
+ violations.push({
832
+ rule: 11,
833
+ code: 'multiple-main',
834
+ message: 'This declares more than one main landmark. A page has exactly one.',
835
+ nodeIds: mains
836
+ });
837
+ }
838
+ if (outputKind === 'layout') {
839
+ const slots = visits.filter((visit)=>visit.node.componentId === 'layoutSlot');
840
+ const rootElement = elementOf(tree.nodes[tree.rootId]);
841
+ const slotElement = slots[0] ? elementOf(slots[0].node) : '';
842
+ // The landmark goes on the slot, else on the root — unless an author
843
+ // chose another element for each.
844
+ const chosenAway = rootElement !== '' && rootElement !== 'main' && (!slots.length || slotElement !== '' && slotElement !== 'main');
845
+ if (slots.length !== 1 || chosenAway) {
846
+ violations.push({
847
+ rule: 11,
848
+ code: 'layout-slot',
849
+ message: 'A layout has exactly one Layout Slot, where each page renders, and the slot is the main landmark.',
850
+ nodeIds: slots.map((slot)=>slot.id)
851
+ });
852
+ }
853
+ }
854
+ if (outputKind === 'email' || outputKind === 'form') return violations;
855
+ const headings = visits.map((visit)=>({
856
+ id: visit.id,
857
+ level: aiHeadingLevel(visit.node)
858
+ })).filter((entry)=>entry.level !== null);
859
+ const h1s = headings.filter((heading)=>heading.level === 1).map((heading)=>heading.id);
860
+ const page = outputKind === 'page' || outputKind === 'template';
861
+ if (page && h1s.length !== 1) {
862
+ violations.push({
863
+ rule: 11,
864
+ code: h1s.length ? 'multiple-h1' : 'missing-h1',
865
+ message: h1s.length ? `This page has ${h1s.length} top-level headings. A page has one h1; make the others h2.` : "This page has no top-level heading. Give the page's title one h1.",
866
+ nodeIds: h1s
867
+ });
868
+ } else if (!page && h1s.length > (outputKind === 'layout' ? 0 : 1)) {
869
+ violations.push({
870
+ rule: 11,
871
+ code: 'multiple-h1',
872
+ message: outputKind === 'layout' ? 'A layout wraps pages that bring their own h1, so it carries none.' : 'A component carries at most one h1.',
873
+ nodeIds: h1s
874
+ });
875
+ }
876
+ const skipped = [];
877
+ let previous = page ? 0 : ((_ref = (_headings_ = headings[0]) == null ? void 0 : _headings_.level) != null ? _ref : 1) - 1;
878
+ for (const heading of headings){
879
+ if (heading.level > previous + 1) skipped.push(heading.id);
880
+ previous = heading.level;
881
+ }
882
+ if (skipped.length) {
883
+ violations.push({
884
+ rule: 11,
885
+ code: 'skipped-heading',
886
+ message: 'The heading outline skips a level. Step down one level at a time — an h2 before any h3.',
887
+ nodeIds: skipped
888
+ });
889
+ }
890
+ return violations;
891
+ }
892
+ const WIDTH_SX_KEYS = new Set([
893
+ 'width',
894
+ 'minWidth',
895
+ 'maxWidth'
896
+ ]);
897
+ /**
898
+ * Rule 12. Widths follow the theme's breakpoints and the palette's
899
+ * responsive props — a Container's max width, a Grid's span — never a fixed
900
+ * pixel or viewport width that holds at one screen size. An email's column
901
+ * is fixed by the medium, and is exempt.
902
+ */ export function detectAdHocWidths(tree, outputKind) {
903
+ if (outputKind === 'email') return [];
904
+ const fixed = [];
905
+ for (const { id, node } of walkTree(tree)){
906
+ var _node_sx;
907
+ for (const [key, raw] of Object.entries((_node_sx = node.sx) != null ? _node_sx : {})){
908
+ if (!WIDTH_SX_KEYS.has(key)) continue;
909
+ for (const value of sxValues(raw)){
910
+ // MUI reads a number up to 1 as a fraction of the parent.
911
+ if (typeof value === 'number' && value > 1 || typeof value === 'string' && LENGTH_WITH_UNIT.test(value)) {
912
+ fixed.push(id);
913
+ }
914
+ }
915
+ }
916
+ }
917
+ return fixed.length ? [
918
+ {
919
+ rule: 12,
920
+ code: 'fixed-width',
921
+ message: "This sets fixed widths. Size elements with the theme's breakpoints — a Container's max width, a Grid span, or a percentage.",
922
+ nodeIds: unique(fixed)
923
+ }
924
+ ] : [];
925
+ }
926
+ const GRID = 'muiGrid';
927
+ /** A Grid's props that only a container reads; on an item they do nothing. */ const GRID_CONTAINER_PROPS = [
928
+ 'direction',
929
+ 'wrap',
930
+ 'spacing',
931
+ 'rowSpacing',
932
+ 'columnSpacing',
933
+ 'columns'
934
+ ];
935
+ /** A container's props that set what it holds side by side, which a group that only stacks never needs. */ const GRID_ROW_PROPS = [
936
+ 'wrap',
937
+ 'columns',
938
+ 'columnSpacing'
939
+ ];
940
+ /** The directions the palette keeps for a Grid, which lays out rows. */ const GRID_ROW_DIRECTIONS = [
941
+ 'row',
942
+ 'row-reverse'
943
+ ];
944
+ /** A direction a Stack has and a Grid does not; the palette validator drops it from a Grid. */ const GRID_COLUMN_DIRECTION = /^column(?:-reverse)?$/i;
945
+ /** The elements an answer wraps a container's items in, where they belong in the container itself. */ const GRID_ITEM_WRAPPERS = new Set([
946
+ 'muiBox',
947
+ 'muiStack'
948
+ ]);
949
+ /** `sx` keys that space a container's columns outside the widths its items are sized by. */ const GRID_GAP_SX_KEYS = [
950
+ 'gap',
951
+ 'columnGap'
952
+ ];
953
+ /** The columns a container divides a row into when it names none. */ const GRID_DEFAULT_COLUMNS = 12;
954
+ /** The most characters of a written value a re-ask quotes back. */ const GRID_QUOTED_MAX_CHARS = 24;
955
+ /** A value as the model wrote it, as a re-ask quotes it: text in quotes, anything else as JSON, cut short. */ function writtenValueText(value) {
956
+ var _JSON_stringify;
957
+ const text = typeof value === 'string' ? value : (_JSON_stringify = JSON.stringify(value)) != null ? _JSON_stringify : String(value);
958
+ const cut = text.length > GRID_QUOTED_MAX_CHARS ? `${text.slice(0, GRID_QUOTED_MAX_CHARS)}…` : text;
959
+ return typeof value === 'string' ? `the text "${cut}"` : cut;
960
+ }
961
+ /** A Grid's columns: its own `columns` when it names a count, else the renderer's twelve. */ function gridColumns(node) {
962
+ var _node_props;
963
+ const columns = Number((_node_props = node.props) == null ? void 0 : _node_props['columns']);
964
+ return Number.isInteger(columns) && columns > 0 ? columns : GRID_DEFAULT_COLUMNS;
965
+ }
966
+ /**
967
+ * Whether a Grid item's size is one the renderer reads, full width on a phone,
968
+ * and whether it steps down to a column at a larger width. The size is the
969
+ * stored string the besigner's breakpoint row writes and the Grid renderer
970
+ * parses (`parseBreakpointSpan`): a bare span at every width, or pairs such as
971
+ * `xs:12 md:4`.
972
+ */ function gridItemSpan(size, columns) {
973
+ var _span_values;
974
+ if (typeof size !== 'string' && typeof size !== 'number') return {
975
+ phoneFull: false,
976
+ steps: false
977
+ };
978
+ const span = parseBreakpointSpan(size);
979
+ if (span.raw !== undefined) return {
980
+ phoneFull: false,
981
+ steps: false
982
+ };
983
+ if (span.base !== undefined) return {
984
+ phoneFull: span.base === columns,
985
+ steps: false
986
+ };
987
+ const values = (_span_values = span.values) != null ? _span_values : {};
988
+ return {
989
+ phoneFull: values.xs === columns,
990
+ steps: Object.entries(values).some(([breakpoint, value])=>breakpoint !== 'xs' && value !== columns)
991
+ };
992
+ }
993
+ /** The size a container's column is asked for: full width on a phone, a row of up to four from md. */ function gridItemSizeFor(items, columns) {
994
+ const across = Math.min(Math.max(items, 2), 4);
995
+ const md = Math.max(1, Math.floor(columns / (items > 4 ? 3 : across)));
996
+ const sm = Math.floor(columns / 2);
997
+ return items >= 4 && sm !== md ? `xs:${columns} sm:${sm} md:${md}` : `xs:${columns} md:${md}`;
998
+ }
999
+ /**
1000
+ * Rule 12, for a Grid (AGL-3055). The palette's Grid is one element in both
1001
+ * roles: a CONTAINER lays its direct Grid children out in columns, and an ITEM
1002
+ * takes a size. The sizes are fractions of the container's columns, so an
1003
+ * item under a Grid that is not a container is sized against nothing, and a
1004
+ * row of them stacks one under another at every width; a size that holds at
1005
+ * every width keeps a phone's columns as narrow as a desktop's; and an `sx`
1006
+ * gap on a container spaces the columns outside the widths the items were
1007
+ * sized by, so the last column wraps onto a row of its own. A live About
1008
+ * page's practice areas were the first: a Grid with no container holding three
1009
+ * items sized "4", one column at every width.
1010
+ *
1011
+ * A Grid that is not a container is told what to do by what it was written as
1012
+ * (AGL-3078), because one sentence does not mend every shape: a live About
1013
+ * page's section was refused twice for rule 12, with a re-ask that only ever
1014
+ * said to make the Grid a container of sized columns.
1015
+ *
1016
+ * - `grid-not-container`: a row that is not a container. A Grid that holds
1017
+ * sized Grid items, sets a prop only a row reads, or holds two or more
1018
+ * elements of one shape side by side. The rule's intent: a row of cards is a
1019
+ * container of sized items.
1020
+ * - `grid-as-stack`: any other Grid that is not a container and sets a prop
1021
+ * only a container reads, or holds two or more elements without being an
1022
+ * item: a group that only stacks, such as a heading over its text, or one
1023
+ * written with a column direction, which a Grid does not have. The re-ask
1024
+ * offers a Stack or a Box for the group, or a container of sized items.
1025
+ * - `grid-item-outside-container`: a sized Grid item that sits in another
1026
+ * element, such as a Box inside its container. A container sizes only the
1027
+ * items directly in it, so the re-ask asks to move the item there. A Box or a
1028
+ * Stack in a container that holds nothing but sized items is refused through
1029
+ * its items, not as a child of the container that is no item.
1030
+ * - `grid-container-text`: a Grid whose `container` was written as a value the
1031
+ * palette validator cannot read as true or false, such as the text "True". The
1032
+ * re-ask names the value as the fault.
1033
+ * - `grid-item-size`: a container's child that is not a Grid item sized full
1034
+ * width on a phone, or a container of several whose items never step down
1035
+ * to columns at a larger width.
1036
+ * - `grid-gap`: a container spaced by an `sx` gap rather than its spacing.
1037
+ *
1038
+ * Each re-ask names the Grid or its items by the model's own ids and says what
1039
+ * to write, in the size format the renderer reads. `written` is the node as
1040
+ * the model wrote it, by an id of `tree`, where that is known: the palette
1041
+ * validator reads a `container` it cannot parse, and a column direction, as
1042
+ * absent, and only what was written can name them.
1043
+ */ export function detectUnresponsiveGrids(tree, outputKind, written = ()=>undefined) {
1044
+ if (outputKind === 'email' || outputKind === 'form') return [];
1045
+ // Shapes are indexed only once a Grid needs its children compared.
1046
+ let shapes = null;
1047
+ const shapeOf = (id)=>(shapes != null ? shapes : shapes = indexShapes(tree)).shapeOf.get(id);
1048
+ const childrenOf = (id)=>{
1049
+ var _ref;
1050
+ var _tree_nodes_id;
1051
+ return ((_ref = (_tree_nodes_id = tree.nodes[id]) == null ? void 0 : _tree_nodes_id.nodes) != null ? _ref : []).filter((child)=>tree.nodes[child]);
1052
+ };
1053
+ const isGrid = (id)=>{
1054
+ var _tree_nodes_id;
1055
+ return id !== undefined && ((_tree_nodes_id = tree.nodes[id]) == null ? void 0 : _tree_nodes_id.componentId) === GRID;
1056
+ };
1057
+ const isContainer = (id)=>{
1058
+ var _tree_nodes_id_props;
1059
+ return isGrid(id) && ((_tree_nodes_id_props = tree.nodes[id].props) == null ? void 0 : _tree_nodes_id_props['container']) === true;
1060
+ };
1061
+ const isSized = (id)=>{
1062
+ var _tree_nodes_id_props;
1063
+ return isGrid(id) && ((_tree_nodes_id_props = tree.nodes[id].props) == null ? void 0 : _tree_nodes_id_props['size']) !== undefined;
1064
+ };
1065
+ const writtenProp = (id, name)=>{
1066
+ const node = written(id);
1067
+ return isRecord(node) && isRecord(node['props']) ? node['props'][name] : undefined;
1068
+ };
1069
+ /** A `container` the model wrote that the palette validator could not read as true or false. */ const unreadContainer = (id)=>{
1070
+ var _tree_nodes_id_props;
1071
+ const value = writtenProp(id, 'container');
1072
+ return ((_tree_nodes_id_props = tree.nodes[id].props) == null ? void 0 : _tree_nodes_id_props['container']) === undefined && value !== null ? value : undefined;
1073
+ };
1074
+ /** A Box or a Stack holding nothing but sized Grid items, which belong directly in a container. */ const wrapsItems = (id)=>{
1075
+ const children = childrenOf(id);
1076
+ return GRID_ITEM_WRAPPERS.has(tree.nodes[id].componentId) && children.length > 0 && children.every(isSized);
1077
+ };
1078
+ /** A direction the model wrote as a column, which the palette validator drops from a Grid. */ const writtenColumn = (id)=>{
1079
+ const direction = writtenProp(id, 'direction');
1080
+ return typeof direction === 'string' && GRID_COLUMN_DIRECTION.test(direction.trim());
1081
+ };
1082
+ /** A Grid set out as a row: a row direction, a prop only a row reads, or elements of one shape. */ const setsOutRow = (id, children)=>{
1083
+ var _tree_nodes_id_props;
1084
+ return GRID_ROW_DIRECTIONS.includes(String((_tree_nodes_id_props = tree.nodes[id].props) == null ? void 0 : _tree_nodes_id_props['direction'])) || GRID_ROW_PROPS.some((name)=>{
1085
+ var _tree_nodes_id_props;
1086
+ return ((_tree_nodes_id_props = tree.nodes[id].props) == null ? void 0 : _tree_nodes_id_props[name]) !== undefined;
1087
+ }) || children.length >= 2 && new Set(children.map(shapeOf)).size === 1;
1088
+ };
1089
+ const refused = new Set();
1090
+ const rows = [];
1091
+ const stacks = [];
1092
+ const outside = [];
1093
+ const texts = [];
1094
+ const unsized = [];
1095
+ const suggested = [];
1096
+ const gapped = [];
1097
+ for (const visit of walkTree(tree)){
1098
+ var _node_sx;
1099
+ var _node_props, _node_props1;
1100
+ const { id, node } = visit;
1101
+ if (node.componentId !== GRID) continue;
1102
+ const children = childrenOf(id);
1103
+ const [parentId, grandparentId] = [
1104
+ ...visit.ancestors
1105
+ ].reverse();
1106
+ const parent = parentId === undefined ? undefined : tree.nodes[parentId];
1107
+ const sized = ((_node_props = node.props) == null ? void 0 : _node_props['size']) !== undefined;
1108
+ // An item whose container sits one element above it: a Box or a Stack holding only items.
1109
+ const wrappedItem = sized && parentId !== undefined && wrapsItems(parentId) && isContainer(grandparentId);
1110
+ if (((_node_props1 = node.props) == null ? void 0 : _node_props1['container']) !== true) {
1111
+ const holdsSized = children.some(isSized);
1112
+ // An item of a container, or of a Grid refused here for not being the container it is written as.
1113
+ const isItem = parentId !== undefined && isGrid(parentId) && (isContainer(parentId) || childrenOf(parentId).some(isSized) || refused.has(parentId) && unreadContainer(parentId) !== undefined);
1114
+ const setsContainerProps = GRID_CONTAINER_PROPS.some((name)=>{
1115
+ var _node_props;
1116
+ return ((_node_props = node.props) == null ? void 0 : _node_props[name]) !== undefined;
1117
+ });
1118
+ if (!holdsSized && !setsContainerProps && (isItem || children.length < 2) && !wrappedItem) continue;
1119
+ refused.add(id);
1120
+ const text = unreadContainer(id);
1121
+ if (text !== undefined) texts.push({
1122
+ id,
1123
+ value: text
1124
+ });
1125
+ else if (holdsSized) rows.push(id);
1126
+ else if (sized && parent && parent.componentId !== GRID) outside.push({
1127
+ id,
1128
+ within: parent.componentId
1129
+ });
1130
+ else if (writtenColumn(id)) stacks.push({
1131
+ id,
1132
+ column: true
1133
+ });
1134
+ else if (setsOutRow(id, children)) rows.push(id);
1135
+ else stacks.push({
1136
+ id,
1137
+ column: false
1138
+ });
1139
+ continue;
1140
+ }
1141
+ if (wrappedItem && parent) outside.push({
1142
+ id,
1143
+ within: parent.componentId
1144
+ });
1145
+ const columns = gridColumns(node);
1146
+ // The items a Box or a Stack holds for it are sized against it, and refused where they sit.
1147
+ const items = children.flatMap((child)=>wrapsItems(child) ? childrenOf(child) : [
1148
+ child
1149
+ ]);
1150
+ let steps = false;
1151
+ const offending = [];
1152
+ for (const child of items){
1153
+ var _item_props;
1154
+ const item = tree.nodes[child];
1155
+ const span = item.componentId === GRID ? gridItemSpan((_item_props = item.props) == null ? void 0 : _item_props['size'], columns) : null;
1156
+ if (!(span == null ? void 0 : span.phoneFull)) offending.push(child);
1157
+ if (span == null ? void 0 : span.steps) steps = true;
1158
+ }
1159
+ if (offending.length) {
1160
+ unsized.push(...offending);
1161
+ } else if (items.length >= 2 && !steps) {
1162
+ unsized.push(...items);
1163
+ }
1164
+ if (offending.length || items.length >= 2 && !steps) {
1165
+ suggested.push(gridItemSizeFor(items.length, columns));
1166
+ }
1167
+ const sx = (_node_sx = node.sx) != null ? _node_sx : {};
1168
+ const gap = GRID_GAP_SX_KEYS.find((key)=>sx[key] !== undefined);
1169
+ if (gap) gapped.push({
1170
+ id,
1171
+ spacing: sx[gap]
1172
+ });
1173
+ }
1174
+ const violations = [];
1175
+ if (rows.length) {
1176
+ violations.push({
1177
+ rule: 12,
1178
+ code: 'grid-not-container',
1179
+ message: 'A Grid lays out columns only as a container: this one is not, so what it holds stacks at every width. Set "container": true on it, and put each column in a Grid item sized like "xs:12 md:4".',
1180
+ nodeIds: unique(rows)
1181
+ });
1182
+ }
1183
+ if (stacks.length) {
1184
+ violations.push({
1185
+ rule: 12,
1186
+ code: 'grid-as-stack',
1187
+ message: `${stacks[0].column ? 'A Grid lays out rows and has no "column" direction, so this one only stacks what it holds.' : 'A Grid that is not a container lays nothing out, so this one only stacks what it holds.'} Use a Stack (or a Box) for a group that only stacks, such as a heading over its text, or make it a container of sized items: set "container": true on it, and put each column in a Grid item sized like "xs:12 md:4".`,
1188
+ nodeIds: unique(stacks.map((entry)=>entry.id))
1189
+ });
1190
+ }
1191
+ if (outside.length) {
1192
+ const within = displayName(outside[0].within);
1193
+ violations.push({
1194
+ rule: 12,
1195
+ code: 'grid-item-outside-container',
1196
+ message: `A Grid item is sized only by the Grid container it sits directly in, and this one sits in a ${within}, so its size does nothing and it stacks at every width. Move it directly under its Grid container, or, where it has none, put it and the items beside it in one ("container": true).`,
1197
+ nodeIds: unique(outside.map((entry)=>entry.id))
1198
+ });
1199
+ }
1200
+ if (texts.length) {
1201
+ const [first] = texts;
1202
+ violations.push({
1203
+ rule: 12,
1204
+ code: 'grid-container-text',
1205
+ message: `This Grid's "container" is ${writtenValueText(first.value)}, not true, so it is not a container and what it holds stacks at every width. Write "container": true${typeof first.value === 'string' ? ', with no quotes around true' : ''}.`,
1206
+ nodeIds: unique(texts.map((entry)=>entry.id))
1207
+ });
1208
+ }
1209
+ if (unsized.length) {
1210
+ violations.push({
1211
+ rule: 12,
1212
+ code: 'grid-item-size',
1213
+ message: `Every child of a Grid container is a Grid item whose size is full width on a phone and steps up to columns at a larger width, written as one string. Size these like "${suggested[0]}", and wrap any other element in such an item.`,
1214
+ nodeIds: unique(unsized)
1215
+ });
1216
+ }
1217
+ if (gapped.length) {
1218
+ const [first] = gapped;
1219
+ const spacing = typeof first.spacing === 'number' ? first.spacing : 2;
1220
+ violations.push({
1221
+ rule: 12,
1222
+ code: 'grid-gap',
1223
+ message: `A Grid container's items are sized by its "spacing", so an sx gap pushes its last column onto a row of its own. Remove the sx gap and set "spacing": ${spacing}.`,
1224
+ nodeIds: gapped.map((entry)=>entry.id)
1225
+ });
1226
+ }
1227
+ return violations;
1228
+ }
1229
+ /** The palette colors a link or a button can take, and a band can be painted in. */ const LINK_COLOR_FAMILIES = new Set([
1230
+ 'primary',
1231
+ 'secondary',
1232
+ 'success',
1233
+ 'error',
1234
+ 'info',
1235
+ 'warning'
1236
+ ]);
1237
+ /** The elements a page links with; each draws its words in its `color` unless told otherwise. */ const LINK_COMPONENTS = new Set([
1238
+ 'muiButton',
1239
+ 'muiScreenLink'
1240
+ ]);
1241
+ /** Whether an element is one rule 10 holds to a destination: a Button or a Screen Link. */ export function isAiLinkElement(componentId) {
1242
+ return typeof componentId === 'string' && LINK_COMPONENTS.has(componentId);
1243
+ }
1244
+ /** Surfaces that paint their own background, whatever band they sit on. */ const PAPER_COMPONENTS = new Set([
1245
+ 'muiCard',
1246
+ 'muiPaper',
1247
+ 'muiAccordion',
1248
+ 'muiDrawer'
1249
+ ]);
1250
+ /** A palette color token of a link family: `primary.main`, `secondary.dark`. */ const FAMILY_TOKEN = /^([a-z]+)\.(?:main|dark|light)$/;
1251
+ /** The link family a color value names, if it names one. */ function familyOf(value) {
1252
+ var _FAMILY_TOKEN_exec;
1253
+ const family = typeof value === 'string' ? (_FAMILY_TOKEN_exec = FAMILY_TOKEN.exec(value.trim())) == null ? void 0 : _FAMILY_TOKEN_exec[1] : undefined;
1254
+ return family && LINK_COLOR_FAMILIES.has(family) ? family : null;
1255
+ }
1256
+ /**
1257
+ * The band a node sits on: the nearest ancestor that paints a background. An
1258
+ * `sx` background in a link family's color is a band of that family, and an
1259
+ * App Bar is one in its `color`, which is primary when it names none. Any
1260
+ * other background, and every paper surface, is not.
1261
+ */ function bandOf(tree, visit) {
1262
+ for (const id of [
1263
+ ...visit.ancestors
1264
+ ].reverse()){
1265
+ var _ref;
1266
+ var _node_sx, _node_sx1;
1267
+ const node = tree.nodes[id];
1268
+ if (!node) continue;
1269
+ const background = (_ref = (_node_sx = node.sx) == null ? void 0 : _node_sx['bgcolor']) != null ? _ref : (_node_sx1 = node.sx) == null ? void 0 : _node_sx1['backgroundColor'];
1270
+ if (background !== undefined) {
1271
+ const token = sxValues(background).find((value)=>familyOf(value) !== null);
1272
+ return typeof token === 'string' ? {
1273
+ family: familyOf(token),
1274
+ token: token.trim()
1275
+ } : null;
1276
+ }
1277
+ if (node.componentId === 'muiAppBar') {
1278
+ var _node_props;
1279
+ const color = typeof ((_node_props = node.props) == null ? void 0 : _node_props['color']) === 'string' ? node.props['color'] : 'primary';
1280
+ return LINK_COLOR_FAMILIES.has(color) ? {
1281
+ family: color,
1282
+ token: `${color}.main`
1283
+ } : null;
1284
+ }
1285
+ if (PAPER_COMPONENTS.has(node.componentId)) return null;
1286
+ }
1287
+ return null;
1288
+ }
1289
+ /**
1290
+ * The palette family a link or a button draws its words in: its own `sx`
1291
+ * color when it sets one, else its `color` (primary when it names none). A
1292
+ * link told to inherit takes the band's own, and a contained button draws its
1293
+ * label in the family's contrast text on a fill of its own.
1294
+ */ function linkWordsFamily(node) {
1295
+ var _node_sx, _node_props, _node_props1, _node_props2;
1296
+ const own = (_node_sx = node.sx) == null ? void 0 : _node_sx['color'];
1297
+ if (own !== undefined) {
1298
+ var _sxValues_map_find;
1299
+ return (_sxValues_map_find = sxValues(own).map(familyOf).find((family)=>family !== null)) != null ? _sxValues_map_find : null;
1300
+ }
1301
+ const color = typeof ((_node_props = node.props) == null ? void 0 : _node_props['color']) === 'string' ? node.props['color'] : 'primary';
1302
+ if (!LINK_COLOR_FAMILIES.has(color)) return null;
1303
+ const styledAsButton = node.componentId === 'muiButton' || ((_node_props1 = node.props) == null ? void 0 : _node_props1['renderAs']) !== 'link';
1304
+ return styledAsButton && ((_node_props2 = node.props) == null ? void 0 : _node_props2['variant']) === 'contained' ? null : color;
1305
+ }
1306
+ /**
1307
+ * Rule 5, for a link on a colored band (AGL-3056). The doctrine asks for
1308
+ * palette tokens, and a band in `primary.main` holding a Screen Link that
1309
+ * sets no color is two valid tokens: but the link keeps the theme's primary
1310
+ * color, so its words are navy on navy. A live About page's footer was the
1311
+ * first, a "Request a Consultation" link on a primary band. A link or a button
1312
+ * whose words are drawn in the family of the band it sits on is refused, with
1313
+ * a re-ask to inherit the band's contrast text or to set it.
1314
+ */ export function detectInvisibleLinks(tree, outputKind) {
1315
+ if (outputKind === 'email' || outputKind === 'form') return [];
1316
+ const hidden = [];
1317
+ for (const visit of walkTree(tree)){
1318
+ if (!LINK_COMPONENTS.has(visit.node.componentId)) continue;
1319
+ const family = linkWordsFamily(visit.node);
1320
+ const band = family ? bandOf(tree, visit) : null;
1321
+ if (band && band.family === family) hidden.push(_extends({
1322
+ id: visit.id
1323
+ }, band));
1324
+ }
1325
+ if (!hidden.length) return [];
1326
+ const [first] = hidden;
1327
+ return [
1328
+ {
1329
+ rule: 5,
1330
+ code: 'link-color-on-band',
1331
+ message: `A link or button on a ${first.token} band draws its words in the theme's ${first.family} color, the band's own, so they cannot be read. Give it "color": "inherit" under a band whose sx color is ${first.family}.contrastText, or set its own sx color to ${first.family}.contrastText.`,
1332
+ nodeIds: unique(hidden.map((entry)=>entry.id))
1333
+ }
1334
+ ];
1335
+ }
1336
+ /** A link's words that say it goes home. */ const HOME_WORDS = /\bhome(?:\s?page)?\b/i;
1337
+ /**
1338
+ * Rule 10, for a link sent where the site has nothing for it (AGL-3056). A
1339
+ * link whose purpose the site has no screen for is left out; it never goes to
1340
+ * a screen that does something else. The site inventory cannot say what each
1341
+ * screen is for, but it can name the one a model falls back to: a live About
1342
+ * page's footer sent "Request a Consultation" to the home screen, because the
1343
+ * site had no consultation screen. So a Screen Link or a Button, outside the
1344
+ * header and the navigation, that links the home screen with words that do
1345
+ * not say home is refused, with a re-ask to link the screen that does what it
1346
+ * says, or to leave the link out.
1347
+ */ export function detectUnrelatedScreenLinks(tree, outputKind, context = {}) {
1348
+ var _context_homeScreenIds;
1349
+ if (outputKind === 'email' || outputKind === 'form') return [];
1350
+ const home = new Set((_context_homeScreenIds = context.homeScreenIds) != null ? _context_homeScreenIds : []);
1351
+ const sent = [];
1352
+ for (const visit of walkTree(tree)){
1353
+ var _node_props, _node_props1, _node_props2;
1354
+ const { node } = visit;
1355
+ if (!LINK_COMPONENTS.has(node.componentId)) continue;
1356
+ const screenId = (_node_props = node.props) == null ? void 0 : _node_props['screenId'];
1357
+ const linksHome = typeof screenId === 'string' && home.has(screenId) || ((_node_props1 = node.props) == null ? void 0 : _node_props1['href']) === '/';
1358
+ const label = typeof ((_node_props2 = node.props) == null ? void 0 : _node_props2['children']) === 'string' ? node.props['children'].trim() : '';
1359
+ if (!linksHome || !label || HOME_WORDS.test(label)) continue;
1360
+ // The header and the navigation link home by name and by the site's own brand.
1361
+ const inNavigation = hasAncestor(tree, visit, (ancestor)=>ancestor.componentId === 'muiAppBar' || [
1362
+ 'header',
1363
+ 'nav'
1364
+ ].includes(elementOf(ancestor)));
1365
+ if (!inNavigation) sent.push({
1366
+ id: visit.id,
1367
+ label
1368
+ });
1369
+ }
1370
+ if (!sent.length) return [];
1371
+ return [
1372
+ {
1373
+ rule: 10,
1374
+ code: 'link-unrelated-screen',
1375
+ message: `"${sent[0].label}" links the home page, which does not do what its words say. Link the screen that does, or leave the link out when the site has none.`,
1376
+ nodeIds: unique(sent.map((entry)=>entry.id))
1377
+ }
1378
+ ];
1379
+ }
1380
+ /** A prop value with something in it: a destination, or words. */ function isFilled(value) {
1381
+ return typeof value === 'string' && value.trim() !== '';
1382
+ }
1383
+ /**
1384
+ * Rule 10, for a link that goes nowhere (AGL-3072). A Button or a Screen Link
1385
+ * goes where its `screenId` or its `href` says, and those are the only
1386
+ * destinations either element carries: the palette validator keeps a
1387
+ * `screenId` the site has, and an `href` that is a path on the site, an
1388
+ * `https:` address or a binding the caller admitted. Nothing else can stand
1389
+ * in for one. A form is sent by the button the Form draws from its own
1390
+ * `submitLabel`, the elements a page places carry no id an anchor could name
1391
+ * (AGL-2867), and a generated node sets no interaction. So a link with
1392
+ * neither is a dead control: a live About page's hero carried a "Request a
1393
+ * Consultation" button with no destination. It is refused, with a re-ask
1394
+ * naming its words and what it may link.
1395
+ *
1396
+ * On a page a plan lays out (`context.pageSections`), a link may also go to a
1397
+ * section of the same page (AGL-3097). A live hero's re-ask was answered with
1398
+ * "#consultation-form", an anchor to the form the plan places last, and the
1399
+ * page stopped at its first section. A link that names a section, by
1400
+ * `scrollTo` or by a fragment that section's name's words make up, goes
1401
+ * there, and the page step writes the platform's Scroll to element
1402
+ * interaction for it. On a page as stored (`context.scrollTargetIds`), a link
1403
+ * carrying that interaction to one of its section roots goes there. Two
1404
+ * faults get a sentence of their own:
1405
+ *
1406
+ * - `link-fragment`: an `href` fragment that names no section. The palette
1407
+ * validator drops it, so what the link was written with goes nowhere.
1408
+ * - `scroll-target-unknown`: a `scrollTo` naming no section of the page.
1409
+ *
1410
+ * `written` is the node as the model wrote it and `stored` the node as the
1411
+ * check was given it, each by an id of `tree`.
1412
+ */ export function detectLinksWithoutDestination(tree, outputKind, context = {}, written = ()=>undefined, stored = ()=>undefined) {
1413
+ var _context_pageSections, _context_scrollTargetIds;
1414
+ if (outputKind === 'email' || outputKind === 'form') return [];
1415
+ const sections = (_context_pageSections = context.pageSections) != null ? _context_pageSections : [];
1416
+ const targets = new Set((_context_scrollTargetIds = context.scrollTargetIds) != null ? _context_scrollTargetIds : []);
1417
+ const dead = [];
1418
+ const fragments = [];
1419
+ const unknown = [];
1420
+ for (const visit of walkTree(tree)){
1421
+ var _node_props, _node_props1, _node_props2;
1422
+ const { node } = visit;
1423
+ if (!LINK_COMPONENTS.has(node.componentId)) continue;
1424
+ if (isFilled((_node_props = node.props) == null ? void 0 : _node_props['screenId']) || isFilled((_node_props1 = node.props) == null ? void 0 : _node_props1['href'])) continue;
1425
+ if (aiStoredScrollTargets(stored(visit.id)).some((target)=>targets.has(target))) continue;
1426
+ const label = typeof ((_node_props2 = node.props) == null ? void 0 : _node_props2['children']) === 'string' ? node.props['children'].trim() : '';
1427
+ const target = aiPageLinkTarget(written(visit.id), sections);
1428
+ if ((target == null ? void 0 : target.kind) === 'section') continue;
1429
+ if ((target == null ? void 0 : target.kind) === 'unknown' && target.via === 'href') {
1430
+ fragments.push({
1431
+ id: visit.id,
1432
+ label,
1433
+ value: target.value
1434
+ });
1435
+ } else if ((target == null ? void 0 : target.kind) === 'unknown' && context.pageSections) {
1436
+ unknown.push({
1437
+ id: visit.id,
1438
+ label,
1439
+ value: target.value
1440
+ });
1441
+ } else {
1442
+ dead.push({
1443
+ id: visit.id,
1444
+ label,
1445
+ inForm: hasAncestor(tree, visit, (ancestor)=>ancestor.componentId === 'form')
1446
+ });
1447
+ }
1448
+ }
1449
+ const wordsOf = (entry)=>entry.label ? `"${entry.label}"` : `A ${displayName(tree.nodes[entry.id].componentId)}`;
1450
+ const names = sections.map((name)=>`"${name}"`);
1451
+ const listed = names.length > 1 ? `${names.slice(0, -1).join(', ')} or ${names[names.length - 1]}` : names.join('');
1452
+ const toSection = `To take a visitor to a section of this page, set "${AI_PAGE_SCROLL_TO_PROP}" to that section's name in the plan: ${listed}.`;
1453
+ const violations = [];
1454
+ if (fragments.length) {
1455
+ const [first] = fragments;
1456
+ violations.push({
1457
+ rule: 10,
1458
+ code: 'link-fragment',
1459
+ message: `${wordsOf(first)} links "${first.value}", an anchor, and no element on a page carries an id an anchor could name, so it goes nowhere. ${context.pageSections ? `${toSection} Otherwise give it the "screenId" of a screen the site has that does what its words say, or take it out.` : 'Give it the "screenId" of a screen the site has that does what its words say, or an "href" that is a path on this site or an https: address the brief gives, or take it out.'}`,
1460
+ nodeIds: unique(fragments.map((entry)=>entry.id))
1461
+ });
1462
+ }
1463
+ if (unknown.length) {
1464
+ const [first] = unknown;
1465
+ violations.push({
1466
+ rule: 10,
1467
+ code: 'scroll-target-unknown',
1468
+ message: `${wordsOf(first)} scrolls to "${first.value}", and this page has no section of that name. Set "${AI_PAGE_SCROLL_TO_PROP}" to one of its sections' names in the plan: ${listed}, or take the link out.`,
1469
+ nodeIds: unique(unknown.map((entry)=>entry.id))
1470
+ });
1471
+ }
1472
+ if (dead.length) {
1473
+ const [first] = dead;
1474
+ const instead = dead.some((entry)=>entry.inForm) ? ' A Form draws its own send button from its "submitLabel", so take out a button drawn inside one and set that label instead.' : context.pageSections ? ` ${toSection} When none of these fits, take it out.` : ' When the site has no page for it, take it out: a form on this page is sent by its own button, and no element can be reached by an anchor.';
1475
+ violations.push({
1476
+ rule: 10,
1477
+ code: 'link-without-destination',
1478
+ message: `${wordsOf(first)} goes nowhere. Give it the "screenId" of a screen the site has that does what its words say, or an "href" that is a path on this site or an https: address the brief gives.${instead}`,
1479
+ nodeIds: unique(dead.map((entry)=>entry.id))
1480
+ });
1481
+ }
1482
+ return violations;
1483
+ }
1484
+ /** Answer fields that say "publish", wherever the answer put them. */ const PUBLISH_KEYS = /^(?:publish|published|publishNow|publishAt|goLive|isLive|live|makeLive)$/i;
1485
+ /**
1486
+ * Rule 13. The AI writes drafts. An answer that asks for anything to be
1487
+ * published, or to go live, is refused whatever else it got right; nothing
1488
+ * downstream reads such a field, and the refusal is what keeps a future
1489
+ * reader from starting to.
1490
+ */ export function detectPublishIntent(answer) {
1491
+ const paths = [];
1492
+ const visit = (value, path, depth)=>{
1493
+ if (depth > 12 || paths.length >= 5) return;
1494
+ if (Array.isArray(value)) {
1495
+ value.forEach((inner, index)=>visit(inner, `${path}[${index}]`, depth + 1));
1496
+ return;
1497
+ }
1498
+ if (!isRecord(value)) return;
1499
+ for (const [key, inner] of Object.entries(value)){
1500
+ const here = path ? `${path}.${key}` : key;
1501
+ // An element's props and styles are page content, not instructions.
1502
+ if (key === 'props' || key === 'sx') continue;
1503
+ if (PUBLISH_KEYS.test(key) && inner !== false && inner !== null && inner !== undefined && inner !== '') {
1504
+ paths.push(here);
1505
+ } else if (key === 'status' && typeof inner === 'string' && /^(?:published|live|public)$/i.test(inner.trim())) {
1506
+ paths.push(here);
1507
+ } else {
1508
+ visit(inner, here, depth + 1);
1509
+ }
1510
+ }
1511
+ };
1512
+ visit(answer, '', 0);
1513
+ return paths.length ? [
1514
+ {
1515
+ rule: 13,
1516
+ code: 'publish-intent',
1517
+ message: 'The answer asked to publish. Everything the AI builds is a new draft; review it and publish it yourself.',
1518
+ paths
1519
+ }
1520
+ ] : [];
1521
+ }
1522
+ /** A fact the brief never gave, marked in square brackets: `[Office phone number]`. */ const BRACKETED_FACT = /\[([^[\]{}\n]{1,80})\]/g;
1523
+ /**
1524
+ * The facts in square brackets copy holds (rule 14), each once whatever its
1525
+ * case, in the order they first appear, as the copy spells them
1526
+ * (AGL-3056): what a member fills in before a draft is published.
1527
+ */ export function aiBracketedFacts(texts) {
1528
+ const facts = new Map();
1529
+ for (const text of texts){
1530
+ for (const match of text.matchAll(BRACKETED_FACT)){
1531
+ const fact = match[1].trim();
1532
+ if (fact && !facts.has(fact.toLowerCase())) facts.set(fact.toLowerCase(), `[${fact}]`);
1533
+ }
1534
+ }
1535
+ return [
1536
+ ...facts.values()
1537
+ ];
1538
+ }
1539
+ /**
1540
+ * Filler that stands in for copy. A bracketed marker for a fact the brief
1541
+ * never gave — `[phone number]` — is deliberately NOT here: it is the honest
1542
+ * alternative to inventing the fact, and refusing it would push the re-ask
1543
+ * toward a made-up number.
1544
+ */ const FILLER_COPY = /\blorem\b|\bipsum\b|\bdolor sit\b|\b(?:your|insert|add) (?:text|copy|content|headline|title|description|tagline) here\b|\b(?:placeholder|sample|dummy|filler) (?:text|copy|content)\b|\b(?:text|copy|content) goes here\b/i;
1545
+ const JUDGMENT = /\b(?:simply|obviously|no-brainer|cheap|effortless(?:ly)?|dead simple|super easy|just (?:a|one) click)\b/i;
1546
+ /**
1547
+ * Rule 14. Copy in the site's voice: no lorem ipsum or "your text here"
1548
+ * filler, and — for copy framed for agencies and enterprises — no words that
1549
+ * judge the reader's work as simple or cheap.
1550
+ */ export function detectOffVoiceCopy(samples, framing = null) {
1551
+ const filler = samples.filter((sample)=>FILLER_COPY.test(sample.text));
1552
+ const judged = framing === 'agency' || framing === 'enterprise' ? samples.filter((sample)=>JUDGMENT.test(sample.text)) : [];
1553
+ const at = (list)=>{
1554
+ const ids = unique(list.map((sample)=>sample.at));
1555
+ const paths = ids.filter((id)=>/[.[\]]/.test(id));
1556
+ const nodeIds = ids.filter((id)=>!/[.[\]]/.test(id));
1557
+ return _extends({}, nodeIds.length ? {
1558
+ nodeIds
1559
+ } : {}, paths.length ? {
1560
+ paths
1561
+ } : {});
1562
+ };
1563
+ const violations = [];
1564
+ if (filler.length) {
1565
+ violations.push(_extends({
1566
+ rule: 14,
1567
+ code: 'filler-copy',
1568
+ message: "This leaves filler copy. Write the real words in the site's voice, from what the brief and the site already say."
1569
+ }, at(filler)));
1570
+ }
1571
+ if (judged.length) {
1572
+ violations.push(_extends({
1573
+ rule: 14,
1574
+ code: 'judgment-words',
1575
+ message: 'This copy calls the work simple or cheap, which reads as a judgment to an agency or enterprise audience. Say what it does instead.'
1576
+ }, at(judged)));
1577
+ }
1578
+ return violations;
1579
+ }
1580
+ /** Every piece of copy a tree carries, with the node it sits on. */ export function aiTreeCopy(tree, context = {}) {
1581
+ const samples = [];
1582
+ for (const { id, node } of walkTree(tree)){
1583
+ var _ref, _node_props;
1584
+ var _AI_PALETTE_node_componentId, _node_props1, _node_props2;
1585
+ const roles = (_ref = (_AI_PALETTE_node_componentId = AI_PALETTE[node.componentId]) == null ? void 0 : _AI_PALETTE_node_componentId.propRoles) != null ? _ref : {};
1586
+ for (const [name, value] of Object.entries((_node_props = node.props) != null ? _node_props : {})){
1587
+ if (roles[name] === 'text' && typeof value === 'string' && value.trim()) {
1588
+ samples.push({
1589
+ at: id,
1590
+ text: value
1591
+ });
1592
+ }
1593
+ }
1594
+ const refId = (_node_props1 = node.props) == null ? void 0 : _node_props1[AI_INSTANCE_REF_PROP];
1595
+ const values = (_node_props2 = node.props) == null ? void 0 : _node_props2[REUSABLE_INSTANCE_PROP_VALUES_KEY];
1596
+ if (typeof refId === 'string' && isRecord(values)) {
1597
+ var _ref1;
1598
+ var _context_componentProps;
1599
+ const declared = (_ref1 = (_context_componentProps = context.componentProps) == null ? void 0 : _context_componentProps[refId]) != null ? _ref1 : {};
1600
+ for (const [name, value] of Object.entries(values)){
1601
+ var _declared_name;
1602
+ const type = (_declared_name = declared[name]) != null ? _declared_name : 'text';
1603
+ if ((type === 'text' || type === 'richText') && typeof value === 'string') {
1604
+ samples.push({
1605
+ at: id,
1606
+ text: value
1607
+ });
1608
+ }
1609
+ }
1610
+ }
1611
+ }
1612
+ return samples;
1613
+ }
1614
+ /**
1615
+ * The props a heading, a subhead or a body text shows as a line a reader
1616
+ * reads, by element. A button's or a link's label, a form field's label, a
1617
+ * run of inline text inside a sentence and every accessible name are not
1618
+ * lines, and how they end is not held.
1619
+ */ const LINE_PROPS = {
1620
+ muiTypography: [
1621
+ 'children'
1622
+ ],
1623
+ muiListItemText: [
1624
+ 'primary',
1625
+ 'secondary'
1626
+ ],
1627
+ muiCardHeader: [
1628
+ 'title',
1629
+ 'subheader'
1630
+ ],
1631
+ muiAccordionSummary: [
1632
+ 'children'
1633
+ ],
1634
+ emailText: [
1635
+ 'children'
1636
+ ]
1637
+ };
1638
+ /** Text styles that label rather than say: a caption, an overline, a micro label. */ const LABEL_VARIANTS = new Set([
1639
+ 'caption',
1640
+ 'overline',
1641
+ 'micro'
1642
+ ]);
1643
+ /**
1644
+ * Words no finished line ends on: an article, or a conjunction that joins two
1645
+ * parts. "a" counts only in lowercase, since "Plan A" ends on a name.
1646
+ */ const DANGLING_WORDS = new Set([
1647
+ 'an',
1648
+ 'the',
1649
+ 'and',
1650
+ 'or',
1651
+ 'but',
1652
+ 'nor',
1653
+ '&'
1654
+ ]);
1655
+ /**
1656
+ * Words that open a phrase. A title may end on one it strands, as "What we
1657
+ * help with" does, but a line that ends on one right after a comma or a dash
1658
+ * opened a phrase it never wrote: "…that matter most, with".
1659
+ */ const OPENING_WORDS = new Set([
1660
+ 'about',
1661
+ 'across',
1662
+ 'after',
1663
+ 'against',
1664
+ 'along',
1665
+ 'among',
1666
+ 'as',
1667
+ 'at',
1668
+ 'because',
1669
+ 'before',
1670
+ 'between',
1671
+ 'by',
1672
+ 'for',
1673
+ 'from',
1674
+ 'if',
1675
+ 'in',
1676
+ 'into',
1677
+ 'like',
1678
+ 'of',
1679
+ 'on',
1680
+ 'onto',
1681
+ 'since',
1682
+ 'so',
1683
+ 'than',
1684
+ 'that',
1685
+ 'through',
1686
+ 'to',
1687
+ 'toward',
1688
+ 'towards',
1689
+ 'unless',
1690
+ 'until',
1691
+ 'upon',
1692
+ 'via',
1693
+ 'when',
1694
+ 'where',
1695
+ 'whereas',
1696
+ 'while',
1697
+ 'with',
1698
+ 'within',
1699
+ 'without'
1700
+ ]);
1701
+ /** A line that closes itself: a sentence's end, a closing quote or bracket, or a colon that introduces what follows. */ const CLOSED_LINE = /[.!?…:;"'”’»)\]]$/;
1702
+ /**
1703
+ * The word a line ends on when that word leaves it unfinished (AGL-3072), or
1704
+ * `null`. A line that closes itself, ends on a binding or a bracketed fact the
1705
+ * member fills, or ends on any other word, is finished as far as a check can
1706
+ * tell. Exported for the specs that hold the closed list.
1707
+ */ export function aiDanglingWord(line) {
1708
+ var _exec;
1709
+ const text = line.trim();
1710
+ if (!text || CLOSED_LINE.test(text)) return null;
1711
+ const word = (_exec = /(?:^|[^A-Za-z&'’])([A-Za-z]+|&)$/.exec(text)) == null ? void 0 : _exec[1];
1712
+ if (!word) return null;
1713
+ const lower = word.toLowerCase();
1714
+ if (word === 'a' || DANGLING_WORDS.has(lower)) return word;
1715
+ // A comma, a semicolon or a dash breaks the line before the word; the hyphen
1716
+ // of "Drop-in" or "Add-on" joins one word and breaks nothing.
1717
+ const before = text.slice(0, text.length - word.length);
1718
+ return OPENING_WORDS.has(lower) && /(?:[,;—–]|\s-)\s*$/.test(before) ? word : null;
1719
+ }
1720
+ /** The last words of a line, as a re-ask quotes them. */ function lineTail(line) {
1721
+ const words = line.trim().split(/\s+/);
1722
+ return words.length > 6 ? `…${words.slice(-6).join(' ')}` : words.join(' ');
1723
+ }
1724
+ /**
1725
+ * Rule 14, for a line cut short (AGL-3072). Copy in the site's voice is
1726
+ * finished copy: a heading, a subhead or a body text that ends on an article
1727
+ * or a joining conjunction, or on a word that opens a phrase right after a
1728
+ * comma, with no closing punctuation, reads as a sentence cut off mid-thought.
1729
+ * A live About page's hero subhead ended "…that matter most, with". A closed
1730
+ * list of words, on the lines a reader reads, keeps it deterministic: a
1731
+ * label, a button, a caption and a bracketed fact are never held to it.
1732
+ */ export function detectDanglingWords(tree) {
1733
+ const found = [];
1734
+ for (const { id, node } of walkTree(tree)){
1735
+ var _ref;
1736
+ var _node_props, _node_props1;
1737
+ const props = LINE_PROPS[node.componentId];
1738
+ if (!props) continue;
1739
+ if (node.componentId === 'muiTypography' && LABEL_VARIANTS.has(String((_ref = (_node_props = node.props) == null ? void 0 : _node_props['variant']) != null ? _ref : ''))) continue;
1740
+ if (node.componentId === 'emailText' && ((_node_props1 = node.props) == null ? void 0 : _node_props1['variant']) === 'caption') continue;
1741
+ for (const name of props){
1742
+ var _node_props2;
1743
+ const line = (_node_props2 = node.props) == null ? void 0 : _node_props2[name];
1744
+ const word = typeof line === 'string' ? aiDanglingWord(line) : null;
1745
+ if (word) found.push({
1746
+ id,
1747
+ line: line,
1748
+ word
1749
+ });
1750
+ }
1751
+ }
1752
+ if (!found.length) return [];
1753
+ const [first] = found;
1754
+ return [
1755
+ {
1756
+ rule: 14,
1757
+ code: 'dangling-word',
1758
+ message: `"${lineTail(first.line)}" has no closing punctuation, and its last word, "${first.word}", leaves the sentence unfinished. Finish the sentence, or end the line before "${first.word}".`,
1759
+ nodeIds: unique(found.map((entry)=>entry.id))
1760
+ }
1761
+ ];
1762
+ }
1763
+ /** The labels a button or a link shows, beside the lines: cut, they read as broken too. */ const LABEL_PROPS = {
1764
+ muiButton: [
1765
+ 'children'
1766
+ ],
1767
+ muiScreenLink: [
1768
+ 'children'
1769
+ ],
1770
+ emailButton: [
1771
+ 'children'
1772
+ ]
1773
+ };
1774
+ /** The line the palette validator writes when it cuts a prop at its ceiling. */ const CUT_REPAIR = /^(.+)\.([A-Za-z0-9_]+) was over (\d+) characters; truncated$/;
1775
+ /** The first words of a line, as a re-ask quotes them. */ function lineHead(line) {
1776
+ const words = line.trim().split(/\s+/);
1777
+ return words.length > 8 ? `${words.slice(0, 8).join(' ')}…` : words.join(' ');
1778
+ }
1779
+ /**
1780
+ * Rule 14, for a line cut at its ceiling (AGL-3076). The palette validator
1781
+ * holds copy to a ceiling for its role and cuts what runs past it, and says so
1782
+ * only in its repairs. A cut line is unfinished wherever the cut falls, after
1783
+ * a word or inside one, and a heading style's ceiling of
1784
+ * `AI_TEXT_LIMITS.headline` characters is one the catalog never shows a model:
1785
+ * a live About page's hero subhead, a Typography in the h5 style, was cut at
1786
+ * 120 characters to "…that matter most, with". So a heading, a subhead, a body
1787
+ * line, or a button's or a link's label the validator cut is refused, with a
1788
+ * re-ask naming the ceiling.
1789
+ *
1790
+ * `repairs` are the palette validator's, naming nodes by the ids of the tree
1791
+ * it was given, `written`; `idOf` turns such an id into the id the violation
1792
+ * names.
1793
+ */ export function detectCutLines(repairs, written, idOf = (id)=>id) {
1794
+ const cut = [];
1795
+ for (const repair of repairs){
1796
+ var _ref, _LINE_PROPS_node_componentId;
1797
+ const match = CUT_REPAIR.exec(repair);
1798
+ if (!match) continue;
1799
+ const [, nodeId, name, limit] = match;
1800
+ const node = written[nodeId];
1801
+ if (!isRecord(node) || typeof node['componentId'] !== 'string') continue;
1802
+ const props = isRecord(node['props']) ? node['props'] : {};
1803
+ const shown = (_ref = (_LINE_PROPS_node_componentId = LINE_PROPS[node['componentId']]) != null ? _LINE_PROPS_node_componentId : LABEL_PROPS[node['componentId']]) != null ? _ref : [];
1804
+ const line = props[name];
1805
+ if (!shown.includes(name) || typeof line !== 'string') continue;
1806
+ cut.push({
1807
+ id: idOf(nodeId),
1808
+ line,
1809
+ limit: Number(limit),
1810
+ heading: node['componentId'] === 'muiTypography' && isHeadlineVariant(props['variant'])
1811
+ });
1812
+ }
1813
+ if (!cut.length) return [];
1814
+ const [first] = cut;
1815
+ const message = first.heading ? `A line in a heading style holds at most ${first.limit} characters, and "${lineHead(first.line)}" runs past them, so it was cut off where they end. Write it whole within ${first.limit} characters, or give a longer line a subtitle or body style.` : `"${lineHead(first.line)}" runs past the ${first.limit} characters its element holds, so it was cut off where they end. Write it whole within ${first.limit} characters.`;
1816
+ return [
1817
+ {
1818
+ rule: 14,
1819
+ code: 'copy-cut-at-ceiling',
1820
+ message,
1821
+ nodeIds: unique(cut.map((entry)=>entry.id))
1822
+ }
1823
+ ];
1824
+ }
1825
+ const PURE_CONTAINERS = new Set([
1826
+ 'muiBox',
1827
+ 'muiStack',
1828
+ 'muiContainer',
1829
+ 'muiGrid',
1830
+ 'section'
1831
+ ]);
1832
+ const EMBED_COMPONENTS = new Set([
1833
+ 'videoEmbed',
1834
+ 'custom-html',
1835
+ 'functionWidget'
1836
+ ]);
1837
+ const DUPLICATE_SX_MIN_KEYS = 2;
1838
+ const DUPLICATE_SX_MIN_NODES = 4;
1839
+ /**
1840
+ * Rule 16. The flattest tree that renders the design: no container that
1841
+ * wraps one other container and adds nothing, no empty containers, no inline
1842
+ * style repeated where a token or a component prop would carry it, no font
1843
+ * beyond the theme's, images lazy below the first, video by poster rather
1844
+ * than autoplay, and no third-party embed the plan did not name.
1845
+ */ export function detectHeavyDocument(tree, outputKind, context = {}) {
1846
+ const visits = walkTree(tree);
1847
+ const wrappers = [];
1848
+ const empty = [];
1849
+ const fonts = [];
1850
+ const eager = [];
1851
+ const video = [];
1852
+ const embeds = [];
1853
+ const sxSeen = new Map();
1854
+ let images = 0;
1855
+ for (const { id, node } of visits){
1856
+ var _node_nodes, _node_props, _node_sx, _node_sx1;
1857
+ var _node_props1, _node_sx2, _node_props2, _node_props3, _node_props4, _node_props5;
1858
+ const entry = AI_PALETTE[node.componentId];
1859
+ const children = ((_node_nodes = node.nodes) != null ? _node_nodes : []).filter((child)=>tree.nodes[child]);
1860
+ const ownProps = Object.entries((_node_props = node.props) != null ? _node_props : {}).filter(([name, value])=>!(name === 'component' && value === 'div'));
1861
+ if (id !== tree.rootId && PURE_CONTAINERS.has(node.componentId) && children.length === 1 && PURE_CONTAINERS.has(tree.nodes[children[0]].componentId) && !ownProps.length && !Object.keys((_node_sx = node.sx) != null ? _node_sx : {}).length) {
1862
+ wrappers.push(id);
1863
+ }
1864
+ const boundForm = node.componentId === 'form' && typeof ((_node_props1 = node.props) == null ? void 0 : _node_props1['formId']) === 'string';
1865
+ if ((entry == null ? void 0 : entry.acceptsChildren) && !children.length && !boundForm) empty.push(id);
1866
+ if (outputKind !== 'email' && ((_node_sx2 = node.sx) == null ? void 0 : _node_sx2['fontFamily']) !== undefined) fonts.push(id);
1867
+ if (node.componentId === 'image') {
1868
+ var _node_props6;
1869
+ images += 1;
1870
+ if (images > 1 && ((_node_props6 = node.props) == null ? void 0 : _node_props6['loading']) === 'eager') eager.push(id);
1871
+ }
1872
+ if (node.componentId === 'video' && (((_node_props2 = node.props) == null ? void 0 : _node_props2['autoPlay']) === true || ((_node_props3 = node.props) == null ? void 0 : _node_props3['preload']) === 'auto' || typeof ((_node_props4 = node.props) == null ? void 0 : _node_props4['src']) === 'string' && node.props['src'] && !((_node_props5 = node.props) == null ? void 0 : _node_props5['poster']))) {
1873
+ video.push(id);
1874
+ }
1875
+ if (EMBED_COMPONENTS.has(node.componentId) && !context.allowEmbeds) embeds.push(id);
1876
+ const sx = (_node_sx1 = node.sx) != null ? _node_sx1 : {};
1877
+ if (Object.keys(sx).length >= DUPLICATE_SX_MIN_KEYS) {
1878
+ var _sxSeen_get;
1879
+ const key = JSON.stringify(Object.keys(sx).sort().map((name)=>[
1880
+ name,
1881
+ sx[name]
1882
+ ]));
1883
+ sxSeen.set(key, [
1884
+ ...(_sxSeen_get = sxSeen.get(key)) != null ? _sxSeen_get : [],
1885
+ id
1886
+ ]);
1887
+ }
1888
+ }
1889
+ const duplicatedSx = [
1890
+ ...sxSeen.values()
1891
+ ].filter((ids)=>ids.length >= DUPLICATE_SX_MIN_NODES).flat();
1892
+ const violations = [];
1893
+ const add = (code, ids, message)=>{
1894
+ if (ids.length) violations.push({
1895
+ rule: 16,
1896
+ code,
1897
+ message,
1898
+ nodeIds: ids
1899
+ });
1900
+ };
1901
+ add('wrapper-in-wrapper', wrappers, 'A container wraps a single container and adds nothing. Remove the outer one.');
1902
+ add('empty-container', empty, 'An element meant to hold content is empty. Remove it, or fill it.');
1903
+ add('duplicated-inline-style', duplicatedSx, 'The same inline style repeats across elements. Carry it in a theme token or a component prop, once.');
1904
+ add('extra-font', fonts, "This names a font the theme does not load. Use the theme's typography.");
1905
+ add('eager-image', eager, 'An image below the first loads eagerly. Leave loading unset so images further down the page load when they are reached.');
1906
+ add('autoplay-video', video, 'A video loads its file before anyone plays it. Give it a poster, and let it play on click.');
1907
+ add('third-party-embed', embeds, 'This embeds a third-party player or script, which loads its own code on every visit. Use a Video from the media library, or name the embed and its cost in the plan.');
1908
+ return violations;
1909
+ }
1910
+ /** The items a list or a row of cards repeats, as a re-ask names them. */ const ITEM_NOUNS = {
1911
+ muiListItem: 'list item',
1912
+ muiCard: 'card'
1913
+ };
1914
+ /** Elements that frame or space what they hold and show nothing of their own. */ const FRAME_COMPONENTS = new Set([
1915
+ 'div',
1916
+ 'section',
1917
+ 'muiBox',
1918
+ 'muiStack',
1919
+ 'muiContainer',
1920
+ 'muiGrid',
1921
+ 'muiPaper',
1922
+ 'muiCard',
1923
+ 'muiCardContent',
1924
+ 'muiCardActions',
1925
+ 'muiListItem',
1926
+ 'muiAccordion',
1927
+ 'muiAccordionDetails'
1928
+ ]);
1929
+ /** Elements that show only their words, by the props that hold them. */ const WORD_PROPS = _extends({}, LINE_PROPS, {
1930
+ muiList: [
1931
+ 'subheader'
1932
+ ],
1933
+ muiInlineText: [
1934
+ 'children'
1935
+ ],
1936
+ muiButton: [
1937
+ 'children'
1938
+ ],
1939
+ muiScreenLink: [
1940
+ 'children'
1941
+ ]
1942
+ });
1943
+ /** Whether a node shows anything of its own: its words, or whatever an element that is neither a frame nor words draws. */ function showsOwn(node) {
1944
+ const words = WORD_PROPS[node.componentId];
1945
+ if (words) return words.some((name)=>{
1946
+ var _node_props;
1947
+ return isFilled((_node_props = node.props) == null ? void 0 : _node_props[name]);
1948
+ });
1949
+ return !FRAME_COMPONENTS.has(node.componentId);
1950
+ }
1951
+ /**
1952
+ * Rule 16, for an item with nothing in it (AGL-3072). A list item or a card
1953
+ * that holds elements, none of which shows a word, a picture or anything
1954
+ * else, is an empty row or an empty box on the page: a live About page's list
1955
+ * of estate planning services ended on a List Item whose List Item Text had
1956
+ * no words. An item that holds no element at all is `empty-container`'s, and
1957
+ * is not reported twice.
1958
+ */ export function detectEmptyItems(tree) {
1959
+ const empty = [];
1960
+ for (const { id, node } of walkTree(tree)){
1961
+ var _node_nodes;
1962
+ const noun = ITEM_NOUNS[node.componentId];
1963
+ if (!noun || !((_node_nodes = node.nodes) != null ? _node_nodes : []).some((child)=>tree.nodes[child])) continue;
1964
+ if (!walkTree({
1965
+ rootId: id,
1966
+ nodes: tree.nodes
1967
+ }).some((inner)=>showsOwn(inner.node))) {
1968
+ empty.push({
1969
+ id,
1970
+ noun
1971
+ });
1972
+ }
1973
+ }
1974
+ if (!empty.length) return [];
1975
+ const nouns = unique(empty.map((entry)=>entry.noun));
1976
+ const message = nouns.length > 1 ? 'A list item and a card hold nothing to read or see. Give each its words, or take it out.' : nouns[0] === 'list item' ? 'A list item holds no words, so its row shows empty. Write the words of its List Item Text, or take the item out.' : 'A card holds nothing to read or see. Give it its words, or take the card out.';
1977
+ return [
1978
+ {
1979
+ rule: 16,
1980
+ code: 'empty-item',
1981
+ message,
1982
+ nodeIds: empty.map((entry)=>entry.id)
1983
+ }
1984
+ ];
1985
+ }
1986
+ const EMAIL_COLUMN_PX = 600;
1987
+ const MEDIA_IMAGE_PROPS = {
1988
+ image: [
1989
+ 'src'
1990
+ ],
1991
+ emailImage: [
1992
+ 'src'
1993
+ ],
1994
+ video: [
1995
+ 'poster'
1996
+ ],
1997
+ collectionEntryAuthor: [
1998
+ 'image'
1999
+ ],
2000
+ contentAuthorProfile: [
2001
+ 'image'
2002
+ ]
2003
+ };
2004
+ /** The variant a slot of this width is served. */ function servedVariantWidth(slotWidth) {
2005
+ var _MEDIA_CDN_VARIANT_WIDTHS_find;
2006
+ const widest = MEDIA_CDN_VARIANT_WIDTHS[MEDIA_CDN_VARIANT_WIDTHS.length - 1];
2007
+ if (!slotWidth) return widest;
2008
+ return (_MEDIA_CDN_VARIANT_WIDTHS_find = MEDIA_CDN_VARIANT_WIDTHS.find((width)=>width >= slotWidth)) != null ? _MEDIA_CDN_VARIANT_WIDTHS_find : widest;
2009
+ }
2010
+ function pixels(value) {
2011
+ const parsed = typeof value === 'number' ? value : Number.parseFloat(String(value != null ? value : ''));
2012
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
2013
+ }
2014
+ function imageRequests(tree, outputKind, context) {
2015
+ const requests = [];
2016
+ for (const { id, node } of walkTree(tree)){
2017
+ var _MEDIA_IMAGE_PROPS_node_componentId;
2018
+ const named = (_MEDIA_IMAGE_PROPS_node_componentId = MEDIA_IMAGE_PROPS[node.componentId]) != null ? _MEDIA_IMAGE_PROPS_node_componentId : [];
2019
+ const values = mediaValues(node, context).filter(({ name })=>named.includes(name) || name.startsWith(`${REUSABLE_INSTANCE_PROP_VALUES_KEY}.`));
2020
+ for (const { value } of values){
2021
+ var _context_assets, _node_props;
2022
+ const ref = parseMediaRef(value);
2023
+ const facts = ref ? (_context_assets = context.assets) == null ? void 0 : _context_assets[ref.mediaId] : undefined;
2024
+ if (!(facts == null ? void 0 : facts.sizeBytes)) {
2025
+ requests.push({
2026
+ id,
2027
+ bytes: null
2028
+ });
2029
+ continue;
2030
+ }
2031
+ const slot = pixels((_node_props = node.props) == null ? void 0 : _node_props['width']);
2032
+ const variant = servedVariantWidth(outputKind === 'email' ? Math.min(slot != null ? slot : EMAIL_COLUMN_PX, EMAIL_COLUMN_PX) : slot);
2033
+ // A variant is the original scaled down, so its bytes scale with its
2034
+ // area; a variant wider than the original is the original.
2035
+ const ratio = facts.width ? Math.min(1, variant / facts.width) : 1;
2036
+ requests.push({
2037
+ id,
2038
+ bytes: Math.round(facts.sizeBytes * ratio * ratio)
2039
+ });
2040
+ }
2041
+ }
2042
+ return requests;
2043
+ }
2044
+ /** The rule-17 measurements of one validated tree. */ export function scoreAiOutput(tree, outputKind, context = {}) {
2045
+ const visits = walkTree(tree);
2046
+ const images = imageRequests(tree, outputKind, context);
2047
+ const families = new Set();
2048
+ for (const { node } of visits){
2049
+ var _node_sx;
2050
+ for (const family of sxValues((_node_sx = node.sx) == null ? void 0 : _node_sx['fontFamily'])){
2051
+ if (typeof family === 'string' && family.trim()) families.add(family.trim().toLowerCase());
2052
+ }
2053
+ }
2054
+ let emailHtmlBytes = null;
2055
+ if (outputKind === 'email') {
2056
+ const { html } = renderEmailHtml({
2057
+ nodes: tree.nodes,
2058
+ rootId: tree.rootId,
2059
+ sanitize: (value)=>sanitizeAuthorHtml(value)
2060
+ });
2061
+ emailHtmlBytes = new TextEncoder().encode(html).length;
2062
+ }
2063
+ return {
2064
+ nodes: visits.length,
2065
+ bytes: nodeMapBytes(tree.nodes),
2066
+ imageBytes: images.reduce((sum, image)=>{
2067
+ var _image_bytes;
2068
+ return sum + ((_image_bytes = image.bytes) != null ? _image_bytes : 0);
2069
+ }, 0),
2070
+ imagesUnmeasured: images.filter((image)=>image.bytes === null).length,
2071
+ embeds: visits.filter(({ node })=>EMBED_COMPONENTS.has(node.componentId)).length,
2072
+ fontFamilies: families.size,
2073
+ emailHtmlBytes
2074
+ };
2075
+ }
2076
+ const METRIC_LABELS = {
2077
+ nodes: 'elements',
2078
+ bytes: 'stored size',
2079
+ imageBytes: 'image weight',
2080
+ embeds: 'third-party embeds',
2081
+ fontFamilies: 'extra font families',
2082
+ emailHtmlBytes: `rendered size (mail clients clip past ${formatBytes(AI_EMAIL_CLIP_BYTES)})`
2083
+ };
2084
+ function figure(metric, value) {
2085
+ return metric === 'nodes' || metric === 'embeds' || metric === 'fontFamilies' ? value.toLocaleString('en-US') : formatBytes(value);
2086
+ }
2087
+ /** The parts of a tree that weigh most under one metric, heaviest first. */ function heaviest(metric, tree, outputKind, context) {
2088
+ switch(metric){
2089
+ case 'bytes':
2090
+ case 'emailHtmlBytes':
2091
+ return measureNodeMap(tree.nodes).largest.map((entry)=>entry.id);
2092
+ case 'nodes':
2093
+ {
2094
+ const index = indexShapes(tree);
2095
+ return index.visits.filter((visit)=>visit.depth >= 1 && visit.depth <= 2).sort((a, b)=>{
2096
+ var _index_sizeOf_get, _index_sizeOf_get1;
2097
+ return ((_index_sizeOf_get = index.sizeOf.get(b.id)) != null ? _index_sizeOf_get : 0) - ((_index_sizeOf_get1 = index.sizeOf.get(a.id)) != null ? _index_sizeOf_get1 : 0);
2098
+ }).slice(0, 3).map((visit)=>visit.id);
2099
+ }
2100
+ case 'imageBytes':
2101
+ return imageRequests(tree, outputKind, context).filter((image)=>image.bytes !== null).sort((a, b)=>{
2102
+ var _b_bytes, _a_bytes;
2103
+ return ((_b_bytes = b.bytes) != null ? _b_bytes : 0) - ((_a_bytes = a.bytes) != null ? _a_bytes : 0);
2104
+ }).slice(0, 3).map((image)=>image.id);
2105
+ case 'embeds':
2106
+ return walkTree(tree).filter(({ node })=>EMBED_COMPONENTS.has(node.componentId)).map((visit)=>visit.id);
2107
+ case 'fontFamilies':
2108
+ return walkTree(tree).filter(({ node })=>{
2109
+ var _node_sx;
2110
+ return ((_node_sx = node.sx) == null ? void 0 : _node_sx['fontFamily']) !== undefined;
2111
+ }).map((visit)=>visit.id);
2112
+ }
2113
+ }
2114
+ const BUDGET_METRICS = [
2115
+ 'nodes',
2116
+ 'bytes',
2117
+ 'imageBytes',
2118
+ 'embeds',
2119
+ 'fontFamilies',
2120
+ 'emailHtmlBytes'
2121
+ ];
2122
+ /**
2123
+ * Rule 17. An output over its kind's budget is refused with the figure, the
2124
+ * budget and the heaviest parts named, so the re-ask knows where to cut.
2125
+ */ export function detectOverBudget(score, outputKind, tree, context = {}) {
2126
+ const budget = AI_OUTPUT_BUDGETS[outputKind];
2127
+ const violations = [];
2128
+ for (const metric of BUDGET_METRICS){
2129
+ const value = score[metric];
2130
+ const limit = budget[metric];
2131
+ if (value === null || limit === undefined || value <= limit) continue;
2132
+ const offenders = heaviest(metric, tree, outputKind, context);
2133
+ const parts = offenders.length ? ` The heaviest parts: ${listNames(offenders, tree)}.` : '';
2134
+ violations.push({
2135
+ rule: 17,
2136
+ code: `over-budget-${metric}`,
2137
+ message: `This ${outputKind} measures ${figure(metric, value)} of ${METRIC_LABELS[metric]} against a budget of ${figure(metric, limit)}.${parts}`,
2138
+ nodeIds: offenders,
2139
+ figure: {
2140
+ metric,
2141
+ value,
2142
+ budget: limit
2143
+ }
2144
+ });
2145
+ }
2146
+ return violations;
2147
+ }
2148
+ /**
2149
+ * What a first visit to a generated output is estimated to transfer.
2150
+ *
2151
+ * The page half is a MEASUREMENT the platform already holds, not a guess:
2152
+ * `ESTIMATED_PAGE_TRANSFER_BYTES` is the first-party weight a published page
2153
+ * measured at settle, which `check:page-view-rate` holds against the
2154
+ * tenant page budget and the bandwidth meter is priced from. The tenant's
2155
+ * real-user vitals (`web-vitals-rum.ts`) report to analytics per visit and
2156
+ * are not readable for a draft nobody has visited, so they are not a second
2157
+ * source here. On top of that weight go this output's own stored document
2158
+ * and the images it places. A component or a layout reports what it adds to
2159
+ * a page; an email is not a page load and has none.
2160
+ */ export function estimateAiOutputLoad(score, outputKind) {
2161
+ if (outputKind === 'email') return null;
2162
+ const pageBytes = outputKind === 'page' || outputKind === 'template' ? ESTIMATED_PAGE_TRANSFER_BYTES : 0;
2163
+ return {
2164
+ pageBytes,
2165
+ documentBytes: score.bytes,
2166
+ imageBytes: score.imageBytes,
2167
+ imagesUnmeasured: score.imagesUnmeasured,
2168
+ embeds: score.embeds,
2169
+ totalBytes: Math.round(pageBytes + score.bytes + score.imageBytes)
2170
+ };
2171
+ }
2172
+ /**
2173
+ * A NODE MAP THAT DOES NOT AGREE WITH ITSELF (AGL-3143).
2174
+ *
2175
+ * A flat node map is two halves that have to match: what each node lists
2176
+ * under `nodes`, and what the map holds. A model writing one section in one
2177
+ * pass gets them out of step in two ways, and neither reaches it as
2178
+ * something it can act on.
2179
+ *
2180
+ * An element written but listed by nobody is dropped on the way in, so the
2181
+ * container that should have held it reads as empty and the answer comes
2182
+ * back "an element meant to hold content is empty — remove it, or fill it".
2183
+ * The content is already written; it is one line away from being placed, and
2184
+ * the re-ask does not say so. A live page build was told exactly that of a
2185
+ * card grid whose repeated card it had written and not placed; it answered by
2186
+ * naming four children and writing one.
2187
+ *
2188
+ * Which is the other half: a name listed under `nodes` that the map never
2189
+ * holds. That one is refused, but by the sanitizer under the palette
2190
+ * validator, as `Missing node "card2"` — which the door turns into "the
2191
+ * answer could not be used as a section", with no rule, no node and nothing
2192
+ * to change. It cost that page its last attempt and the build stopped three
2193
+ * sections in, so the page's form — the thing being measured — was never
2194
+ * reached at all.
2195
+ *
2196
+ * Both are read off the tree AS THE MODEL WROTE IT, before the palette
2197
+ * validator drops anything, so a re-ask can quote the node at fault.
2198
+ */ export function detectDisagreeingNodes(input) {
2199
+ if (!isRecord(input) || typeof input['rootId'] !== 'string' || !isRecord(input['nodes'])) return [];
2200
+ const rootId = input['rootId'];
2201
+ const nodes = input['nodes'];
2202
+ if (!isRecord(nodes[rootId])) return [];
2203
+ const childrenOf = (id)=>{
2204
+ const node = nodes[id];
2205
+ const listed = isRecord(node) ? node['nodes'] : undefined;
2206
+ return Array.isArray(listed) ? listed.filter((child)=>typeof child === 'string') : [];
2207
+ };
2208
+ const reached = new Set([
2209
+ rootId
2210
+ ]);
2211
+ const missing = new Map();
2212
+ const queue = [
2213
+ rootId
2214
+ ];
2215
+ while(queue.length){
2216
+ const id = queue.shift();
2217
+ for (const child of childrenOf(id)){
2218
+ if (!isRecord(nodes[child])) {
2219
+ var _missing_get;
2220
+ missing.set(id, [
2221
+ ...(_missing_get = missing.get(id)) != null ? _missing_get : [],
2222
+ child
2223
+ ]);
2224
+ continue;
2225
+ }
2226
+ if (reached.has(child)) continue;
2227
+ reached.add(child);
2228
+ queue.push(child);
2229
+ }
2230
+ }
2231
+ const orphans = Object.keys(nodes).filter((id)=>!reached.has(id) && isRecord(nodes[id]));
2232
+ const violations = [];
2233
+ if (missing.size) {
2234
+ const named = unique([
2235
+ ...missing.values()
2236
+ ].flat());
2237
+ violations.push({
2238
+ rule: null,
2239
+ code: 'missing-child',
2240
+ message: 'This holds elements that were never written, so the answer could not be read. Write each one, or take its name out of the list of what this holds.',
2241
+ detail: `Listed under "nodes" and missing from the answer: ${named.map((id)=>`"${id}"`).join(', ')}.`,
2242
+ nodeIds: [
2243
+ ...missing.keys()
2244
+ ]
2245
+ });
2246
+ }
2247
+ if (orphans.length) {
2248
+ violations.push({
2249
+ rule: 16,
2250
+ code: 'orphan-node',
2251
+ message: 'This element was written, but nothing holds it, so none of it reaches the page. List it under the element it belongs in, or take it out.',
2252
+ nodeIds: orphans
2253
+ });
2254
+ }
2255
+ return violations;
2256
+ }
2257
+ /**
2258
+ * The whole check on one emitted tree: the palette validator first, then
2259
+ * every tree rule, then the budget. `otherPages` are the trees generated
2260
+ * beside this one, for the across-pages half of rule 1.
2261
+ */ export function validateAiDoctrineTree(input, outputKind, context = {}, otherPages = []) {
2262
+ const publish = detectPublishIntent(input);
2263
+ // Asked of the answer as written, because the palette validator drops an
2264
+ // element nobody lists and refuses a name nobody wrote in words no re-ask
2265
+ // can act on (AGL-3143).
2266
+ const disagreeing = detectDisagreeingNodes(input);
2267
+ if (disagreeing.length) {
2268
+ return {
2269
+ ok: false,
2270
+ tree: null,
2271
+ violations: [
2272
+ ...disagreeing,
2273
+ ...publish
2274
+ ],
2275
+ score: null,
2276
+ load: null
2277
+ };
2278
+ }
2279
+ const validated = validateAiNodeTree(input, AI_OUTPUT_SURFACE[outputKind], context);
2280
+ if (validated.ok === false) {
2281
+ return {
2282
+ ok: false,
2283
+ tree: null,
2284
+ violations: [
2285
+ {
2286
+ rule: null,
2287
+ code: `tree-${validated.code}`,
2288
+ message: `The answer could not be used as a ${outputKind}.`,
2289
+ detail: validated.error
2290
+ },
2291
+ ...publish
2292
+ ],
2293
+ score: null,
2294
+ load: null
2295
+ };
2296
+ }
2297
+ const tree = {
2298
+ rootId: validated.rootId,
2299
+ nodes: validated.nodes
2300
+ };
2301
+ // The validator's repairs name the nodes as they were written; every finding names the minted ids.
2302
+ const minted = new Map(Object.entries(validated.sourceIds).map(([id, source])=>[
2303
+ source,
2304
+ id
2305
+ ]));
2306
+ const written = isRecord(input) && isRecord(input['nodes']) ? input['nodes'] : {};
2307
+ // What the model wrote of a node, by its minted id: the answer this check was
2308
+ // given, or what the caller says was written of a tree it stored first.
2309
+ const writtenOf = (id)=>{
2310
+ var _validated_sourceIds_id;
2311
+ const source = (_validated_sourceIds_id = validated.sourceIds[id]) != null ? _validated_sourceIds_id : id;
2312
+ return context.writtenNode ? context.writtenNode(source) : written[source];
2313
+ };
2314
+ // A node as this check was given it, by its minted id: for a page as stored, what the page carries.
2315
+ const storedOf = (id)=>{
2316
+ var _validated_sourceIds_id;
2317
+ return written[(_validated_sourceIds_id = validated.sourceIds[id]) != null ? _validated_sourceIds_id : id];
2318
+ };
2319
+ const violations = [
2320
+ ...publish,
2321
+ ...detectRepeatedSubtrees(tree, otherPages, context),
2322
+ ...detectLayoutRegions(tree, outputKind),
2323
+ ...detectInlineForms(tree, outputKind, context),
2324
+ ...detectLiteralStyles(tree, outputKind),
2325
+ ...detectInvisibleLinks(tree, outputKind),
2326
+ ...detectOffBrandEmail(tree, outputKind, context.brand),
2327
+ ...detectTypedData(tree),
2328
+ ...detectImageSources(tree, context),
2329
+ ...detectUnrelatedScreenLinks(tree, outputKind, context),
2330
+ ...detectLinksWithoutDestination(tree, outputKind, context, writtenOf, storedOf),
2331
+ ...detectDocumentStructure(tree, outputKind),
2332
+ ...detectAdHocWidths(tree, outputKind),
2333
+ ...detectUnresponsiveGrids(tree, outputKind, writtenOf),
2334
+ ...detectOffVoiceCopy(aiTreeCopy(tree, context), context.framing),
2335
+ ...detectCutLines(validated.repairs, written, (id)=>{
2336
+ var _minted_get;
2337
+ return (_minted_get = minted.get(id)) != null ? _minted_get : id;
2338
+ }),
2339
+ ...detectDanglingWords(tree),
2340
+ ...detectHeavyDocument(tree, outputKind, context),
2341
+ ...detectEmptyItems(tree)
2342
+ ];
2343
+ const score = scoreAiOutput(tree, outputKind, context);
2344
+ violations.push(...detectOverBudget(score, outputKind, tree, context));
2345
+ return {
2346
+ ok: violations.length === 0,
2347
+ tree: validated,
2348
+ violations,
2349
+ score,
2350
+ load: estimateAiOutputLoad(score, outputKind)
2351
+ };
2352
+ }
2353
+ const NAME_STOPWORDS = new Set([
2354
+ 'a',
2355
+ 'an',
2356
+ 'and',
2357
+ 'the',
2358
+ 'of',
2359
+ 'for',
2360
+ 'to',
2361
+ 'our',
2362
+ 'your',
2363
+ 'my',
2364
+ 'us',
2365
+ 'we',
2366
+ 'page',
2367
+ 'pages',
2368
+ 'section',
2369
+ 'new',
2370
+ 'site',
2371
+ 'block',
2372
+ 'main'
2373
+ ]);
2374
+ function nameTokens(value) {
2375
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, ' ').split(' ').filter(Boolean).map((word)=>word.length > 3 && word.endsWith('s') && !word.endsWith('ss') ? word.slice(0, -1) : word).filter((word)=>!NAME_STOPWORDS.has(word));
2376
+ }
2377
+ /**
2378
+ * Whether two names name the same thing: the same words once filler is
2379
+ * dropped, or every word of a name of two or more words inside the other.
2380
+ */ export function aiNamesMatch(left, right) {
2381
+ const a = new Set(nameTokens(left));
2382
+ const b = new Set(nameTokens(right));
2383
+ if (!a.size || !b.size) return false;
2384
+ const [small, large] = a.size <= b.size ? [
2385
+ a,
2386
+ b
2387
+ ] : [
2388
+ b,
2389
+ a
2390
+ ];
2391
+ return [
2392
+ ...small
2393
+ ].every((word)=>large.has(word)) && (small.size === large.size || small.size >= 2);
2394
+ }
2395
+ function inventoryKinds(inventory) {
2396
+ const kinds = new Map();
2397
+ if (!inventory) return kinds;
2398
+ const add = (rows, kind)=>{
2399
+ for (const row of rows)kinds.set(row.id, kind);
2400
+ };
2401
+ add(inventory.components, 'component');
2402
+ add(inventory.layouts, 'layout');
2403
+ add(inventory.templates, 'template');
2404
+ add(inventory.forms, 'form');
2405
+ add(inventory.datasets, 'dataset');
2406
+ add(inventory.collections, 'collection');
2407
+ add(inventory.screens, 'screen');
2408
+ return kinds;
2409
+ }
2410
+ /** What a plan reference resolves to: an inventory record's kind, or a creation's. */ function refKind(ref, plan, kinds) {
2411
+ var _ref, _kinds_get;
2412
+ var _aiPlanCreateFor;
2413
+ if (!ref) return null;
2414
+ if (isAiPlanNewRef(ref)) return (_ref = (_aiPlanCreateFor = aiPlanCreateFor(plan, ref)) == null ? void 0 : _aiPlanCreateFor.kind) != null ? _ref : null;
2415
+ return (_kinds_get = kinds.get(ref)) != null ? _kinds_get : null;
2416
+ }
2417
+ function sectionPaths(plan) {
2418
+ return plan.screens.flatMap((screen, screenIndex)=>screen.sections.map((section, sectionIndex)=>({
2419
+ screen,
2420
+ screenIndex,
2421
+ section,
2422
+ path: `screens[${screenIndex}].sections[${sectionIndex}]`
2423
+ })));
2424
+ }
2425
+ const LAYOUT_REGION_NAME = /\b(?:site header|header|nav(?:igation)?|nav ?bar|footer|announcement(?: bar)?|cookie(?: notice| banner| consent)?|top ?bar)\b/i;
2426
+ const FORM_SECTION_NAME = /\bform\b|\bsign[- ]?up\b|\bsubscribe\b|\bnewsletter\b|\bcontact us\b|\b(?:get|request) a quote\b|\bregist(?:er|ration)\b|\b(?:en|in)quiry\b/i;
2427
+ /**
2428
+ * Rule 1 (plan): repeated items place a component, and a section two screens
2429
+ * share is one. A workspace that keeps no reusable components draws them in
2430
+ * their sections instead (AGL-3030), and nothing is refused.
2431
+ */ export function detectPlanRepeats(plan, inventory, capabilities = null) {
2432
+ if ((capabilities == null ? void 0 : capabilities.reusableComponents) === false) return [];
2433
+ const kinds = inventoryKinds(inventory);
2434
+ const violations = [];
2435
+ const placesComponent = (uses)=>uses.some((ref)=>refKind(ref, plan, kinds) === 'component');
2436
+ const bound = (uses)=>uses.some((ref)=>[
2437
+ 'dataset',
2438
+ 'collection'
2439
+ ].includes(String(refKind(ref, plan, kinds))));
2440
+ const repeated = sectionPaths(plan).filter(({ section })=>section.items >= AI_REPEAT_MIN_COUNT && !placesComponent(section.uses) && !bound(section.uses));
2441
+ if (repeated.length) {
2442
+ violations.push({
2443
+ rule: 1,
2444
+ code: 'plan-repeated-items',
2445
+ message: 'A section repeats items without a component. Plan one reusable component for the item, reuse one the site has, or bind the items to data.',
2446
+ paths: repeated.map((entry)=>entry.path)
2447
+ });
2448
+ }
2449
+ const byName = new Map();
2450
+ for (const entry of sectionPaths(plan)){
2451
+ var _byName_get;
2452
+ const key = nameTokens(entry.section.name).join(' ');
2453
+ if (!key || LAYOUT_REGION_NAME.test(entry.section.name)) continue;
2454
+ byName.set(key, [
2455
+ ...(_byName_get = byName.get(key)) != null ? _byName_get : [],
2456
+ entry
2457
+ ]);
2458
+ }
2459
+ for (const entries of byName.values()){
2460
+ const screens = new Set(entries.map((entry)=>entry.screenIndex));
2461
+ if (screens.size < 2) continue;
2462
+ const shared = entries.map((entry)=>entry.section.uses.filter((ref)=>refKind(ref, plan, kinds) === 'component')).reduce((common, refs)=>common.filter((ref)=>refs.includes(ref)));
2463
+ if (shared.length) continue;
2464
+ violations.push({
2465
+ rule: 1,
2466
+ code: 'plan-section-across-screens',
2467
+ message: `The "${entries[0].section.name}" section is planned on ${screens.size} screens. Plan it as one reusable component and place it on each.`,
2468
+ paths: entries.map((entry)=>entry.path)
2469
+ });
2470
+ }
2471
+ return violations;
2472
+ }
2473
+ /** The fewest sections of one item, side by side and of one kind, that are one list split apart. */ export const AI_SPLIT_LIST_MIN_SECTIONS = 2;
2474
+ /** A section name's label: what comes before a colon or a spaced dash, as "practice area" in "practice area: family law". */ const SECTION_LABEL = /^(.+?)\s*(?::|\s[-–—])\s*\S/;
2475
+ /** A name for many of what a name names one of. */ function pluralOf(name) {
2476
+ const words = name.trim();
2477
+ if (/s$/i.test(words)) return words;
2478
+ return /[^aeiou]y$/i.test(words) ? `${words.slice(0, -1)}ies` : `${words}s`;
2479
+ }
2480
+ /**
2481
+ * What kind of item a section of one item shows, for telling a list split
2482
+ * into sections apart from sections that each show one different thing: the
2483
+ * components it places, else the label its name gives the item. `null` when
2484
+ * neither says, or when the section binds its item to data.
2485
+ */ function sectionItemKind(section, plan, kinds, inventory) {
2486
+ var _ref;
2487
+ var _SECTION_LABEL_exec_, _SECTION_LABEL_exec;
2488
+ if (section.uses.some((ref)=>[
2489
+ 'dataset',
2490
+ 'collection'
2491
+ ].includes(String(refKind(ref, plan, kinds))))) return null;
2492
+ const label = (_ref = (_SECTION_LABEL_exec = SECTION_LABEL.exec(section.name)) == null ? void 0 : (_SECTION_LABEL_exec_ = _SECTION_LABEL_exec[1]) == null ? void 0 : _SECTION_LABEL_exec_.trim()) != null ? _ref : '';
2493
+ const components = section.uses.filter((ref)=>refKind(ref, plan, kinds) === 'component');
2494
+ if (components.length) {
2495
+ var _aiPlanCreateFor, _inventory_components_find;
2496
+ const [first] = components;
2497
+ const named = isAiPlanNewRef(first) ? (_aiPlanCreateFor = aiPlanCreateFor(plan, first)) == null ? void 0 : _aiPlanCreateFor.name : inventory == null ? void 0 : (_inventory_components_find = inventory.components.find((row)=>row.id === first)) == null ? void 0 : _inventory_components_find.name;
2498
+ return {
2499
+ key: `component:${components.map((ref)=>ref.toLowerCase()).sort().join('|')}`,
2500
+ name: pluralOf(label || named || first),
2501
+ components
2502
+ };
2503
+ }
2504
+ const words = nameTokens(label).join(' ');
2505
+ return words ? {
2506
+ key: `label:${words}`,
2507
+ name: pluralOf(label),
2508
+ components: []
2509
+ } : null;
2510
+ }
2511
+ /**
2512
+ * Rule 1 (plan): a list is one section whose items repeat (AGL-3071). A
2513
+ * section's `items` counts what it repeats, so two or more sections side by
2514
+ * side that each show one item of the same kind — the same component, or a
2515
+ * name that labels the same kind of item — are one list split apart. A live
2516
+ * Free About page planned "the four areas we practice" as four sections of one
2517
+ * item: none was drawn from the one written-once item a repeated section is
2518
+ * built from, and the four came out in three different shapes. The re-ask
2519
+ * names the sections and gives the one section to plan instead. Where the
2520
+ * workspace keeps reusable components, a list long enough for rule 1 places
2521
+ * one for its item, so the section it gives places one too.
2522
+ *
2523
+ * Sections of several items are never joined: two lists that share a card
2524
+ * are two lists, and rule 8 counts each within its own section (AGL-3061).
2525
+ */ export function detectPlanSplitLists(plan, inventory, capabilities = null) {
2526
+ const kinds = inventoryKinds(inventory);
2527
+ const violations = [];
2528
+ plan.screens.forEach((screen, screenIndex)=>{
2529
+ let run = [];
2530
+ const close = ()=>{
2531
+ if (run.length >= AI_SPLIT_LIST_MIN_SECTIONS) {
2532
+ const names = run.map(({ index })=>`"${screen.sections[index].name}"`);
2533
+ const listed = `${names.slice(0, -1).join(', ')} and ${names[names.length - 1]}`;
2534
+ const [{ kind }] = run;
2535
+ // Rule 1 asks a list this long on a workspace that keeps components to place one for its item.
2536
+ const component = !kind.components.length && (capabilities == null ? void 0 : capabilities.reusableComponents) !== false && run.length >= AI_REPEAT_MIN_COUNT;
2537
+ const section = {
2538
+ name: kind.name,
2539
+ uses: component ? [
2540
+ 'new:<name>'
2541
+ ] : kind.components,
2542
+ items: run.length
2543
+ };
2544
+ const instead = component ? `, placing one reusable component for the item: reuse one the site has by its id, or declare one in create and place it as new:<name>, as in ${JSON.stringify(section)}.` : `: ${JSON.stringify(section)}.`;
2545
+ violations.push({
2546
+ rule: 1,
2547
+ code: 'plan-split-list',
2548
+ message: `The sections ${listed} each show one item of the same kind, so they are one list split apart. Plan them as one section whose ${run.length} items repeat${instead}`,
2549
+ paths: run.map(({ index })=>`screens[${screenIndex}].sections[${index}]`)
2550
+ });
2551
+ }
2552
+ run = [];
2553
+ };
2554
+ screen.sections.forEach((section, index)=>{
2555
+ const kind = section.items === 1 ? sectionItemKind(section, plan, kinds, inventory) : null;
2556
+ if (!kind || run.length && run[0].kind.key !== kind.key) close();
2557
+ if (kind) run.push({
2558
+ index,
2559
+ kind
2560
+ });
2561
+ });
2562
+ close();
2563
+ });
2564
+ return violations;
2565
+ }
2566
+ /** Where the plan's sections place a record of one kind, as `screens[0].sections[1].uses[0]`. */ function placementsOf(plan, kinds, kind) {
2567
+ return sectionPaths(plan).flatMap(({ section, path })=>section.uses.flatMap((ref, index)=>refKind(ref, plan, kinds) === kind ? [
2568
+ `${path}.uses[${index}]`
2569
+ ] : []));
2570
+ }
2571
+ /**
2572
+ * Rule 2 (plan): every screen declares a layout, no section is a copy of a
2573
+ * layout region, and no section places a layout, which frames a whole screen
2574
+ * (AGL-3040). A site with no layout, where this job may not create one
2575
+ * (AGL-3030), has nothing to declare: its screens name none, and a screen
2576
+ * that names none is not refused there. A screen whose layout is a `new:`
2577
+ * reference the plan never creates is refused here, and only here.
2578
+ */ export function detectPlanLayoutRegions(plan, inventory, capabilities = null) {
2579
+ var _ref;
2580
+ const kinds = inventoryKinds(inventory);
2581
+ const violations = [];
2582
+ const mayCreateLayout = !capabilities || capabilities.create.layout.allowed;
2583
+ const noLayoutToName = !mayCreateLayout && !((_ref = inventory == null ? void 0 : inventory.layouts.length) != null ? _ref : 0);
2584
+ const unlaid = plan.screens.map((screen, index)=>({
2585
+ screen,
2586
+ index
2587
+ })).filter(({ screen })=>refKind(screen.layout, plan, kinds) !== 'layout' && !(noLayoutToName && screen.layout === null));
2588
+ if (unlaid.length) {
2589
+ violations.push({
2590
+ rule: 2,
2591
+ code: 'plan-screen-without-layout',
2592
+ message: noLayoutToName ? 'A screen names a layout the site does not have, and this job may not create one. Leave the layout empty.' : mayCreateLayout ? "A screen names no layout the site has or the plan creates. Put every screen in the site's layout, or plan one." : 'A screen names no layout the site has, and this job may not create one. Put every screen in a layout the site has.',
2593
+ paths: unlaid.map(({ index })=>`screens[${index}].layout`)
2594
+ });
2595
+ }
2596
+ const regions = sectionPaths(plan).filter(({ section })=>LAYOUT_REGION_NAME.test(section.name));
2597
+ if (regions.length) {
2598
+ violations.push({
2599
+ rule: 2,
2600
+ code: 'plan-layout-region-section',
2601
+ message: 'A screen plans its own header, navigation or footer. Those live in the layout; take the section off the screen.',
2602
+ paths: regions.map((entry)=>entry.path)
2603
+ });
2604
+ }
2605
+ const placed = placementsOf(plan, kinds, 'layout');
2606
+ if (placed.length) {
2607
+ violations.push({
2608
+ rule: 2,
2609
+ code: 'plan-layout-in-section',
2610
+ message: "A section places a layout. A layout frames a whole screen and is never placed inside one: name it as the screen's layout, and take it out of the section's uses.",
2611
+ paths: placed
2612
+ });
2613
+ }
2614
+ return violations;
2615
+ }
2616
+ /**
2617
+ * Rule 3 (plan): a section that collects answers places a form from the Forms
2618
+ * page. A workspace that keeps no saved forms draws the form on the page
2619
+ * (AGL-3030), and nothing is refused.
2620
+ */ export function detectPlanInlineForms(plan, inventory, capabilities = null) {
2621
+ if ((capabilities == null ? void 0 : capabilities.reusableComponents) === false) return [];
2622
+ const kinds = inventoryKinds(inventory);
2623
+ const unbound = sectionPaths(plan).filter(({ section })=>FORM_SECTION_NAME.test(section.name) && !section.uses.some((ref)=>refKind(ref, plan, kinds) === 'form'));
2624
+ return unbound.length ? [
2625
+ {
2626
+ rule: 3,
2627
+ code: 'plan-form-not-placed',
2628
+ message: 'A section collects answers without a form. Reuse a form from the Forms page, or plan one there, and place it by id.',
2629
+ paths: unbound.map((entry)=>entry.path)
2630
+ }
2631
+ ] : [];
2632
+ }
2633
+ /** Pages sharing one shape this many times are a template applied. */ export const AI_SIMILAR_PAGES_MIN = 3;
2634
+ /**
2635
+ * Rule 4 (plan): three or more screens with the same sections share one
2636
+ * template, and a template is what a screen applies, never what one of its
2637
+ * sections places (AGL-3040).
2638
+ */ export function detectUntemplatedSimilarPages(plan, inventory = null) {
2639
+ const groups = new Map();
2640
+ plan.screens.forEach((screen, index)=>{
2641
+ var _groups_get;
2642
+ if (screen.sections.length < 2) return;
2643
+ const signature = screen.sections.map((section)=>nameTokens(section.name).join(' ')).join('|');
2644
+ groups.set(signature, [
2645
+ ...(_groups_get = groups.get(signature)) != null ? _groups_get : [],
2646
+ index
2647
+ ]);
2648
+ });
2649
+ const violations = [];
2650
+ for (const indexes of groups.values()){
2651
+ if (indexes.length < AI_SIMILAR_PAGES_MIN) continue;
2652
+ const templates = new Set(indexes.map((index)=>plan.screens[index].template));
2653
+ if (templates.size === 1 && !templates.has(null)) continue;
2654
+ violations.push({
2655
+ rule: 4,
2656
+ code: 'plan-similar-pages',
2657
+ message: `${indexes.length} screens share one shape. Plan one template and apply it ${indexes.length} times with each page's copy, or bind it to a collection.`,
2658
+ paths: indexes.map((index)=>`screens[${index}].template`)
2659
+ });
2660
+ }
2661
+ const placed = placementsOf(plan, inventoryKinds(inventory), 'template');
2662
+ if (placed.length) {
2663
+ violations.push({
2664
+ rule: 4,
2665
+ code: 'plan-template-in-section',
2666
+ message: "A section places a template. A template is applied to a whole screen and is never placed inside one: take it out of the section's uses, and name it as the screen's template only when the whole screen is built from it.",
2667
+ paths: placed
2668
+ });
2669
+ }
2670
+ return violations;
2671
+ }
2672
+ /** Rule 5 (plan): a color value belongs only in a theme change. */ export function detectPlanLiteralColors(plan) {
2673
+ const paths = [];
2674
+ plan.create.forEach((entry, index)=>{
2675
+ if (entry.kind === 'theme-change') return;
2676
+ if (hasLiteralColor(entry.why) || entry.fields.some(hasLiteralColor)) {
2677
+ paths.push(`create[${index}]`);
2678
+ }
2679
+ });
2680
+ for (const { section, path } of sectionPaths(plan)){
2681
+ if (hasLiteralColor(section.name)) paths.push(path);
2682
+ }
2683
+ return paths.length ? [
2684
+ {
2685
+ rule: 5,
2686
+ code: 'plan-literal-color',
2687
+ message: 'The plan writes a color by value outside a theme change. Name theme tokens, and plan a theme change for a color the theme lacks.',
2688
+ paths
2689
+ }
2690
+ ] : [];
2691
+ }
2692
+ const REUSE_BEFORE_CREATE_KINDS = new Set([
2693
+ 'component',
2694
+ 'layout',
2695
+ 'form',
2696
+ 'dataset'
2697
+ ]);
2698
+ /** Rule 7 (plan): reuse names real records, and a creation is an exception that says why. */ export function detectCreateBeforeReuse(plan, inventory) {
2699
+ var _ref, _ref1, _ref2, _ref3;
2700
+ const kinds = inventoryKinds(inventory);
2701
+ const violations = [];
2702
+ const unknown = plan.reuse.map((entry, index)=>({
2703
+ entry,
2704
+ index
2705
+ })).filter(({ entry })=>entry.kind !== 'theme' && kinds.get(entry.id) !== entry.kind);
2706
+ if (unknown.length) {
2707
+ violations.push({
2708
+ rule: 7,
2709
+ code: 'plan-reuse-unknown',
2710
+ message: 'The plan reuses something the site does not have. Reuse only what the site inventory lists.',
2711
+ paths: unknown.map(({ index })=>`reuse[${index}]`)
2712
+ });
2713
+ }
2714
+ const unexplained = plan.create.map((entry, index)=>({
2715
+ entry,
2716
+ index
2717
+ })).filter(({ entry })=>!entry.why.trim());
2718
+ if (unexplained.length) {
2719
+ violations.push({
2720
+ rule: 7,
2721
+ code: 'plan-create-without-why',
2722
+ message: 'The plan creates something without saying why nothing the site has will do.',
2723
+ paths: unexplained.map(({ index })=>`create[${index}].why`)
2724
+ });
2725
+ }
2726
+ const rows = {
2727
+ component: (_ref = inventory == null ? void 0 : inventory.components) != null ? _ref : [],
2728
+ layout: (_ref1 = inventory == null ? void 0 : inventory.layouts) != null ? _ref1 : [],
2729
+ form: (_ref2 = inventory == null ? void 0 : inventory.forms) != null ? _ref2 : [],
2730
+ dataset: (_ref3 = inventory == null ? void 0 : inventory.datasets) != null ? _ref3 : []
2731
+ };
2732
+ plan.create.forEach((entry, index)=>{
2733
+ if (!REUSE_BEFORE_CREATE_KINDS.has(entry.kind) || entry.duplicateOf) return;
2734
+ const existing = rows[entry.kind].find((row)=>aiNamesMatch(row.name, entry.name));
2735
+ if (!existing) return;
2736
+ violations.push({
2737
+ rule: 7,
2738
+ code: 'plan-duplicates-existing',
2739
+ message: `The site already has a ${entry.kind} named "${existing.name}". Reuse it, or propose extending it, instead of creating "${entry.name}".`,
2740
+ paths: [
2741
+ `create[${index}]`
2742
+ ]
2743
+ });
2744
+ });
2745
+ return violations;
2746
+ }
2747
+ /**
2748
+ * Rule 7 (plan): a plan creates only what the request says this job may
2749
+ * create on this site (AGL-3030) — what the workspace's plan includes, what
2750
+ * the site has room for, and what the job builds itself. One violation a
2751
+ * creation, each saying why and what to do instead.
2752
+ */ export function detectPlanUncreatable(plan, capabilities) {
2753
+ if (!capabilities) return [];
2754
+ return aiPlanUncreatable(plan, capabilities).map((entry)=>({
2755
+ rule: 7,
2756
+ code: 'plan-create-not-allowed',
2757
+ message: entry.message,
2758
+ paths: [
2759
+ entry.path
2760
+ ]
2761
+ }));
2762
+ }
2763
+ /**
2764
+ * What a section most likely meant by a creation it places and the plan never
2765
+ * declares, so the refusal can name the way out: a form where the reference
2766
+ * or its section reads as one and the section places no form yet, otherwise
2767
+ * a component. Only the message reads it; the reference is refused whatever
2768
+ * it was meant to be.
2769
+ */ function undeclaredPlacementKind(name, section, plan, kinds) {
2770
+ if (FORM_SECTION_NAME.test(name)) return 'form';
2771
+ const placesForm = section.uses.some((ref)=>refKind(ref, plan, kinds) === 'form');
2772
+ return FORM_SECTION_NAME.test(section.name) && !placesForm ? 'form' : 'component';
2773
+ }
2774
+ /**
2775
+ * Rule 7 (plan): what a plan places, it reuses or it creates (AGL-3040). The
2776
+ * doctrine has a plan refer to what it creates as new:<name>, so a reference
2777
+ * that names no entry of the create list is a creation nothing builds. A page
2778
+ * kept with one stops at its first pass, sending the member to make that
2779
+ * creation by hand, which a workspace that cannot make it never can. Each is
2780
+ * refused with the one re-ask, once a name however often it is placed, and
2781
+ * says what to do on THIS workspace: declare the creation where the job may
2782
+ * make one more of that kind, and otherwise build the page without it, drawn
2783
+ * on the page or placed from what the site has. A screen's layout is rule
2784
+ * 2's (`plan-screen-without-layout`), and is not reported twice.
2785
+ */ export function detectPlanUndeclaredCreations(plan, inventory, capabilities = null) {
2786
+ const kinds = inventoryKinds(inventory);
2787
+ const byName = new Map();
2788
+ for (const ref of aiPlanUndeclaredRefs(plan)){
2789
+ var _byName_get;
2790
+ if (ref.field === 'layout') continue;
2791
+ const key = ref.name.toLowerCase();
2792
+ byName.set(key, [
2793
+ ...(_byName_get = byName.get(key)) != null ? _byName_get : [],
2794
+ ref
2795
+ ]);
2796
+ }
2797
+ return [
2798
+ ...byName.values()
2799
+ ].map((refs)=>{
2800
+ const [first] = refs;
2801
+ const screen = plan.screens[first.screenIndex];
2802
+ const section = first.field === 'uses' ? screen.sections[first.sectionIndex] : null;
2803
+ const kind = section ? undeclaredPlacementKind(first.name, section, plan, kinds) : 'template';
2804
+ const refused = capabilities ? aiPlanUncreatableKind(plan, kind, capabilities) : null;
2805
+ const where = section ? `${section.name ? `The "${section.name}" section` : 'A section'} places a creation named "${first.name}", but the plan never creates it` : `${screen.title ? `The screen "${screen.title}"` : 'A screen'} applies a template named "${first.name}", but the plan never creates it`;
2806
+ let message;
2807
+ if (!refused) {
2808
+ const otherwise = section ? `place ${aiCreationNoun(kind)} the site already has by its id` : "leave the screen's template empty";
2809
+ message = `${where}. Declare it in create as ${aiCreationNoun(kind)}, with why nothing the site has will do, or ${otherwise}.`;
2810
+ } else if (section) {
2811
+ message = `${where}, and ${refused.reason}. ${refused.instead} Take it out of the section's uses.`;
2812
+ } else {
2813
+ message = `${where}, and ${refused.reason}. Leave the screen's template empty, or apply a template the site already has.`;
2814
+ }
2815
+ return {
2816
+ rule: 7,
2817
+ code: 'plan-creation-undeclared',
2818
+ message,
2819
+ paths: refs.map((ref)=>ref.path)
2820
+ };
2821
+ });
2822
+ }
2823
+ /**
2824
+ * Rule 8 (plan): long lists are bound, and a list the site already holds is
2825
+ * bound to it. Where this job may not create a dataset (AGL-3030), the way
2826
+ * out the refusal names is a shorter list rather than a dataset it would
2827
+ * only refuse next.
2828
+ */ export function detectPlanTypedData(plan, inventory, capabilities = null) {
2829
+ var _ref, _ref1;
2830
+ const kinds = inventoryKinds(inventory);
2831
+ const violations = [];
2832
+ const data = [
2833
+ ...(_ref = inventory == null ? void 0 : inventory.datasets) != null ? _ref : [],
2834
+ ...(_ref1 = inventory == null ? void 0 : inventory.collections) != null ? _ref1 : []
2835
+ ];
2836
+ const typed = [];
2837
+ const unbound = [];
2838
+ for (const { section, path } of sectionPaths(plan)){
2839
+ const bindsData = section.uses.some((ref)=>[
2840
+ 'dataset',
2841
+ 'collection'
2842
+ ].includes(String(refKind(ref, plan, kinds))));
2843
+ if (bindsData) continue;
2844
+ if (section.items >= AI_TYPED_LIST_MIN_ITEMS) typed.push(path);
2845
+ const match = data.find((row)=>aiNamesMatch(row.name, section.name));
2846
+ if (match) unbound.push({
2847
+ path,
2848
+ name: match.name
2849
+ });
2850
+ }
2851
+ if (typed.length) {
2852
+ violations.push({
2853
+ rule: 8,
2854
+ code: 'plan-typed-list',
2855
+ message: capabilities && !capabilities.create.dataset.allowed ? `A section plans a long list of typed-out items. Bind it to a dataset or collection the site has, or plan fewer than ${AI_TYPED_LIST_MIN_ITEMS} items.` : 'A section plans a long list of typed-out items. Bind it to a dataset or collection, or plan a dataset for data the site does not have yet.',
2856
+ paths: typed
2857
+ });
2858
+ }
2859
+ if (unbound.length) {
2860
+ violations.push({
2861
+ rule: 8,
2862
+ code: 'plan-existing-data-unbound',
2863
+ message: `The site already holds "${unbound[0].name}" as data. Bind the section to it instead of copying it into the page.`,
2864
+ paths: unbound.map((entry)=>entry.path)
2865
+ });
2866
+ }
2867
+ return violations;
2868
+ }
2869
+ const SLUG = /^\/?[a-z0-9]+(?:-[a-z0-9]+)*(?:\/[a-z0-9]+(?:-[a-z0-9]+)*)*\/?$|^\/$/;
2870
+ export const AI_SEO_TITLE_MAX = 70;
2871
+ export const AI_SEO_DESCRIPTION_MAX = 170;
2872
+ function slugKey(slug) {
2873
+ return slug.trim().toLowerCase().replace(/^\/+|\/+$/g, '');
2874
+ }
2875
+ /** Rule 10 (plan): every screen has a free, well-formed slug, an SEO title and description. */ export function detectMissingNavAndSeo(plan, inventory) {
2876
+ var _ref;
2877
+ const violations = [];
2878
+ const taken = new Map(((_ref = inventory == null ? void 0 : inventory.screens) != null ? _ref : []).map((screen)=>[
2879
+ slugKey(screen.slug),
2880
+ screen.name
2881
+ ]));
2882
+ const seen = new Set();
2883
+ const badSlugs = [];
2884
+ const collisions = [];
2885
+ const seo = [];
2886
+ plan.screens.forEach((screen, index)=>{
2887
+ const key = slugKey(screen.slug);
2888
+ if (!screen.slug.trim() || !SLUG.test(screen.slug.trim())) {
2889
+ badSlugs.push(`screens[${index}].slug`);
2890
+ } else if (seen.has(key) || taken.has(key)) {
2891
+ collisions.push(`screens[${index}].slug`);
2892
+ }
2893
+ seen.add(key);
2894
+ if (!screen.seoTitle || screen.seoTitle.length > AI_SEO_TITLE_MAX) {
2895
+ seo.push(`screens[${index}].seoTitle`);
2896
+ }
2897
+ if (!screen.seoDescription || screen.seoDescription.length > AI_SEO_DESCRIPTION_MAX) {
2898
+ seo.push(`screens[${index}].seoDescription`);
2899
+ }
2900
+ });
2901
+ if (badSlugs.length) {
2902
+ violations.push({
2903
+ rule: 10,
2904
+ code: 'plan-slug',
2905
+ message: 'A screen has no usable address. Give each one a slug of lowercase words joined by hyphens.',
2906
+ paths: badSlugs
2907
+ });
2908
+ }
2909
+ if (collisions.length) {
2910
+ violations.push({
2911
+ rule: 10,
2912
+ code: 'plan-slug-taken',
2913
+ message: 'A screen reuses an address the site or the plan already uses. Give each screen its own slug.',
2914
+ paths: collisions
2915
+ });
2916
+ }
2917
+ if (seo.length) {
2918
+ violations.push({
2919
+ rule: 10,
2920
+ code: 'plan-seo',
2921
+ message: `A screen is missing its search title or description, or runs past ${AI_SEO_TITLE_MAX} and ${AI_SEO_DESCRIPTION_MAX} characters. Write both for every screen.`,
2922
+ paths: seo
2923
+ });
2924
+ }
2925
+ return violations;
2926
+ }
2927
+ /** Every piece of copy a plan carries, with its path. */ export function aiPlanCopy(plan) {
2928
+ const samples = [];
2929
+ plan.screens.forEach((screen, index)=>{
2930
+ samples.push({
2931
+ at: `screens[${index}].title`,
2932
+ text: screen.title
2933
+ });
2934
+ samples.push({
2935
+ at: `screens[${index}].seoTitle`,
2936
+ text: screen.seoTitle
2937
+ });
2938
+ samples.push({
2939
+ at: `screens[${index}].seoDescription`,
2940
+ text: screen.seoDescription
2941
+ });
2942
+ });
2943
+ plan.create.forEach((entry, index)=>{
2944
+ samples.push({
2945
+ at: `create[${index}].name`,
2946
+ text: entry.name
2947
+ });
2948
+ });
2949
+ return samples;
2950
+ }
2951
+ /** Rule 15 (plan): a screen or template like one the site has starts as its duplicate. */ export function detectMissedDuplicate(plan, inventory) {
2952
+ const kinds = inventoryKinds(inventory);
2953
+ const violations = [];
2954
+ const unresolved = [];
2955
+ plan.screens.forEach((screen, index)=>{
2956
+ var _ref;
2957
+ if (screen.duplicateOf) {
2958
+ if (kinds.get(screen.duplicateOf) !== 'screen') {
2959
+ unresolved.push(`screens[${index}].duplicateOf`);
2960
+ }
2961
+ return;
2962
+ }
2963
+ const nearest = ((_ref = inventory == null ? void 0 : inventory.screens) != null ? _ref : []).find((existing)=>!existing.template && aiNamesMatch(existing.name, screen.title));
2964
+ if (nearest) {
2965
+ violations.push({
2966
+ rule: 15,
2967
+ code: 'plan-missed-duplicate',
2968
+ message: `The site already has "${nearest.name}". Start the new screen from a duplicate of it, which keeps its bindings and SEO, and edit that.`,
2969
+ paths: [
2970
+ `screens[${index}].duplicateOf`
2971
+ ]
2972
+ });
2973
+ }
2974
+ });
2975
+ plan.create.forEach((entry, index)=>{
2976
+ var _ref;
2977
+ if (entry.duplicateOf) {
2978
+ const kind = kinds.get(entry.duplicateOf);
2979
+ if (!kind || kind !== entry.kind && !(entry.kind === 'template' && kind === 'screen')) {
2980
+ unresolved.push(`create[${index}].duplicateOf`);
2981
+ }
2982
+ return;
2983
+ }
2984
+ if (entry.kind !== 'template') return;
2985
+ const nearest = ((_ref = inventory == null ? void 0 : inventory.templates) != null ? _ref : []).find((existing)=>aiNamesMatch(existing.name, entry.name));
2986
+ if (nearest) {
2987
+ violations.push({
2988
+ rule: 15,
2989
+ code: 'plan-missed-duplicate',
2990
+ message: `The site already has the "${nearest.name}" template. Start from a duplicate of it and edit that.`,
2991
+ paths: [
2992
+ `create[${index}].duplicateOf`
2993
+ ]
2994
+ });
2995
+ }
2996
+ });
2997
+ if (unresolved.length) {
2998
+ violations.push({
2999
+ rule: 15,
3000
+ code: 'plan-duplicate-unknown',
3001
+ message: 'The plan duplicates something the site does not have. Duplicate only what the site inventory lists.',
3002
+ paths: unresolved
3003
+ });
3004
+ }
3005
+ return violations;
3006
+ }
3007
+ /**
3008
+ * The Free taste's wall at its worst (AGL-3030, AGL-3070): each exchange of a
3009
+ * Free page job in credits, every answer at its ceiling, as
3010
+ * `jobs/ai-job-free-page.spec.ts` derives them from the page plan and the
3011
+ * layout generation measured live and the requests as they stand. The spec
3012
+ * fails when a figure it derives moves and this does not, so the section cap
3013
+ * below cannot drift from the proof.
3014
+ */ export const AI_FREE_PAGE_WORST_CASE_CREDITS = {
3015
+ plan: 83,
3016
+ layout: 64,
3017
+ firstSection: 44,
3018
+ laterSection: 20,
3019
+ listing: 3
3020
+ };
3021
+ /**
3022
+ * The most sections a Free job fits in `FREE_AI_TASTE_CREDITS_PER_MONTH` at
3023
+ * its worst: the plan, the layouts it builds first, a listing for each page
3024
+ * and the first section's pass, then as many later passes as the rest pays
3025
+ * for; none when that is already past the wall. A component or a form is never
3026
+ * a Free creation, since rule 7 refuses both on a workspace that keeps no
3027
+ * reusable components, so the wall's proof measures neither.
3028
+ */ export function aiFreePageSectionsWithin(creations, credits = AI_FREE_PAGE_WORST_CASE_CREDITS) {
3029
+ var _creations_pages;
3030
+ const before = credits.plan + creations.layouts * credits.layout + ((_creations_pages = creations.pages) != null ? _creations_pages : 1) * credits.listing + credits.firstSection;
3031
+ if (before > FREE_AI_TASTE_CREDITS_PER_MONTH) return 0;
3032
+ return 1 + Math.floor((FREE_AI_TASTE_CREDITS_PER_MONTH - before) / credits.laterSection);
3033
+ }
3034
+ /**
3035
+ * The layouts a Free plan's job builds before its sections: the ones it
3036
+ * creates that the workspace may make, and on a site with no layout where it
3037
+ * may make one, the one rule 2 asks the plan to create.
3038
+ */ function freePlanLayouts(plan, inventory, capabilities) {
3039
+ var _ref;
3040
+ const refused = aiPlanUncreatable(plan, capabilities).filter((entry)=>entry.kind === 'layout').length;
3041
+ const creates = plan.create.filter((entry)=>entry.kind === 'layout').length - refused;
3042
+ const needs = !((_ref = inventory == null ? void 0 : inventory.layouts.length) != null ? _ref : 0) && capabilities.create.layout.allowed ? 1 : 0;
3043
+ return Math.max(creates, needs);
3044
+ }
3045
+ /**
3046
+ * The Free wall (plan): a plan asks for no more sections than the Free taste
3047
+ * pays for at its worst beside what the job builds first (AGL-3070). A live
3048
+ * Free plan asked for eight sections beside its layout, where the wall's worst
3049
+ * case pays for six, and fit only because every exchange ran under its
3050
+ * ceiling; one that does not runs out of credits with its page half built. The
3051
+ * cap only lowers what a plan may ask for, from the same figures the wall is
3052
+ * proven with, so no plan the proof admits is refused. It is no building rule,
3053
+ * so it names none.
3054
+ */ export function detectPlanOverFreeWall(plan, inventory, capabilities) {
3055
+ if (!(capabilities == null ? void 0 : capabilities.freeTaste)) return [];
3056
+ const asked = plan.screens.reduce((sum, screen)=>sum + screen.sections.length, 0);
3057
+ const layouts = freePlanLayouts(plan, inventory, capabilities);
3058
+ const pages = Math.max(plan.screens.length, 1);
3059
+ const fits = aiFreePageSectionsWithin({
3060
+ layouts,
3061
+ pages
3062
+ });
3063
+ if (asked <= fits) return [];
3064
+ const job = pages > 1 ? `a Free plan of ${pages} pages` : 'a Free page';
3065
+ const beside = layouts ? ` that creates ${layouts > 1 ? `${layouts} layouts` : 'its layout'}` : '';
3066
+ return [
3067
+ {
3068
+ rule: null,
3069
+ code: 'plan-over-free-wall',
3070
+ message: `This plan asks for ${asked} sections, and ${job}${beside} fits ${fits} in the ${FREE_AI_TASTE_CREDITS_PER_MONTH} AI credits a Free workspace has a month. Plan at most ${fits}: draw a list's repeated items in one section, and leave out a section the brief does not ask for.`,
3071
+ paths: plan.screens.map((_, index)=>`screens[${index}].sections`)
3072
+ }
3073
+ ];
3074
+ }
3075
+ /**
3076
+ * Rule 2 (plan): a layout the plan creates lists the regions it has, and
3077
+ * every one is a region this platform builds (AGL-3024).
3078
+ *
3079
+ * A layout has no properties, so its `fields` are its REGIONS — the promise
3080
+ * the build is later held to. A word the vocabulary does not hold is refused
3081
+ * HERE, on the plan, where the re-ask costs a sentence: a promise nothing can
3082
+ * look for would otherwise reach the build as a commitment no check can
3083
+ * settle, and a build that cannot settle its own plan stops for a person
3084
+ * instead of reporting itself done.
3085
+ */ export function detectPlanUnreadableRegions(plan) {
3086
+ const unreadable = [];
3087
+ plan.create.forEach((entry, index)=>{
3088
+ if (entry.kind !== 'layout') return;
3089
+ entry.fields.forEach((field, fieldIndex)=>{
3090
+ if (!aiLayoutRegionOf(field)) {
3091
+ unreadable.push({
3092
+ word: field,
3093
+ path: `create[${index}].fields[${fieldIndex}]`
3094
+ });
3095
+ }
3096
+ });
3097
+ });
3098
+ if (!unreadable.length) return [];
3099
+ const one = unreadable.length === 1;
3100
+ return [
3101
+ {
3102
+ rule: 2,
3103
+ code: 'plan-layout-region-unknown',
3104
+ message: `A layout lists ${unreadable.map((entry)=>`"${entry.word}"`).join(', ')} among its regions, and a layout is built out of ${AI_LAYOUT_REGION_NAMES.join(', ')}. Name ${one ? 'it' : 'them'} with one of those, or leave ${one ? 'it' : 'them'} off the layout.`,
3105
+ paths: unreadable.map((entry)=>entry.path)
3106
+ }
3107
+ ];
3108
+ }
3109
+ /**
3110
+ * Every plan rule, against the site inventory the plan was made from and,
3111
+ * where the job read them, what it may create there (AGL-3030). `null`
3112
+ * capabilities restrict nothing: the doctrine applies whole.
3113
+ */ export function validateAiBuildPlan(plan, inventory, framing = null, capabilities = null) {
3114
+ return [
3115
+ ...detectPlanRepeats(plan, inventory, capabilities),
3116
+ ...detectPlanSplitLists(plan, inventory, capabilities),
3117
+ ...detectPlanOverFreeWall(plan, inventory, capabilities),
3118
+ ...detectPlanLayoutRegions(plan, inventory, capabilities),
3119
+ ...detectPlanUnreadableRegions(plan),
3120
+ ...detectPlanInlineForms(plan, inventory, capabilities),
3121
+ ...detectUntemplatedSimilarPages(plan, inventory),
3122
+ ...detectPlanLiteralColors(plan),
3123
+ ...detectCreateBeforeReuse(plan, inventory),
3124
+ ...detectPlanUncreatable(plan, capabilities),
3125
+ ...detectPlanUndeclaredCreations(plan, inventory, capabilities),
3126
+ ...detectPlanTypedData(plan, inventory, capabilities),
3127
+ ...detectMissingNavAndSeo(plan, inventory),
3128
+ ...detectOffVoiceCopy(aiPlanCopy(plan), framing),
3129
+ ...detectMissedDuplicate(plan, inventory)
3130
+ ];
3131
+ }
3132
+
3133
+ //# sourceMappingURL=ai-doctrine-validators.js.map