@aglyn/plugins-ai 1.0.0-beta.218 → 1.0.0-beta.220

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 (311) hide show
  1. package/README.md +2 -2
  2. package/package.json +18 -18
  3. package/src/index.d.ts +1 -0
  4. package/src/index.js.map +1 -1
  5. package/src/lib/activity/ai-activity-actions.js +8 -1
  6. package/src/lib/activity/ai-activity-actions.js.map +1 -1
  7. package/src/lib/billing/ai-month-meter.d.ts +51 -0
  8. package/src/lib/billing/ai-month-meter.js +89 -0
  9. package/src/lib/billing/ai-month-meter.js.map +1 -0
  10. package/src/lib/billing/ai-overage-close.d.ts +11 -33
  11. package/src/lib/billing/ai-overage-close.js +4 -3
  12. package/src/lib/billing/ai-overage-close.js.map +1 -1
  13. package/src/lib/billing/ai-overage-cutover.d.ts +28 -12
  14. package/src/lib/billing/ai-overage-cutover.js +1 -28
  15. package/src/lib/billing/ai-overage-cutover.js.map +1 -1
  16. package/src/lib/billing/ai-overage-ledger.js +1 -1
  17. package/src/lib/billing/ai-overage-ledger.js.map +1 -1
  18. package/src/lib/billing/ai-overage-standing.js +1 -1
  19. package/src/lib/billing/ai-overage-standing.js.map +1 -1
  20. package/src/lib/components/ai-allotment-editor.component.js +1 -1
  21. package/src/lib/components/ai-allotment-editor.component.js.map +1 -1
  22. package/src/lib/components/ai-assist-provider.component.js +5 -4
  23. package/src/lib/components/ai-assist-provider.component.js.map +1 -1
  24. package/src/lib/components/ai-automation-zones.d.ts +63 -0
  25. package/src/lib/components/ai-automation-zones.js +31 -0
  26. package/src/lib/components/ai-automation-zones.js.map +1 -0
  27. package/src/lib/components/ai-brief-dialog.component.js +3 -3
  28. package/src/lib/components/ai-brief-dialog.component.js.map +1 -1
  29. package/src/lib/components/ai-describe-automation.component.d.ts +1 -1
  30. package/src/lib/components/ai-describe-automation.component.js.map +1 -1
  31. package/src/lib/components/ai-describe-button.component.d.ts +6 -3
  32. package/src/lib/components/ai-describe-button.component.js +4 -1
  33. package/src/lib/components/ai-describe-button.component.js.map +1 -1
  34. package/src/lib/components/ai-experiment-cards.component.js +1 -1
  35. package/src/lib/components/ai-experiment-cards.component.js.map +1 -1
  36. package/src/lib/components/ai-explain-automation.component.d.ts +1 -1
  37. package/src/lib/components/ai-explain-automation.component.js.map +1 -1
  38. package/src/lib/components/ai-job-plan.component.d.ts +8 -0
  39. package/src/lib/components/ai-job-plan.component.js +21 -4
  40. package/src/lib/components/ai-job-plan.component.js.map +1 -1
  41. package/src/lib/components/ai-permissions-on-host.component.d.ts +12 -2
  42. package/src/lib/components/ai-permissions-on-host.component.js +12 -3
  43. package/src/lib/components/ai-permissions-on-host.component.js.map +1 -1
  44. package/src/lib/components/ai-product-copy-card.component.d.ts +1 -1
  45. package/src/lib/components/ai-product-copy-card.component.js.map +1 -1
  46. package/src/lib/components/ai-product-import-option.component.d.ts +1 -1
  47. package/src/lib/components/ai-product-import-option.component.js.map +1 -1
  48. package/src/lib/components/ai-product-zones.d.ts +124 -0
  49. package/src/lib/components/ai-product-zones.js +32 -0
  50. package/src/lib/components/ai-product-zones.js.map +1 -0
  51. package/src/lib/components/ai-products-hub-card.component.d.ts +1 -1
  52. package/src/lib/components/ai-products-hub-card.component.js.map +1 -1
  53. package/src/lib/components/ai-products-proposals.component.d.ts +1 -1
  54. package/src/lib/components/ai-products-proposals.component.js.map +1 -1
  55. package/src/lib/components/ai-seo-audit-card.component.js +35 -92
  56. package/src/lib/components/ai-seo-audit-card.component.js.map +1 -1
  57. package/src/lib/components/ai-seo-fields-card.component.js.map +1 -1
  58. package/src/lib/components/ai-theme-proposal-card.component.js.map +1 -1
  59. package/src/lib/components/assist-jobs-drawer.component.js +21 -15
  60. package/src/lib/components/assist-jobs-drawer.component.js.map +1 -1
  61. package/src/lib/components/assist-panel.component.js +2 -2
  62. package/src/lib/components/assist-panel.component.js.map +1 -1
  63. package/src/lib/components/assist-signals-page.component.js +2 -2
  64. package/src/lib/components/assist-signals-page.component.js.map +1 -1
  65. package/src/lib/components/billing-ai-allotments.component.js +1 -1
  66. package/src/lib/components/billing-ai-allotments.component.js.map +1 -1
  67. package/src/lib/components/billing-assist-overage-card.component.js +1 -1
  68. package/src/lib/components/billing-assist-overage-card.component.js.map +1 -1
  69. package/src/lib/constants.d.ts +5 -0
  70. package/src/lib/constants.js +4 -0
  71. package/src/lib/constants.js.map +1 -1
  72. package/src/lib/declarations.js +2 -0
  73. package/src/lib/declarations.js.map +1 -1
  74. package/src/lib/declarations.server.js +24 -10
  75. package/src/lib/declarations.server.js.map +1 -1
  76. package/src/lib/insights/ai-dataset-catalog.d.ts +49 -0
  77. package/src/lib/insights/ai-dataset-catalog.js +57 -0
  78. package/src/lib/insights/ai-dataset-catalog.js.map +1 -0
  79. package/src/lib/insights/ai-figure-readers.d.ts +6 -39
  80. package/src/lib/insights/ai-figure-readers.js +2 -391
  81. package/src/lib/insights/ai-figure-readers.js.map +1 -1
  82. package/src/lib/insights/ai-insight-digest.d.ts +10 -1
  83. package/src/lib/insights/ai-insight-digest.js +16 -4
  84. package/src/lib/insights/ai-insight-digest.js.map +1 -1
  85. package/src/lib/insights/ai-insight-readers.d.ts +1 -1
  86. package/src/lib/insights/ai-insight-readers.js.map +1 -1
  87. package/src/lib/jobs/ai-email-bindings.d.ts +28 -21
  88. package/src/lib/jobs/ai-email-bindings.js +75 -42
  89. package/src/lib/jobs/ai-email-bindings.js.map +1 -1
  90. package/src/lib/jobs/ai-job-campaign-step.d.ts +0 -2
  91. package/src/lib/jobs/ai-job-campaign-step.js +5 -10
  92. package/src/lib/jobs/ai-job-campaign-step.js.map +1 -1
  93. package/src/lib/jobs/ai-job-component-step.d.ts +6 -1
  94. package/src/lib/jobs/ai-job-component-step.js +32 -9
  95. package/src/lib/jobs/ai-job-component-step.js.map +1 -1
  96. package/src/lib/jobs/ai-job-draft-ids.js +11 -0
  97. package/src/lib/jobs/ai-job-draft-ids.js.map +1 -1
  98. package/src/lib/jobs/ai-job-drafts.js +1 -1
  99. package/src/lib/jobs/ai-job-drafts.js.map +1 -1
  100. package/src/lib/jobs/ai-job-email-step.d.ts +5 -3
  101. package/src/lib/jobs/ai-job-email-step.js +11 -4
  102. package/src/lib/jobs/ai-job-email-step.js.map +1 -1
  103. package/src/lib/jobs/ai-job-experiment-step.d.ts +1 -1
  104. package/src/lib/jobs/ai-job-experiment-step.js +1 -1
  105. package/src/lib/jobs/ai-job-experiment-step.js.map +1 -1
  106. package/src/lib/jobs/ai-job-form-step.d.ts +17 -1
  107. package/src/lib/jobs/ai-job-form-step.js +60 -8
  108. package/src/lib/jobs/ai-job-form-step.js.map +1 -1
  109. package/src/lib/jobs/ai-job-generation.d.ts +22 -1
  110. package/src/lib/jobs/ai-job-generation.js +63 -3
  111. package/src/lib/jobs/ai-job-generation.js.map +1 -1
  112. package/src/lib/jobs/ai-job-insight-step.js +1 -1
  113. package/src/lib/jobs/ai-job-insight-step.js.map +1 -1
  114. package/src/lib/jobs/ai-job-layout-step.d.ts +15 -4
  115. package/src/lib/jobs/ai-job-layout-step.js +46 -13
  116. package/src/lib/jobs/ai-job-layout-step.js.map +1 -1
  117. package/src/lib/jobs/ai-job-page-sections.d.ts +4 -2
  118. package/src/lib/jobs/ai-job-page-sections.js +7 -4
  119. package/src/lib/jobs/ai-job-page-sections.js.map +1 -1
  120. package/src/lib/jobs/ai-job-page-step.js +7 -2
  121. package/src/lib/jobs/ai-job-page-step.js.map +1 -1
  122. package/src/lib/jobs/ai-job-plan-conformance.d.ts +5 -0
  123. package/src/lib/jobs/ai-job-plan-conformance.js +40 -1
  124. package/src/lib/jobs/ai-job-plan-conformance.js.map +1 -1
  125. package/src/lib/jobs/ai-job-plan-creations.d.ts +80 -0
  126. package/src/lib/jobs/ai-job-plan-creations.js +199 -0
  127. package/src/lib/jobs/ai-job-plan-creations.js.map +1 -0
  128. package/src/lib/jobs/ai-job-plan-step.d.ts +11 -2
  129. package/src/lib/jobs/ai-job-plan-step.js +49 -23
  130. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  131. package/src/lib/jobs/ai-job-plugin-drafts.d.ts +14 -6
  132. package/src/lib/jobs/ai-job-plugin-drafts.js +12 -4
  133. package/src/lib/jobs/ai-job-plugin-drafts.js.map +1 -1
  134. package/src/lib/jobs/ai-job-products-step.d.ts +7 -0
  135. package/src/lib/jobs/ai-job-products-step.js +54 -32
  136. package/src/lib/jobs/ai-job-products-step.js.map +1 -1
  137. package/src/lib/jobs/ai-job-seo-step.d.ts +3 -24
  138. package/src/lib/jobs/ai-job-seo-step.js +35 -108
  139. package/src/lib/jobs/ai-job-seo-step.js.map +1 -1
  140. package/src/lib/jobs/ai-job-site-step.d.ts +2 -0
  141. package/src/lib/jobs/ai-job-site-step.js +6 -6
  142. package/src/lib/jobs/ai-job-site-step.js.map +1 -1
  143. package/src/lib/jobs/ai-job-template-step.d.ts +9 -6
  144. package/src/lib/jobs/ai-job-template-step.js +73 -15
  145. package/src/lib/jobs/ai-job-template-step.js.map +1 -1
  146. package/src/lib/jobs/ai-job-theme-step.d.ts +1 -1
  147. package/src/lib/jobs/ai-job-theme-step.js +1 -1
  148. package/src/lib/jobs/ai-job-theme-step.js.map +1 -1
  149. package/src/lib/jobs/ai-job-workflow-step.js +17 -9
  150. package/src/lib/jobs/ai-job-workflow-step.js.map +1 -1
  151. package/src/lib/jobs/ai-jobs.js +1 -1
  152. package/src/lib/jobs/ai-jobs.js.map +1 -1
  153. package/src/lib/jobs/ai-workflow-records.d.ts +20 -11
  154. package/src/lib/jobs/ai-workflow-records.js +80 -43
  155. package/src/lib/jobs/ai-workflow-records.js.map +1 -1
  156. package/src/lib/model/ai-allotments.d.ts +27 -6
  157. package/src/lib/model/ai-allotments.js +54 -14
  158. package/src/lib/model/ai-allotments.js.map +1 -1
  159. package/src/lib/model/ai-automation-outline.d.ts +21 -2
  160. package/src/lib/model/ai-automation-outline.js.map +1 -1
  161. package/src/lib/model/ai-build-plan.d.ts +57 -0
  162. package/src/lib/model/ai-build-plan.js +116 -7
  163. package/src/lib/model/ai-build-plan.js.map +1 -1
  164. package/src/lib/model/ai-job-creations.d.ts +55 -0
  165. package/src/lib/model/ai-job-creations.js +85 -0
  166. package/src/lib/model/ai-job-creations.js.map +1 -0
  167. package/src/lib/model/ai-page-job.d.ts +3 -2
  168. package/src/lib/model/ai-page-job.js +4 -3
  169. package/src/lib/model/ai-page-job.js.map +1 -1
  170. package/src/lib/model/ai-seo.d.ts +11 -51
  171. package/src/lib/model/ai-seo.js +0 -17
  172. package/src/lib/model/ai-seo.js.map +1 -1
  173. package/src/lib/model/ai-site-inventory.js +1 -1
  174. package/src/lib/model/ai-site-inventory.js.map +1 -1
  175. package/src/lib/model/ai-site-job.d.ts +4 -2
  176. package/src/lib/model/ai-site-job.js +8 -4
  177. package/src/lib/model/ai-site-job.js.map +1 -1
  178. package/src/lib/model/ai-template-subjects.d.ts +10 -0
  179. package/src/lib/model/ai-template-subjects.js +16 -2
  180. package/src/lib/model/ai-template-subjects.js.map +1 -1
  181. package/src/lib/model/ai-theme-proposal.d.ts +1 -1
  182. package/src/lib/model/ai-theme-proposal.js +1 -1
  183. package/src/lib/model/ai-theme-proposal.js.map +1 -1
  184. package/src/lib/model/ai-tokens.js +1 -1
  185. package/src/lib/model/ai-tokens.js.map +1 -1
  186. package/src/lib/model/ai-usage-by-user.js +1 -1
  187. package/src/lib/model/ai-usage-by-user.js.map +1 -1
  188. package/src/lib/model/ai-workflow-job.d.ts +1 -3
  189. package/src/lib/model/ai-workflow-job.js +1 -2
  190. package/src/lib/model/ai-workflow-job.js.map +1 -1
  191. package/src/lib/plan-entitlements.d.ts +149 -0
  192. package/src/lib/plan-entitlements.js +168 -0
  193. package/src/lib/plan-entitlements.js.map +1 -0
  194. package/src/lib/plugin.js +1 -1
  195. package/src/lib/plugin.js.map +1 -1
  196. package/src/lib/providers/catalog.js +1 -1
  197. package/src/lib/providers/catalog.js.map +1 -1
  198. package/src/lib/providers/model-choice.js +1 -1
  199. package/src/lib/providers/model-choice.js.map +1 -1
  200. package/src/lib/runtime/ai-device-audit.d.ts +1 -1
  201. package/src/lib/runtime/ai-device-audit.js +1 -1
  202. package/src/lib/runtime/ai-device-audit.js.map +1 -1
  203. package/src/lib/runtime/ai-doctrine-validators.d.ts +25 -4
  204. package/src/lib/runtime/ai-doctrine-validators.js +178 -21
  205. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  206. package/src/lib/runtime/ai-doctrine.js +5 -5
  207. package/src/lib/runtime/ai-doctrine.js.map +1 -1
  208. package/src/lib/runtime/ai-eval-live.js +30 -15
  209. package/src/lib/runtime/ai-eval-live.js.map +1 -1
  210. package/src/lib/runtime/ai-eval.d.ts +9 -2
  211. package/src/lib/runtime/ai-eval.js +29 -2
  212. package/src/lib/runtime/ai-eval.js.map +1 -1
  213. package/src/lib/runtime/ai-gate.js +1 -1
  214. package/src/lib/runtime/ai-gate.js.map +1 -1
  215. package/src/lib/runtime/ai-node-tree.js +3 -3
  216. package/src/lib/runtime/ai-node-tree.js.map +1 -1
  217. package/src/lib/runtime/ai-palette.d.ts +7 -1
  218. package/src/lib/runtime/ai-palette.generated.d.ts +1 -1
  219. package/src/lib/runtime/ai-palette.generated.js +16 -13
  220. package/src/lib/runtime/ai-palette.generated.js.map +1 -1
  221. package/src/lib/runtime/ai-palette.js +7 -1
  222. package/src/lib/runtime/ai-palette.js.map +1 -1
  223. package/src/lib/runtime/seo-audit.d.ts +13 -63
  224. package/src/lib/runtime/seo-audit.js +22 -207
  225. package/src/lib/runtime/seo-audit.js.map +1 -1
  226. package/src/lib/runtime/site-inventory.js +29 -23
  227. package/src/lib/runtime/site-inventory.js.map +1 -1
  228. package/src/lib/server/ai-admin-org.js +1 -1
  229. package/src/lib/server/ai-admin-org.js.map +1 -1
  230. package/src/lib/server/ai-admin-orgs-spend.js +1 -1
  231. package/src/lib/server/ai-admin-orgs-spend.js.map +1 -1
  232. package/src/lib/server/ai-admin-signals.js +1 -1
  233. package/src/lib/server/ai-admin-signals.js.map +1 -1
  234. package/src/lib/server/ai-allotments.js +1 -1
  235. package/src/lib/server/ai-allotments.js.map +1 -1
  236. package/src/lib/server/ai-assist-prompts.js +3 -3
  237. package/src/lib/server/ai-assist-prompts.js.map +1 -1
  238. package/src/lib/server/ai-assist.js +2 -2
  239. package/src/lib/server/ai-assist.js.map +1 -1
  240. package/src/lib/server/assist-chat.d.ts +1 -1
  241. package/src/lib/server/assist-chat.js +13 -12
  242. package/src/lib/server/assist-chat.js.map +1 -1
  243. package/src/lib/server/assist-view-context.js +23 -23
  244. package/src/lib/server/assist-view-context.js.map +1 -1
  245. package/src/lib/server/billing-credits.d.ts +5 -0
  246. package/src/lib/server/billing-credits.js +8 -2
  247. package/src/lib/server/billing-credits.js.map +1 -1
  248. package/src/lib/server/billing-overage.js +2 -2
  249. package/src/lib/server/billing-overage.js.map +1 -1
  250. package/src/lib/server.d.ts +1 -0
  251. package/src/lib/server.js.map +1 -1
  252. package/src/lib/testing/stand-in-automation-index.d.ts +20 -0
  253. package/src/lib/testing/stand-in-automation-index.js +102 -0
  254. package/src/lib/testing/stand-in-automation-index.js.map +1 -0
  255. package/src/lib/testing/stand-in-dataset-index.d.ts +20 -0
  256. package/src/lib/testing/stand-in-dataset-index.js +80 -0
  257. package/src/lib/testing/stand-in-dataset-index.js.map +1 -0
  258. package/src/lib/testing/stand-in-list-send-time.d.ts +4 -0
  259. package/src/lib/testing/stand-in-list-send-time.js +77 -0
  260. package/src/lib/testing/stand-in-list-send-time.js.map +1 -0
  261. package/src/lib/testing/stand-in-pipeline-index.d.ts +32 -0
  262. package/src/lib/testing/stand-in-pipeline-index.js +59 -0
  263. package/src/lib/testing/stand-in-pipeline-index.js.map +1 -0
  264. package/src/lib/testing/stand-in-product-index.d.ts +20 -0
  265. package/src/lib/testing/stand-in-product-index.js +105 -0
  266. package/src/lib/testing/stand-in-product-index.js.map +1 -0
  267. package/src/lib/tools/ai-component-tool.js +2 -2
  268. package/src/lib/tools/ai-component-tool.js.map +1 -1
  269. package/src/lib/tools/ai-duplicate-resource-tool.js +1 -1
  270. package/src/lib/tools/ai-duplicate-resource-tool.js.map +1 -1
  271. package/src/lib/tools/ai-seo-tool.d.ts +6 -18
  272. package/src/lib/tools/ai-seo-tool.js +7 -38
  273. package/src/lib/tools/ai-seo-tool.js.map +1 -1
  274. package/src/lib/tools/ai-workflow-tool.d.ts +1 -1
  275. package/src/lib/tools/ai-workflow-tool.js +1 -1
  276. package/src/lib/tools/ai-workflow-tool.js.map +1 -1
  277. package/src/lib/usage/ai-allotment-alerts.d.ts +10 -7
  278. package/src/lib/usage/ai-allotment-alerts.js +39 -15
  279. package/src/lib/usage/ai-allotment-alerts.js.map +1 -1
  280. package/src/lib/usage/ai-quota-checks.d.ts +37 -0
  281. package/src/lib/usage/ai-quota-checks.js +69 -0
  282. package/src/lib/usage/ai-quota-checks.js.map +1 -0
  283. package/src/lib/usage/ai-usage-alerts.js +22 -10
  284. package/src/lib/usage/ai-usage-alerts.js.map +1 -1
  285. package/src/lib/usage/ai-usage-by-user.js +1 -1
  286. package/src/lib/usage/ai-usage-by-user.js.map +1 -1
  287. package/src/lib/usage/assist-ceiling.d.ts +2 -2
  288. package/src/lib/usage/assist-ceiling.js +4 -4
  289. package/src/lib/usage/assist-ceiling.js.map +1 -1
  290. package/src/lib/usage/assist-credits.d.ts +494 -0
  291. package/src/lib/usage/assist-credits.js +524 -0
  292. package/src/lib/usage/assist-credits.js.map +1 -0
  293. package/src/lib/usage/assist-free-taste.d.ts +1 -1
  294. package/src/lib/usage/assist-free-taste.js +3 -2
  295. package/src/lib/usage/assist-free-taste.js.map +1 -1
  296. package/src/lib/usage/assist-signal-mining.js +1 -1
  297. package/src/lib/usage/assist-signal-mining.js.map +1 -1
  298. package/src/lib/usage/assist-spend-guards.d.ts +153 -0
  299. package/src/lib/usage/assist-spend-guards.js +182 -0
  300. package/src/lib/usage/assist-spend-guards.js.map +1 -0
  301. package/src/lib/usage/assist-usage.d.ts +1 -1
  302. package/src/lib/usage/assist-usage.js +1 -1
  303. package/src/lib/usage/assist-usage.js.map +1 -1
  304. package/src/lib/usage/staff-org-ai.js +1 -1
  305. package/src/lib/usage/staff-org-ai.js.map +1 -1
  306. package/src/lib/usage-axes.d.ts +49 -0
  307. package/src/lib/usage-axes.js +108 -0
  308. package/src/lib/usage-axes.js.map +1 -0
  309. package/src/lib/runtime/seo-page-facts.d.ts +0 -61
  310. package/src/lib/runtime/seo-page-facts.js +0 -150
  311. package/src/lib/runtime/seo-page-facts.js.map +0 -1
@@ -0,0 +1,59 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ import { registerPluginRecordIndex } from "@aglyn/aglyn/plugin-manager/plugin-record-index";
17
+ import { unregisterPluginServices } from "@aglyn/aglyn/plugin-manager/plugin-services";
18
+ /**
19
+ * The `pipeline` index the plugin that keeps pipelines publishes (AGL-3080),
20
+ * stood in with the answers a spec hands it — this plugin may not load the
21
+ * CRM that registers the real one. It answers in the shape the CRM's
22
+ * `pipeline-record-index.ts` documents (live pipelines, `facts.stages`), and
23
+ * records every question, so a spec can say what was asked. What the CRM
24
+ * itself answers is held in its own spec.
25
+ */ const OWNER = 'crm';
26
+ /** Registers the index, answering `pipelines` to every site of every org. */ export function standInPipelineIndex(pipelines) {
27
+ const stood = {
28
+ asked: []
29
+ };
30
+ const records = pipelines.map((pipeline)=>({
31
+ id: pipeline.id,
32
+ name: pipeline.name,
33
+ facts: {
34
+ stages: pipeline.stages
35
+ }
36
+ }));
37
+ registerPluginRecordIndex('pipeline', {
38
+ async list (request) {
39
+ stood.asked.push(request);
40
+ return {
41
+ records: records.slice(0, request.limit),
42
+ truncated: records.length > request.limit
43
+ };
44
+ },
45
+ async get (request) {
46
+ var _records_find;
47
+ stood.asked.push(request);
48
+ return (_records_find = records.find((record)=>record.id === request.id)) != null ? _records_find : null;
49
+ }
50
+ }, {
51
+ pluginId: OWNER
52
+ });
53
+ return stood;
54
+ }
55
+ /** Forgets it, so the next spec starts with no plugin keeping pipelines. */ export function removeStandInPipelineIndex() {
56
+ unregisterPluginServices(OWNER);
57
+ }
58
+
59
+ //# sourceMappingURL=stand-in-pipeline-index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/testing/stand-in-pipeline-index.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n registerPluginRecordIndex,\n type PluginIndexedRecord,\n type PluginRecordIndexScope,\n} from '@aglyn/aglyn/plugin-manager/plugin-record-index'\nimport { unregisterPluginServices } from '@aglyn/aglyn/plugin-manager/plugin-services'\n\n/**\n * The `pipeline` index the plugin that keeps pipelines publishes (AGL-3080),\n * stood in with the answers a spec hands it — this plugin may not load the\n * CRM that registers the real one. It answers in the shape the CRM's\n * `pipeline-record-index.ts` documents (live pipelines, `facts.stages`), and\n * records every question, so a spec can say what was asked. What the CRM\n * itself answers is held in its own spec.\n */\n\nconst OWNER = 'crm'\n\nexport interface StoodInPipelineIndex {\n /** Every `list` and `get` asked, in order. */\n asked: Array<PluginRecordIndexScope & { limit?: number; id?: string }>\n}\n\n/** Registers the index, answering `pipelines` to every site of every org. */\nexport function standInPipelineIndex(\n pipelines: ReadonlyArray<{ id: string; name: string; stages: ReadonlyArray<Record<string, unknown>> }>,\n): StoodInPipelineIndex {\n const stood: StoodInPipelineIndex = { asked: [] }\n const records: PluginIndexedRecord[] = pipelines.map((pipeline) => ({\n id: pipeline.id,\n name: pipeline.name,\n facts: { stages: pipeline.stages },\n }))\n registerPluginRecordIndex(\n 'pipeline',\n {\n async list(request) {\n stood.asked.push(request)\n return { records: records.slice(0, request.limit), truncated: records.length > request.limit }\n },\n async get(request) {\n stood.asked.push(request)\n return records.find((record) => record.id === request.id) ?? null\n },\n },\n { pluginId: OWNER },\n )\n return stood\n}\n\n/** Forgets it, so the next spec starts with no plugin keeping pipelines. */\nexport function removeStandInPipelineIndex(): void {\n unregisterPluginServices(OWNER)\n}\n"],"names":["registerPluginRecordIndex","unregisterPluginServices","OWNER","standInPipelineIndex","pipelines","stood","asked","records","map","pipeline","id","name","facts","stages","list","request","push","slice","limit","truncated","length","get","find","record","pluginId","removeStandInPipelineIndex"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,yBAAyB,QAGpB,kDAAiD;AACxD,SAASC,wBAAwB,QAAQ,8CAA6C;AAEtF;;;;;;;CAOC,GAED,MAAMC,QAAQ;AAOd,2EAA2E,GAC3E,OAAO,SAASC,qBACdC,SAAsG;IAEtG,MAAMC,QAA8B;QAAEC,OAAO,EAAE;IAAC;IAChD,MAAMC,UAAiCH,UAAUI,GAAG,CAAC,CAACC,WAAc,CAAA;YAClEC,IAAID,SAASC,EAAE;YACfC,MAAMF,SAASE,IAAI;YACnBC,OAAO;gBAAEC,QAAQJ,SAASI,MAAM;YAAC;QACnC,CAAA;IACAb,0BACE,YACA;QACE,MAAMc,MAAKC,OAAO;YAChBV,MAAMC,KAAK,CAACU,IAAI,CAACD;YACjB,OAAO;gBAAER,SAASA,QAAQU,KAAK,CAAC,GAAGF,QAAQG,KAAK;gBAAGC,WAAWZ,QAAQa,MAAM,GAAGL,QAAQG,KAAK;YAAC;QAC/F;QACA,MAAMG,KAAIN,OAAO;gBAERR;YADPF,MAAMC,KAAK,CAACU,IAAI,CAACD;YACjB,QAAOR,gBAAAA,QAAQe,IAAI,CAAC,CAACC,SAAWA,OAAOb,EAAE,KAAKK,QAAQL,EAAE,aAAjDH,gBAAsD;QAC/D;IACF,GACA;QAAEiB,UAAUtB;IAAM;IAEpB,OAAOG;AACT;AAEA,0EAA0E,GAC1E,OAAO,SAASoB;IACdxB,yBAAyBC;AAC3B"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ /** Registers both indexes, reading through `firestore`. */
18
+ export declare function standInProductIndexes(firestore: FirebaseFirestore.Firestore): void;
19
+ /** Forgets both, so the next spec starts with no plugin keeping products. */
20
+ export declare function removeStandInProductIndexes(): void;
@@ -0,0 +1,105 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ import { registerPluginRecordIndex } from "@aglyn/aglyn/plugin-manager/plugin-record-index";
17
+ import { unregisterPluginServices } from "@aglyn/aglyn/plugin-manager/plugin-services";
18
+ /**
19
+ * The product and category indexes a plugin that keeps products publishes
20
+ * (AGL-3080), stood in over a spec's own Firestore double — this plugin may
21
+ * not load the commerce plugin that registers the real ones. It answers in
22
+ * the shape the commerce plugin's `product-record-index.ts` documents: live,
23
+ * named records only, with the product facts its docblock lists. What the
24
+ * commerce plugin itself answers is held in its own spec.
25
+ */ const OWNER = 'commerce';
26
+ const str = (value)=>typeof value === 'string' ? value : '';
27
+ const strings = (value)=>(Array.isArray(value) ? value : []).map(str).filter(Boolean);
28
+ function product(id, data) {
29
+ var _data_seo;
30
+ if (!data || data['deletedAt'] != null || !str(data['name'])) return null;
31
+ const seo = (_data_seo = data['seo']) != null ? _data_seo : {};
32
+ const mediaUrls = Array.isArray(data['mediaUrls']) ? data['mediaUrls'] : [];
33
+ return {
34
+ id,
35
+ name: str(data['name']),
36
+ facts: {
37
+ type: data['type'],
38
+ description: str(data['description']),
39
+ tags: strings(data['tags']),
40
+ categoryIds: strings(data['categoryIds']),
41
+ options: (Array.isArray(data['options']) ? data['options'] : []).map((option)=>{
42
+ const record = option != null ? option : {};
43
+ return {
44
+ name: str(record['name']),
45
+ values: strings(record['values'])
46
+ };
47
+ }),
48
+ imageUrl: str(mediaUrls[0]) || str(data['imageUrl']) || null,
49
+ seoTitle: str(seo['title']),
50
+ seoDescription: str(seo['description'])
51
+ }
52
+ };
53
+ }
54
+ /** Registers both indexes, reading through `firestore`. */ export function standInProductIndexes(firestore) {
55
+ const site = (hostId, name)=>firestore.collection('hosts').doc(hostId).collection(name);
56
+ registerPluginRecordIndex('product', {
57
+ async list ({ hostId, limit }) {
58
+ if (!hostId) return {
59
+ records: [],
60
+ truncated: false
61
+ };
62
+ const snapshot = await site(hostId, 'products').limit(limit + 1).get();
63
+ const records = snapshot.docs.map((doc)=>product(doc.id, doc.data())).filter((record)=>record !== null);
64
+ return {
65
+ records: records.slice(0, limit),
66
+ truncated: snapshot.docs.length > limit
67
+ };
68
+ },
69
+ async get ({ hostId, id }) {
70
+ if (!hostId) return null;
71
+ const snapshot = await site(hostId, 'products').doc(id).get();
72
+ return snapshot.exists ? product(snapshot.id, snapshot.data()) : null;
73
+ }
74
+ }, {
75
+ pluginId: OWNER
76
+ });
77
+ registerPluginRecordIndex('productCategory', {
78
+ async list ({ hostId, limit }) {
79
+ if (!hostId) return {
80
+ records: [],
81
+ truncated: false
82
+ };
83
+ const snapshot = await site(hostId, 'productCategories').limit(limit + 1).get();
84
+ const records = snapshot.docs.map((doc)=>({
85
+ id: doc.id,
86
+ name: str(doc.get('name')),
87
+ facts: {}
88
+ })).filter((record)=>record.name);
89
+ return {
90
+ records: records.slice(0, limit),
91
+ truncated: snapshot.docs.length > limit
92
+ };
93
+ },
94
+ async get () {
95
+ return null;
96
+ }
97
+ }, {
98
+ pluginId: OWNER
99
+ });
100
+ }
101
+ /** Forgets both, so the next spec starts with no plugin keeping products. */ export function removeStandInProductIndexes() {
102
+ unregisterPluginServices(OWNER);
103
+ }
104
+
105
+ //# sourceMappingURL=stand-in-product-index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/testing/stand-in-product-index.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n registerPluginRecordIndex,\n type PluginIndexedRecord,\n} from '@aglyn/aglyn/plugin-manager/plugin-record-index'\nimport { unregisterPluginServices } from '@aglyn/aglyn/plugin-manager/plugin-services'\n\n/**\n * The product and category indexes a plugin that keeps products publishes\n * (AGL-3080), stood in over a spec's own Firestore double — this plugin may\n * not load the commerce plugin that registers the real ones. It answers in\n * the shape the commerce plugin's `product-record-index.ts` documents: live,\n * named records only, with the product facts its docblock lists. What the\n * commerce plugin itself answers is held in its own spec.\n */\n\nconst OWNER = 'commerce'\n\nconst str = (value: unknown): string => (typeof value === 'string' ? value : '')\nconst strings = (value: unknown): string[] =>\n (Array.isArray(value) ? value : []).map(str).filter(Boolean)\n\nfunction product(id: string, data: Record<string, unknown> | undefined): PluginIndexedRecord | null {\n if (!data || data['deletedAt'] != null || !str(data['name'])) return null\n const seo = (data['seo'] ?? {}) as Record<string, unknown>\n const mediaUrls = Array.isArray(data['mediaUrls']) ? data['mediaUrls'] : []\n return {\n id,\n name: str(data['name']),\n facts: {\n type: data['type'],\n description: str(data['description']),\n tags: strings(data['tags']),\n categoryIds: strings(data['categoryIds']),\n options: (Array.isArray(data['options']) ? data['options'] : []).map((option) => {\n const record = (option ?? {}) as Record<string, unknown>\n return { name: str(record['name']), values: strings(record['values']) }\n }),\n imageUrl: str(mediaUrls[0]) || str(data['imageUrl']) || null,\n seoTitle: str(seo['title']),\n seoDescription: str(seo['description']),\n },\n }\n}\n\n/** Registers both indexes, reading through `firestore`. */\nexport function standInProductIndexes(firestore: FirebaseFirestore.Firestore): void {\n const site = (hostId: string, name: string) =>\n firestore.collection('hosts').doc(hostId).collection(name)\n registerPluginRecordIndex(\n 'product',\n {\n async list({ hostId, limit }) {\n if (!hostId) return { records: [], truncated: false }\n const snapshot = await site(hostId, 'products').limit(limit + 1).get()\n const records = snapshot.docs\n .map((doc) => product(doc.id, doc.data()))\n .filter((record): record is PluginIndexedRecord => record !== null)\n return { records: records.slice(0, limit), truncated: snapshot.docs.length > limit }\n },\n async get({ hostId, id }) {\n if (!hostId) return null\n const snapshot = await site(hostId, 'products').doc(id).get()\n return snapshot.exists ? product(snapshot.id, snapshot.data()) : null\n },\n },\n { pluginId: OWNER },\n )\n registerPluginRecordIndex(\n 'productCategory',\n {\n async list({ hostId, limit }) {\n if (!hostId) return { records: [], truncated: false }\n const snapshot = await site(hostId, 'productCategories').limit(limit + 1).get()\n const records = snapshot.docs\n .map((doc) => ({ id: doc.id, name: str(doc.get('name')), facts: {} }))\n .filter((record) => record.name)\n return { records: records.slice(0, limit), truncated: snapshot.docs.length > limit }\n },\n async get() {\n return null\n },\n },\n { pluginId: OWNER },\n )\n}\n\n/** Forgets both, so the next spec starts with no plugin keeping products. */\nexport function removeStandInProductIndexes(): void {\n unregisterPluginServices(OWNER)\n}\n"],"names":["registerPluginRecordIndex","unregisterPluginServices","OWNER","str","value","strings","Array","isArray","map","filter","Boolean","product","id","data","seo","mediaUrls","name","facts","type","description","tags","categoryIds","options","option","record","values","imageUrl","seoTitle","seoDescription","standInProductIndexes","firestore","site","hostId","collection","doc","list","limit","records","truncated","snapshot","get","docs","slice","length","exists","pluginId","removeStandInProductIndexes"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,yBAAyB,QAEpB,kDAAiD;AACxD,SAASC,wBAAwB,QAAQ,8CAA6C;AAEtF;;;;;;;CAOC,GAED,MAAMC,QAAQ;AAEd,MAAMC,MAAM,CAACC,QAA4B,OAAOA,UAAU,WAAWA,QAAQ;AAC7E,MAAMC,UAAU,CAACD,QACf,AAACE,CAAAA,MAAMC,OAAO,CAACH,SAASA,QAAQ,EAAE,AAAD,EAAGI,GAAG,CAACL,KAAKM,MAAM,CAACC;AAEtD,SAASC,QAAQC,EAAU,EAAEC,IAAyC;QAEvDA;IADb,IAAI,CAACA,QAAQA,IAAI,CAAC,YAAY,IAAI,QAAQ,CAACV,IAAIU,IAAI,CAAC,OAAO,GAAG,OAAO;IACrE,MAAMC,OAAOD,YAAAA,IAAI,CAAC,MAAM,YAAXA,YAAe,CAAC;IAC7B,MAAME,YAAYT,MAAMC,OAAO,CAACM,IAAI,CAAC,YAAY,IAAIA,IAAI,CAAC,YAAY,GAAG,EAAE;IAC3E,OAAO;QACLD;QACAI,MAAMb,IAAIU,IAAI,CAAC,OAAO;QACtBI,OAAO;YACLC,MAAML,IAAI,CAAC,OAAO;YAClBM,aAAahB,IAAIU,IAAI,CAAC,cAAc;YACpCO,MAAMf,QAAQQ,IAAI,CAAC,OAAO;YAC1BQ,aAAahB,QAAQQ,IAAI,CAAC,cAAc;YACxCS,SAAS,AAAChB,CAAAA,MAAMC,OAAO,CAACM,IAAI,CAAC,UAAU,IAAIA,IAAI,CAAC,UAAU,GAAG,EAAE,AAAD,EAAGL,GAAG,CAAC,CAACe;gBACpE,MAAMC,SAAUD,iBAAAA,SAAU,CAAC;gBAC3B,OAAO;oBAAEP,MAAMb,IAAIqB,MAAM,CAAC,OAAO;oBAAGC,QAAQpB,QAAQmB,MAAM,CAAC,SAAS;gBAAE;YACxE;YACAE,UAAUvB,IAAIY,SAAS,CAAC,EAAE,KAAKZ,IAAIU,IAAI,CAAC,WAAW,KAAK;YACxDc,UAAUxB,IAAIW,GAAG,CAAC,QAAQ;YAC1Bc,gBAAgBzB,IAAIW,GAAG,CAAC,cAAc;QACxC;IACF;AACF;AAEA,yDAAyD,GACzD,OAAO,SAASe,sBAAsBC,SAAsC;IAC1E,MAAMC,OAAO,CAACC,QAAgBhB,OAC5Bc,UAAUG,UAAU,CAAC,SAASC,GAAG,CAACF,QAAQC,UAAU,CAACjB;IACvDhB,0BACE,WACA;QACE,MAAMmC,MAAK,EAAEH,MAAM,EAAEI,KAAK,EAAE;YAC1B,IAAI,CAACJ,QAAQ,OAAO;gBAAEK,SAAS,EAAE;gBAAEC,WAAW;YAAM;YACpD,MAAMC,WAAW,MAAMR,KAAKC,QAAQ,YAAYI,KAAK,CAACA,QAAQ,GAAGI,GAAG;YACpE,MAAMH,UAAUE,SAASE,IAAI,CAC1BjC,GAAG,CAAC,CAAC0B,MAAQvB,QAAQuB,IAAItB,EAAE,EAAEsB,IAAIrB,IAAI,KACrCJ,MAAM,CAAC,CAACe,SAA0CA,WAAW;YAChE,OAAO;gBAAEa,SAASA,QAAQK,KAAK,CAAC,GAAGN;gBAAQE,WAAWC,SAASE,IAAI,CAACE,MAAM,GAAGP;YAAM;QACrF;QACA,MAAMI,KAAI,EAAER,MAAM,EAAEpB,EAAE,EAAE;YACtB,IAAI,CAACoB,QAAQ,OAAO;YACpB,MAAMO,WAAW,MAAMR,KAAKC,QAAQ,YAAYE,GAAG,CAACtB,IAAI4B,GAAG;YAC3D,OAAOD,SAASK,MAAM,GAAGjC,QAAQ4B,SAAS3B,EAAE,EAAE2B,SAAS1B,IAAI,MAAM;QACnE;IACF,GACA;QAAEgC,UAAU3C;IAAM;IAEpBF,0BACE,mBACA;QACE,MAAMmC,MAAK,EAAEH,MAAM,EAAEI,KAAK,EAAE;YAC1B,IAAI,CAACJ,QAAQ,OAAO;gBAAEK,SAAS,EAAE;gBAAEC,WAAW;YAAM;YACpD,MAAMC,WAAW,MAAMR,KAAKC,QAAQ,qBAAqBI,KAAK,CAACA,QAAQ,GAAGI,GAAG;YAC7E,MAAMH,UAAUE,SAASE,IAAI,CAC1BjC,GAAG,CAAC,CAAC0B,MAAS,CAAA;oBAAEtB,IAAIsB,IAAItB,EAAE;oBAAEI,MAAMb,IAAI+B,IAAIM,GAAG,CAAC;oBAAUvB,OAAO,CAAC;gBAAE,CAAA,GAClER,MAAM,CAAC,CAACe,SAAWA,OAAOR,IAAI;YACjC,OAAO;gBAAEqB,SAASA,QAAQK,KAAK,CAAC,GAAGN;gBAAQE,WAAWC,SAASE,IAAI,CAACE,MAAM,GAAGP;YAAM;QACrF;QACA,MAAMI;YACJ,OAAO;QACT;IACF,GACA;QAAEK,UAAU3C;IAAM;AAEtB;AAEA,2EAA2E,GAC3E,OAAO,SAAS4C;IACd7C,yBAAyBC;AAC3B"}
@@ -132,7 +132,7 @@ const ANSWER_MAX = 100;
132
132
  },
133
133
  defaultValue: {
134
134
  type: 'string',
135
- description: 'What the component shows until a page sets it: copy for text and richText, a screen id from the site inventory or "" for href, "" for image and icon, a number for number, "true" or "false" for boolean, one answer’s value for choice.'
135
+ description: 'What the component shows until a page sets it: copy for text and richText, a page id from the site inventory or "" for href, "" for image and icon, a number for number, "true" or "false" for boolean, one answer’s value for choice.'
136
136
  },
137
137
  options: {
138
138
  type: 'array',
@@ -210,7 +210,7 @@ const isRecord = (value)=>typeof value === 'object' && value !== null && !Array.
210
210
  return scope.screenIds.has(value) || isRootRelativePath(value) || isHttpsUrl(value) ? {
211
211
  value
212
212
  } : {
213
- error: 'is neither a screen id from the site inventory, a path on this site nor an https: address',
213
+ error: 'is neither a page id from the site inventory, a path on this site nor an https: address',
214
214
  rule: 1
215
215
  };
216
216
  case 'number':
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/tools/ai-component-tool.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { parseMediaRef } from '@aglyn/aglyn/app-utils/media-ref'\nimport { COMPONENT_PROP_NAME_PATTERN } from '@aglyn/aglyn/app-utils/reusable-component-keys'\nimport { FieldComponentType } from '@aglyn/aglyn/foundation/definitions/components.types'\nimport type {\n ReusableComponentProp,\n ReusableComponentPropOption,\n ReusableComponentPropType,\n ReusableComponentPropValue,\n} from '@aglyn/aglyn/foundation/definitions/platform.types'\nimport { REUSABLE_PROP_KINDS } from '@aglyn/aglyn/foundation/definitions/property-kinds'\nimport type { AiTool } from '../providers/contract'\nimport { aiDoctrineTreeTool } from '../runtime/ai-doctrine'\nimport type {\n AiDoctrineRuleNumber,\n AiDoctrineViolation,\n} from '../runtime/ai-doctrine-validators'\nimport { HOSTILE_TEXT, isHttpsUrl, isRootRelativePath } from '../runtime/ai-node-tree'\nimport { AI_TEXT_LIMITS } from '../runtime/ai-palette'\n\n/**\n * The tool a generated reusable component arrives through (AGL-2908), and\n * the reading of the properties it declares.\n *\n * ── The kinds are the Properties dialog's ────────────────────────────────\n *\n * FILE ▸ Properties… declares a property as one of `REUSABLE_PROP_KINDS`,\n * and so does this tool: the kinds it offers are that table less the ones\n * `AI_COMPONENT_PROP_KINDS_NOT_OFFERED` names, each with the reason it is\n * left out, and a spec holds the two in both directions. A kind added to the\n * dialog does not compile here until it is offered or given a reason.\n *\n * What is offered is what a generated element can carry and a brief can\n * fill: copy (Text, Long text), a picture (Image), a link (Link), an icon\n * the site owner picks (Icon), a number (Number), a switch or a part's\n * visibility (Yes / no), and a setting with fixed answers (Choice).\n *\n * ── A declaration as the dialog saves one ────────────────────────────────\n *\n * `readAiComponentProps` returns each property in the shape the dialog's\n * cleaner stores: trimmed, with only the fields its kind uses, and a default\n * in the kind's own type — a Yes / no a real `true` or `false`, a Number a\n * number. Every refusal is a building-rule finding the doctrine's re-ask\n * quotes by its path.\n */\n\ntype OfferedKind = 'text' | 'richText' | 'image' | 'href' | 'icon' | 'number' | 'boolean' | 'choice'\n\nconst STYLE_REASON =\n 'Styling comes from the theme and the component’s own styles (rules 5 and 12); a page restyles one placement with an override, not a property.'\nconst RECORD_REASON =\n 'A pick from the site’s own records is made where a page places the component (rule 8), never invented as a generated default.'\nconst PLUGIN_REASON =\n 'Plugin settings belong to the plugin’s own element, which a generated component never places.'\n\n/**\n * Why each property kind the dialog declares is not offered to a model.\n * Keyed by every kind outside the offered ones, so the table is complete by\n * type.\n */\nexport const AI_COMPONENT_PROP_KINDS_NOT_OFFERED: Readonly<\n Record<Exclude<ReusableComponentPropType, OfferedKind>, string>\n> = {\n [FieldComponentType.MARKDOWN]:\n 'Long text binds to the same formatted-document fields, with a default a brief can write.',\n [FieldComponentType.DATA_TABLE]:\n 'A table is rows and columns a page fills in, not one value a brief describes.',\n [FieldComponentType.SLIDER]:\n 'Number binds to every slider, without the range settings a Slider property must declare.',\n [FieldComponentType.CHECKBOX]:\n 'Yes / no binds to every switch and single tick box, and no generated element offers a list of tick boxes.',\n [FieldComponentType.RADIO]:\n 'Choice binds to every field Radio buttons binds to, with its answers checked in one place.',\n [FieldComponentType.TOGGLE_BUTTON]:\n 'Choice binds to every field Toggle buttons binds to, with its answers checked in one place.',\n [FieldComponentType.DUAL_LIST_SELECT]:\n 'A pick list holds several answers, and no generated element offers a field that takes several.',\n [FieldComponentType.COLOR_PICKER]:\n 'Colors come from the theme (rule 5); a page recolors one placement with an override, not a property.',\n [FieldComponentType.CSS_DIMENSION]: STYLE_REASON,\n [FieldComponentType.CSS_BORDER]: STYLE_REASON,\n [FieldComponentType.CSS_GRADIENT]: STYLE_REASON,\n [FieldComponentType.BREAKPOINT_SPAN]: STYLE_REASON,\n [FieldComponentType.PRESET_CHOICE]: STYLE_REASON,\n [FieldComponentType.THEME_SCALE]: STYLE_REASON,\n [FieldComponentType.DATE_PICKER]:\n 'A date is a fact the brief would have to give (rule 14), not copy a component shows until a page sets it.',\n [FieldComponentType.TIME_PICKER]:\n 'A time is a fact the brief would have to give (rule 14), not copy a component shows until a page sets it.',\n [FieldComponentType.NODE_SELECT]: RECORD_REASON,\n [FieldComponentType.PRODUCT_SELECT]: RECORD_REASON,\n [FieldComponentType.COLLECTION_SELECT]: RECORD_REASON,\n [FieldComponentType.CATEGORY_SELECT]: RECORD_REASON,\n [FieldComponentType.DATASET_SELECT]: RECORD_REASON,\n [FieldComponentType.DATASET_FIELD_SELECT]: RECORD_REASON,\n [FieldComponentType.FORM_SELECT]:\n 'A form is placed by its id from the Forms page (rule 3), never chosen through a generated property.',\n [FieldComponentType.PLUGIN_SELECT]: PLUGIN_REASON,\n [FieldComponentType.PLUGIN_SETTINGS]: PLUGIN_REASON,\n}\n\n/**\n * The kinds a generated component may declare: the dialog's table less the\n * kinds above, in the dialog's own order.\n */\nexport const AI_COMPONENT_PROP_KINDS: readonly OfferedKind[] = (\n Object.keys(REUSABLE_PROP_KINDS) as ReusableComponentPropType[]\n).filter(\n (type): type is OfferedKind =>\n !Object.prototype.hasOwnProperty.call(AI_COMPONENT_PROP_KINDS_NOT_OFFERED, type),\n)\n\nexport type AiComponentPropKind = OfferedKind\n\n/**\n * The kinds a property proposed for a section already on a page may take\n * (AGL-3054): every offered kind but an Icon. That door reads each default off\n * the live field it binds, and an icon's default is drawn from a path the\n * picker stores beside its id, which the field it reads does not carry.\n */\nexport const AI_COMPONENT_SELECTION_PROP_KINDS: readonly AiComponentPropKind[] =\n AI_COMPONENT_PROP_KINDS.filter((type) => type !== 'icon')\n\n/** The kinds as a model reads them: the stored type and the name a page reads. */\nexport function aiComponentPropKindWords(\n kinds: readonly AiComponentPropKind[] = AI_COMPONENT_PROP_KINDS,\n): string {\n return kinds.map((type) => `${type} (${REUSABLE_PROP_KINDS[type].label})`).join(', ')\n}\n\nexport const AI_COMPONENT_TOOL_NAME = 'submit_component'\n\n/** The most properties one generated component declares. */\nexport const AI_COMPONENT_MAX_PROPS = 20\n\nconst NAME_MAX = 40\nconst LABEL_MAX = 80\nconst HELP_MAX = 200\nconst ANSWER_MAX = 100\n\n/**\n * The strict tool a component arrives through: the doctrine's tree, as the\n * doctrine's own component tool carries it, and the properties beside it.\n * Every field of a property is required and flat, because optional\n * properties are not portable across strict tool implementations; a kind\n * that has no answers sends `[]`.\n */\nexport function aiComponentTool(): AiTool {\n const doctrine = aiDoctrineTreeTool('component')\n const tree = (doctrine.inputSchema as { properties: Record<string, unknown> }).properties['tree']\n return {\n name: AI_COMPONENT_TOOL_NAME,\n description:\n 'Submit the reusable component: its tree as one flat node map, and every property the tree binds.',\n strict: true,\n inputSchema: {\n type: 'object',\n additionalProperties: false,\n required: ['tree', 'props'],\n properties: {\n tree,\n props: {\n type: 'array',\n description:\n 'Every property the tree binds with {{prop.<name>}}, in the order a page sets them.',\n items: {\n type: 'object',\n additionalProperties: false,\n required: ['name', 'type', 'label', 'description', 'defaultValue', 'options'],\n properties: {\n name: {\n type: 'string',\n description:\n 'Letters, digits and underscores, starting with a letter; bound as {{prop.<name>}}.',\n },\n type: {\n type: 'string',\n enum: [...AI_COMPONENT_PROP_KINDS],\n description: `The kind, as type (the name a page reads): ${aiComponentPropKindWords()}.`,\n },\n label: { type: 'string', description: 'The words a page reads beside the field.' },\n description: { type: 'string', description: 'Help beside the field, or \"\".' },\n defaultValue: {\n type: 'string',\n description:\n 'What the component shows until a page sets it: copy for text and richText, a screen id from the site inventory or \"\" for href, \"\" for image and icon, a number for number, \"true\" or \"false\" for boolean, one answer’s value for choice.',\n },\n options: {\n type: 'array',\n description: 'The answers of a choice; [] for every other kind.',\n items: {\n type: 'object',\n additionalProperties: false,\n required: ['value', 'label'],\n properties: {\n value: {\n type: 'string',\n description: 'What the bound field receives: one of the values that field lists.',\n },\n label: { type: 'string', description: 'What a page picks.' },\n },\n },\n },\n },\n },\n },\n },\n },\n }\n}\n\nexport interface AiComponentPropsScope {\n /** The screens a Link default may name: the site inventory's. */\n screenIds: ReadonlySet<string>\n}\n\nexport interface AiComponentPropsReading {\n /** Every property that could be read, as the Properties dialog stores one. */\n props: ReusableComponentProp[]\n /**\n * The names of properties that were declared under a usable name and\n * refused for something else — their kind, answers, default or label — so\n * a check of the tree does not report their tokens a second time.\n */\n refused: string[]\n violations: AiDoctrineViolation[]\n}\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value)\n\n/** A scalar the answer wrote, as trimmed text. */\nfunction scalar(value: unknown): string {\n return typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean'\n ? String(value).trim()\n : ''\n}\n\ntype DefaultReading =\n | { value: ReusableComponentPropValue | undefined }\n | { error: string; rule: AiDoctrineRuleNumber }\n\n/** A default as the kind stores it, or why it cannot be one. */\nfunction readDefault(\n type: OfferedKind,\n raw: unknown,\n options: readonly ReusableComponentPropOption[],\n scope: AiComponentPropsScope,\n): DefaultReading {\n const value = scalar(raw)\n if (HOSTILE_TEXT.test(value)) return { error: 'carries markup or script', rule: 1 }\n // Nothing set is a default of its own: the kind's \"not set\".\n if (!value) return { value: undefined }\n switch (type) {\n case 'text':\n case 'richText':\n return value.length > AI_TEXT_LIMITS.body\n ? { error: `runs past ${AI_TEXT_LIMITS.body} characters`, rule: 1 }\n : { value }\n case 'icon':\n // The picker stores an icon with the drawing a model cannot name, so\n // the site owner picks it where a page places the component.\n return { error: 'is an icon, which the site owner picks; leave it \"\"', rule: 1 }\n case 'image':\n if (/^https?:\\/\\//i.test(value)) {\n return { error: 'links a picture from another website; leave it empty for an upload', rule: 9 }\n }\n return parseMediaRef(value)\n ? { value }\n : { error: 'is not a picture from the media library; leave it empty for an upload', rule: 9 }\n case 'href':\n return scope.screenIds.has(value) || isRootRelativePath(value) || isHttpsUrl(value)\n ? { value }\n : {\n error: 'is neither a screen id from the site inventory, a path on this site nor an https: address',\n rule: 1,\n }\n case 'number': {\n const parsed = Number(value)\n return Number.isFinite(parsed) ? { value: parsed } : { error: 'is not a number', rule: 1 }\n }\n case 'boolean':\n if (value === 'true') return { value: true }\n if (value === 'false') return { value: false }\n return { error: 'is neither true nor false', rule: 1 }\n case 'choice':\n return options.some((option) => option.value === value)\n ? { value }\n : { error: 'is not the value of one of its answers', rule: 1 }\n }\n}\n\n/**\n * The properties a component answer declares, read into the shape the\n * Properties dialog stores, with a finding for everything that would not\n * save there or would not read as the brief's own copy.\n */\nexport function readAiComponentProps(\n answer: Record<string, unknown>,\n scope: AiComponentPropsScope,\n): AiComponentPropsReading {\n const raw = answer['props']\n if (!Array.isArray(raw)) {\n return {\n props: [],\n refused: [],\n violations: [\n {\n rule: 1,\n code: 'props-missing',\n message:\n 'The component came without its list of properties. Declare every property the tree binds, or an empty list.',\n paths: ['props'],\n },\n ],\n }\n }\n const violations: AiDoctrineViolation[] = []\n const finding = (\n code: string,\n message: string,\n path: string,\n rule: AiDoctrineRuleNumber = 1,\n ): void => {\n violations.push({ rule, code, message, paths: [path] })\n }\n if (raw.length > AI_COMPONENT_MAX_PROPS) {\n finding(\n 'props-over-limit',\n `The component declares ${raw.length} properties, and one component declares at most ${AI_COMPONENT_MAX_PROPS}. Keep a property for each value that differs between placements.`,\n 'props',\n )\n }\n const kindNames = AI_COMPONENT_PROP_KINDS.map((type) => REUSABLE_PROP_KINDS[type].label)\n const props: ReusableComponentProp[] = []\n const seen = new Set<string>()\n raw.slice(0, AI_COMPONENT_MAX_PROPS).forEach((entry, index) => {\n const at = `props[${index}]`\n if (!isRecord(entry)) {\n finding('prop-shape', 'A property could not be read. Give each one a name, a kind and a default.', at)\n return\n }\n const name = scalar(entry['name'])\n const called = name ? `\"${name}\"` : `number ${index + 1}`\n if (!COMPONENT_PROP_NAME_PATTERN.test(name) || name.length > NAME_MAX) {\n finding(\n 'prop-name',\n `The property ${called} is not a name a property can have. Use letters, digits and underscores, starting with a letter, in ${NAME_MAX} characters or fewer.`,\n `${at}.name`,\n )\n return\n }\n if (seen.has(name)) {\n finding('prop-duplicate', `Two properties are named \"${name}\". Give each property its own name.`, `${at}.name`)\n return\n }\n seen.add(name)\n const type = scalar(entry['type']) as OfferedKind\n if (!AI_COMPONENT_PROP_KINDS.includes(type)) {\n finding(\n 'prop-kind',\n `The property \"${name}\" is a kind a generated component does not declare. Use one of: ${kindNames.join(', ')}.`,\n `${at}.type`,\n )\n return\n }\n const options: ReusableComponentPropOption[] = []\n if (type === 'choice') {\n const answers = Array.isArray(entry['options']) ? entry['options'] : []\n const values = new Set<string>()\n for (const option of answers) {\n const value = isRecord(option) ? scalar(option['value']).slice(0, ANSWER_MAX) : ''\n const label = isRecord(option) ? scalar(option['label']).slice(0, LABEL_MAX) : ''\n if (!value || HOSTILE_TEXT.test(value) || HOSTILE_TEXT.test(label)) {\n finding('prop-answers', `Every answer of \"${name}\" needs a plain value.`, `${at}.options`)\n return\n }\n if (values.has(value)) {\n finding('prop-answers', `Two answers of \"${name}\" share the value \"${value}\".`, `${at}.options`)\n return\n }\n values.add(value)\n options.push({ value, ...(label && label !== value ? { label } : {}) })\n }\n if (!options.length) {\n finding(\n 'prop-answers',\n `The Choice \"${name}\" lists no answers. Give it the answers a page picks from.`,\n `${at}.options`,\n )\n return\n }\n }\n const read = readDefault(type, entry['defaultValue'], options, scope)\n if ('error' in read) {\n finding('prop-default', `The default of \"${name}\" ${read.error}.`, `${at}.defaultValue`, read.rule)\n return\n }\n const label = scalar(entry['label']).slice(0, LABEL_MAX)\n const description = scalar(entry['description']).slice(0, HELP_MAX)\n if (HOSTILE_TEXT.test(label) || HOSTILE_TEXT.test(description)) {\n finding('prop-shape', `The label or help of \"${name}\" carries markup or script.`, at)\n return\n }\n props.push({\n name,\n type,\n ...(label ? { label } : {}),\n ...(description ? { description } : {}),\n ...(read.value !== undefined ? { defaultValue: read.value } : {}),\n ...(type === 'choice' ? { options } : {}),\n })\n })\n // A name that read, and was then refused for its kind, answers, default or label.\n const refused = [...seen].filter((name) => !props.some((prop) => prop.name === name))\n return { props, refused, violations }\n}\n"],"names":["parseMediaRef","COMPONENT_PROP_NAME_PATTERN","FieldComponentType","REUSABLE_PROP_KINDS","aiDoctrineTreeTool","HOSTILE_TEXT","isHttpsUrl","isRootRelativePath","AI_TEXT_LIMITS","STYLE_REASON","RECORD_REASON","PLUGIN_REASON","AI_COMPONENT_PROP_KINDS_NOT_OFFERED","MARKDOWN","DATA_TABLE","SLIDER","CHECKBOX","RADIO","TOGGLE_BUTTON","DUAL_LIST_SELECT","COLOR_PICKER","CSS_DIMENSION","CSS_BORDER","CSS_GRADIENT","BREAKPOINT_SPAN","PRESET_CHOICE","THEME_SCALE","DATE_PICKER","TIME_PICKER","NODE_SELECT","PRODUCT_SELECT","COLLECTION_SELECT","CATEGORY_SELECT","DATASET_SELECT","DATASET_FIELD_SELECT","FORM_SELECT","PLUGIN_SELECT","PLUGIN_SETTINGS","AI_COMPONENT_PROP_KINDS","Object","keys","filter","type","prototype","hasOwnProperty","call","AI_COMPONENT_SELECTION_PROP_KINDS","aiComponentPropKindWords","kinds","map","label","join","AI_COMPONENT_TOOL_NAME","AI_COMPONENT_MAX_PROPS","NAME_MAX","LABEL_MAX","HELP_MAX","ANSWER_MAX","aiComponentTool","doctrine","tree","inputSchema","properties","name","description","strict","additionalProperties","required","props","items","enum","defaultValue","options","value","isRecord","Array","isArray","scalar","String","trim","readDefault","raw","scope","test","error","rule","undefined","length","body","screenIds","has","parsed","Number","isFinite","some","option","readAiComponentProps","answer","refused","violations","code","message","paths","finding","path","push","kindNames","seen","Set","slice","forEach","entry","index","at","called","add","includes","answers","values","read","prop"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/tools/ai-component-tool.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { parseMediaRef } from '@aglyn/aglyn/app-utils/media-ref'\nimport { COMPONENT_PROP_NAME_PATTERN } from '@aglyn/aglyn/app-utils/reusable-component-keys'\nimport { FieldComponentType } from '@aglyn/aglyn/foundation/definitions/components.types'\nimport type {\n ReusableComponentProp,\n ReusableComponentPropOption,\n ReusableComponentPropType,\n ReusableComponentPropValue,\n} from '@aglyn/aglyn/foundation/definitions/platform.types'\nimport { REUSABLE_PROP_KINDS } from '@aglyn/aglyn/foundation/definitions/property-kinds'\nimport type { AiTool } from '../providers/contract'\nimport { aiDoctrineTreeTool } from '../runtime/ai-doctrine'\nimport type {\n AiDoctrineRuleNumber,\n AiDoctrineViolation,\n} from '../runtime/ai-doctrine-validators'\nimport { HOSTILE_TEXT, isHttpsUrl, isRootRelativePath } from '../runtime/ai-node-tree'\nimport { AI_TEXT_LIMITS } from '../runtime/ai-palette'\n\n/**\n * The tool a generated reusable component arrives through (AGL-2908), and\n * the reading of the properties it declares.\n *\n * ── The kinds are the Properties dialog's ────────────────────────────────\n *\n * FILE ▸ Properties… declares a property as one of `REUSABLE_PROP_KINDS`,\n * and so does this tool: the kinds it offers are that table less the ones\n * `AI_COMPONENT_PROP_KINDS_NOT_OFFERED` names, each with the reason it is\n * left out, and a spec holds the two in both directions. A kind added to the\n * dialog does not compile here until it is offered or given a reason.\n *\n * What is offered is what a generated element can carry and a brief can\n * fill: copy (Text, Long text), a picture (Image), a link (Link), an icon\n * the site owner picks (Icon), a number (Number), a switch or a part's\n * visibility (Yes / no), and a setting with fixed answers (Choice).\n *\n * ── A declaration as the dialog saves one ────────────────────────────────\n *\n * `readAiComponentProps` returns each property in the shape the dialog's\n * cleaner stores: trimmed, with only the fields its kind uses, and a default\n * in the kind's own type — a Yes / no a real `true` or `false`, a Number a\n * number. Every refusal is a building-rule finding the doctrine's re-ask\n * quotes by its path.\n */\n\ntype OfferedKind = 'text' | 'richText' | 'image' | 'href' | 'icon' | 'number' | 'boolean' | 'choice'\n\nconst STYLE_REASON =\n 'Styling comes from the theme and the component’s own styles (rules 5 and 12); a page restyles one placement with an override, not a property.'\nconst RECORD_REASON =\n 'A pick from the site’s own records is made where a page places the component (rule 8), never invented as a generated default.'\nconst PLUGIN_REASON =\n 'Plugin settings belong to the plugin’s own element, which a generated component never places.'\n\n/**\n * Why each property kind the dialog declares is not offered to a model.\n * Keyed by every kind outside the offered ones, so the table is complete by\n * type.\n */\nexport const AI_COMPONENT_PROP_KINDS_NOT_OFFERED: Readonly<\n Record<Exclude<ReusableComponentPropType, OfferedKind>, string>\n> = {\n [FieldComponentType.MARKDOWN]:\n 'Long text binds to the same formatted-document fields, with a default a brief can write.',\n [FieldComponentType.DATA_TABLE]:\n 'A table is rows and columns a page fills in, not one value a brief describes.',\n [FieldComponentType.SLIDER]:\n 'Number binds to every slider, without the range settings a Slider property must declare.',\n [FieldComponentType.CHECKBOX]:\n 'Yes / no binds to every switch and single tick box, and no generated element offers a list of tick boxes.',\n [FieldComponentType.RADIO]:\n 'Choice binds to every field Radio buttons binds to, with its answers checked in one place.',\n [FieldComponentType.TOGGLE_BUTTON]:\n 'Choice binds to every field Toggle buttons binds to, with its answers checked in one place.',\n [FieldComponentType.DUAL_LIST_SELECT]:\n 'A pick list holds several answers, and no generated element offers a field that takes several.',\n [FieldComponentType.COLOR_PICKER]:\n 'Colors come from the theme (rule 5); a page recolors one placement with an override, not a property.',\n [FieldComponentType.CSS_DIMENSION]: STYLE_REASON,\n [FieldComponentType.CSS_BORDER]: STYLE_REASON,\n [FieldComponentType.CSS_GRADIENT]: STYLE_REASON,\n [FieldComponentType.BREAKPOINT_SPAN]: STYLE_REASON,\n [FieldComponentType.PRESET_CHOICE]: STYLE_REASON,\n [FieldComponentType.THEME_SCALE]: STYLE_REASON,\n [FieldComponentType.DATE_PICKER]:\n 'A date is a fact the brief would have to give (rule 14), not copy a component shows until a page sets it.',\n [FieldComponentType.TIME_PICKER]:\n 'A time is a fact the brief would have to give (rule 14), not copy a component shows until a page sets it.',\n [FieldComponentType.NODE_SELECT]: RECORD_REASON,\n [FieldComponentType.PRODUCT_SELECT]: RECORD_REASON,\n [FieldComponentType.COLLECTION_SELECT]: RECORD_REASON,\n [FieldComponentType.CATEGORY_SELECT]: RECORD_REASON,\n [FieldComponentType.DATASET_SELECT]: RECORD_REASON,\n [FieldComponentType.DATASET_FIELD_SELECT]: RECORD_REASON,\n [FieldComponentType.FORM_SELECT]:\n 'A form is placed by its id from the Forms page (rule 3), never chosen through a generated property.',\n [FieldComponentType.PLUGIN_SELECT]: PLUGIN_REASON,\n [FieldComponentType.PLUGIN_SETTINGS]: PLUGIN_REASON,\n}\n\n/**\n * The kinds a generated component may declare: the dialog's table less the\n * kinds above, in the dialog's own order.\n */\nexport const AI_COMPONENT_PROP_KINDS: readonly OfferedKind[] = (\n Object.keys(REUSABLE_PROP_KINDS) as ReusableComponentPropType[]\n).filter(\n (type): type is OfferedKind =>\n !Object.prototype.hasOwnProperty.call(AI_COMPONENT_PROP_KINDS_NOT_OFFERED, type),\n)\n\nexport type AiComponentPropKind = OfferedKind\n\n/**\n * The kinds a property proposed for a section already on a page may take\n * (AGL-3054): every offered kind but an Icon. That door reads each default off\n * the live field it binds, and an icon's default is drawn from a path the\n * picker stores beside its id, which the field it reads does not carry.\n */\nexport const AI_COMPONENT_SELECTION_PROP_KINDS: readonly AiComponentPropKind[] =\n AI_COMPONENT_PROP_KINDS.filter((type) => type !== 'icon')\n\n/** The kinds as a model reads them: the stored type and the name a page reads. */\nexport function aiComponentPropKindWords(\n kinds: readonly AiComponentPropKind[] = AI_COMPONENT_PROP_KINDS,\n): string {\n return kinds.map((type) => `${type} (${REUSABLE_PROP_KINDS[type].label})`).join(', ')\n}\n\nexport const AI_COMPONENT_TOOL_NAME = 'submit_component'\n\n/** The most properties one generated component declares. */\nexport const AI_COMPONENT_MAX_PROPS = 20\n\nconst NAME_MAX = 40\nconst LABEL_MAX = 80\nconst HELP_MAX = 200\nconst ANSWER_MAX = 100\n\n/**\n * The strict tool a component arrives through: the doctrine's tree, as the\n * doctrine's own component tool carries it, and the properties beside it.\n * Every field of a property is required and flat, because optional\n * properties are not portable across strict tool implementations; a kind\n * that has no answers sends `[]`.\n */\nexport function aiComponentTool(): AiTool {\n const doctrine = aiDoctrineTreeTool('component')\n const tree = (doctrine.inputSchema as { properties: Record<string, unknown> }).properties['tree']\n return {\n name: AI_COMPONENT_TOOL_NAME,\n description:\n 'Submit the reusable component: its tree as one flat node map, and every property the tree binds.',\n strict: true,\n inputSchema: {\n type: 'object',\n additionalProperties: false,\n required: ['tree', 'props'],\n properties: {\n tree,\n props: {\n type: 'array',\n description:\n 'Every property the tree binds with {{prop.<name>}}, in the order a page sets them.',\n items: {\n type: 'object',\n additionalProperties: false,\n required: ['name', 'type', 'label', 'description', 'defaultValue', 'options'],\n properties: {\n name: {\n type: 'string',\n description:\n 'Letters, digits and underscores, starting with a letter; bound as {{prop.<name>}}.',\n },\n type: {\n type: 'string',\n enum: [...AI_COMPONENT_PROP_KINDS],\n description: `The kind, as type (the name a page reads): ${aiComponentPropKindWords()}.`,\n },\n label: { type: 'string', description: 'The words a page reads beside the field.' },\n description: { type: 'string', description: 'Help beside the field, or \"\".' },\n defaultValue: {\n type: 'string',\n description:\n 'What the component shows until a page sets it: copy for text and richText, a page id from the site inventory or \"\" for href, \"\" for image and icon, a number for number, \"true\" or \"false\" for boolean, one answer’s value for choice.',\n },\n options: {\n type: 'array',\n description: 'The answers of a choice; [] for every other kind.',\n items: {\n type: 'object',\n additionalProperties: false,\n required: ['value', 'label'],\n properties: {\n value: {\n type: 'string',\n description: 'What the bound field receives: one of the values that field lists.',\n },\n label: { type: 'string', description: 'What a page picks.' },\n },\n },\n },\n },\n },\n },\n },\n },\n }\n}\n\nexport interface AiComponentPropsScope {\n /** The screens a Link default may name: the site inventory's. */\n screenIds: ReadonlySet<string>\n}\n\nexport interface AiComponentPropsReading {\n /** Every property that could be read, as the Properties dialog stores one. */\n props: ReusableComponentProp[]\n /**\n * The names of properties that were declared under a usable name and\n * refused for something else — their kind, answers, default or label — so\n * a check of the tree does not report their tokens a second time.\n */\n refused: string[]\n violations: AiDoctrineViolation[]\n}\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value)\n\n/** A scalar the answer wrote, as trimmed text. */\nfunction scalar(value: unknown): string {\n return typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean'\n ? String(value).trim()\n : ''\n}\n\ntype DefaultReading =\n | { value: ReusableComponentPropValue | undefined }\n | { error: string; rule: AiDoctrineRuleNumber }\n\n/** A default as the kind stores it, or why it cannot be one. */\nfunction readDefault(\n type: OfferedKind,\n raw: unknown,\n options: readonly ReusableComponentPropOption[],\n scope: AiComponentPropsScope,\n): DefaultReading {\n const value = scalar(raw)\n if (HOSTILE_TEXT.test(value)) return { error: 'carries markup or script', rule: 1 }\n // Nothing set is a default of its own: the kind's \"not set\".\n if (!value) return { value: undefined }\n switch (type) {\n case 'text':\n case 'richText':\n return value.length > AI_TEXT_LIMITS.body\n ? { error: `runs past ${AI_TEXT_LIMITS.body} characters`, rule: 1 }\n : { value }\n case 'icon':\n // The picker stores an icon with the drawing a model cannot name, so\n // the site owner picks it where a page places the component.\n return { error: 'is an icon, which the site owner picks; leave it \"\"', rule: 1 }\n case 'image':\n if (/^https?:\\/\\//i.test(value)) {\n return { error: 'links a picture from another website; leave it empty for an upload', rule: 9 }\n }\n return parseMediaRef(value)\n ? { value }\n : { error: 'is not a picture from the media library; leave it empty for an upload', rule: 9 }\n case 'href':\n return scope.screenIds.has(value) || isRootRelativePath(value) || isHttpsUrl(value)\n ? { value }\n : {\n error: 'is neither a page id from the site inventory, a path on this site nor an https: address',\n rule: 1,\n }\n case 'number': {\n const parsed = Number(value)\n return Number.isFinite(parsed) ? { value: parsed } : { error: 'is not a number', rule: 1 }\n }\n case 'boolean':\n if (value === 'true') return { value: true }\n if (value === 'false') return { value: false }\n return { error: 'is neither true nor false', rule: 1 }\n case 'choice':\n return options.some((option) => option.value === value)\n ? { value }\n : { error: 'is not the value of one of its answers', rule: 1 }\n }\n}\n\n/**\n * The properties a component answer declares, read into the shape the\n * Properties dialog stores, with a finding for everything that would not\n * save there or would not read as the brief's own copy.\n */\nexport function readAiComponentProps(\n answer: Record<string, unknown>,\n scope: AiComponentPropsScope,\n): AiComponentPropsReading {\n const raw = answer['props']\n if (!Array.isArray(raw)) {\n return {\n props: [],\n refused: [],\n violations: [\n {\n rule: 1,\n code: 'props-missing',\n message:\n 'The component came without its list of properties. Declare every property the tree binds, or an empty list.',\n paths: ['props'],\n },\n ],\n }\n }\n const violations: AiDoctrineViolation[] = []\n const finding = (\n code: string,\n message: string,\n path: string,\n rule: AiDoctrineRuleNumber = 1,\n ): void => {\n violations.push({ rule, code, message, paths: [path] })\n }\n if (raw.length > AI_COMPONENT_MAX_PROPS) {\n finding(\n 'props-over-limit',\n `The component declares ${raw.length} properties, and one component declares at most ${AI_COMPONENT_MAX_PROPS}. Keep a property for each value that differs between placements.`,\n 'props',\n )\n }\n const kindNames = AI_COMPONENT_PROP_KINDS.map((type) => REUSABLE_PROP_KINDS[type].label)\n const props: ReusableComponentProp[] = []\n const seen = new Set<string>()\n raw.slice(0, AI_COMPONENT_MAX_PROPS).forEach((entry, index) => {\n const at = `props[${index}]`\n if (!isRecord(entry)) {\n finding('prop-shape', 'A property could not be read. Give each one a name, a kind and a default.', at)\n return\n }\n const name = scalar(entry['name'])\n const called = name ? `\"${name}\"` : `number ${index + 1}`\n if (!COMPONENT_PROP_NAME_PATTERN.test(name) || name.length > NAME_MAX) {\n finding(\n 'prop-name',\n `The property ${called} is not a name a property can have. Use letters, digits and underscores, starting with a letter, in ${NAME_MAX} characters or fewer.`,\n `${at}.name`,\n )\n return\n }\n if (seen.has(name)) {\n finding('prop-duplicate', `Two properties are named \"${name}\". Give each property its own name.`, `${at}.name`)\n return\n }\n seen.add(name)\n const type = scalar(entry['type']) as OfferedKind\n if (!AI_COMPONENT_PROP_KINDS.includes(type)) {\n finding(\n 'prop-kind',\n `The property \"${name}\" is a kind a generated component does not declare. Use one of: ${kindNames.join(', ')}.`,\n `${at}.type`,\n )\n return\n }\n const options: ReusableComponentPropOption[] = []\n if (type === 'choice') {\n const answers = Array.isArray(entry['options']) ? entry['options'] : []\n const values = new Set<string>()\n for (const option of answers) {\n const value = isRecord(option) ? scalar(option['value']).slice(0, ANSWER_MAX) : ''\n const label = isRecord(option) ? scalar(option['label']).slice(0, LABEL_MAX) : ''\n if (!value || HOSTILE_TEXT.test(value) || HOSTILE_TEXT.test(label)) {\n finding('prop-answers', `Every answer of \"${name}\" needs a plain value.`, `${at}.options`)\n return\n }\n if (values.has(value)) {\n finding('prop-answers', `Two answers of \"${name}\" share the value \"${value}\".`, `${at}.options`)\n return\n }\n values.add(value)\n options.push({ value, ...(label && label !== value ? { label } : {}) })\n }\n if (!options.length) {\n finding(\n 'prop-answers',\n `The Choice \"${name}\" lists no answers. Give it the answers a page picks from.`,\n `${at}.options`,\n )\n return\n }\n }\n const read = readDefault(type, entry['defaultValue'], options, scope)\n if ('error' in read) {\n finding('prop-default', `The default of \"${name}\" ${read.error}.`, `${at}.defaultValue`, read.rule)\n return\n }\n const label = scalar(entry['label']).slice(0, LABEL_MAX)\n const description = scalar(entry['description']).slice(0, HELP_MAX)\n if (HOSTILE_TEXT.test(label) || HOSTILE_TEXT.test(description)) {\n finding('prop-shape', `The label or help of \"${name}\" carries markup or script.`, at)\n return\n }\n props.push({\n name,\n type,\n ...(label ? { label } : {}),\n ...(description ? { description } : {}),\n ...(read.value !== undefined ? { defaultValue: read.value } : {}),\n ...(type === 'choice' ? { options } : {}),\n })\n })\n // A name that read, and was then refused for its kind, answers, default or label.\n const refused = [...seen].filter((name) => !props.some((prop) => prop.name === name))\n return { props, refused, violations }\n}\n"],"names":["parseMediaRef","COMPONENT_PROP_NAME_PATTERN","FieldComponentType","REUSABLE_PROP_KINDS","aiDoctrineTreeTool","HOSTILE_TEXT","isHttpsUrl","isRootRelativePath","AI_TEXT_LIMITS","STYLE_REASON","RECORD_REASON","PLUGIN_REASON","AI_COMPONENT_PROP_KINDS_NOT_OFFERED","MARKDOWN","DATA_TABLE","SLIDER","CHECKBOX","RADIO","TOGGLE_BUTTON","DUAL_LIST_SELECT","COLOR_PICKER","CSS_DIMENSION","CSS_BORDER","CSS_GRADIENT","BREAKPOINT_SPAN","PRESET_CHOICE","THEME_SCALE","DATE_PICKER","TIME_PICKER","NODE_SELECT","PRODUCT_SELECT","COLLECTION_SELECT","CATEGORY_SELECT","DATASET_SELECT","DATASET_FIELD_SELECT","FORM_SELECT","PLUGIN_SELECT","PLUGIN_SETTINGS","AI_COMPONENT_PROP_KINDS","Object","keys","filter","type","prototype","hasOwnProperty","call","AI_COMPONENT_SELECTION_PROP_KINDS","aiComponentPropKindWords","kinds","map","label","join","AI_COMPONENT_TOOL_NAME","AI_COMPONENT_MAX_PROPS","NAME_MAX","LABEL_MAX","HELP_MAX","ANSWER_MAX","aiComponentTool","doctrine","tree","inputSchema","properties","name","description","strict","additionalProperties","required","props","items","enum","defaultValue","options","value","isRecord","Array","isArray","scalar","String","trim","readDefault","raw","scope","test","error","rule","undefined","length","body","screenIds","has","parsed","Number","isFinite","some","option","readAiComponentProps","answer","refused","violations","code","message","paths","finding","path","push","kindNames","seen","Set","slice","forEach","entry","index","at","called","add","includes","answers","values","read","prop"],"mappings":"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"}
@@ -40,7 +40,7 @@ import { getOrgForUser, memberHasPermissionOnHost } from "@aglyn/tenant-data-adm
40
40
  */ export const AI_DUPLICATE_RESOURCE_TOOL_NAME = 'duplicate_resource';
41
41
  /** The structured-output tool a generation offers the model. */ export const AI_DUPLICATE_RESOURCE_TOOL = {
42
42
  name: AI_DUPLICATE_RESOURCE_TOOL_NAME,
43
- description: 'Copy a resource this site already has — a screen, an email design, a component, a layout, ' + 'a template, a form or a workflow — as a new draft to build from, instead of creating it again. ' + 'The copy is unpublished: it has no address, runs nothing and is placed nowhere until a person does.',
43
+ description: 'Copy a resource this site already has — a page (kind "screen"), an email design, a component, a layout, ' + 'a template, a form or a workflow — as a new draft to build from, instead of creating it again. ' + 'The copy is unpublished: it has no address, runs nothing and is placed nowhere until a person does.',
44
44
  inputSchema: {
45
45
  type: 'object',
46
46
  properties: {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/tools/ai-duplicate-resource-tool.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n DUPLICABLE_HOST_RESOURCE_KINDS,\n DUPLICATE_NAME_MAX,\n isDuplicableHostResourceKind,\n type DuplicableHostResourceKind,\n} from '@aglyn/aglyn/app-utils/duplicate-resource'\nimport { hostRoleCanWrite } from '@aglyn/aglyn/app-utils/organizations'\nimport { orgPermissionLabel } from '@aglyn/aglyn/app-utils/org-permissions'\nimport { duplicateResource } from '@aglyn/tenant-data-admin/server/duplicate-resource'\nimport {\n getOrgForUser,\n memberHasPermissionOnHost,\n} from '@aglyn/tenant-data-admin/server/organizations'\nimport type { AiTool } from '../providers/contract'\nimport type { AiGateContext } from '../runtime/ai-gate'\n\n/**\n * The `duplicate_resource` tool (AGL-2936, AGL-2984): a model that would\n * rebuild something the site already has copies it instead, through core's\n * `duplicateResource` — the same copy the row menus and the console door\n * make, with the same uniqueness rules, the same band arithmetic and the same\n * activity rows.\n *\n * It spends no tokens of its own, so it calls no provider: it runs inside a\n * door or a job step that has already climbed `aiGateLadder`, and takes that\n * ladder's context as its proof — the caller is signed in and verified, the\n * release flag, entitlement and lockdown rungs admitted the request, and the\n * workspace's band and caps admitted a reservation. On top of that it asks\n * what a copy needs whichever door called it: `ai.generate`, because a copy\n * is a generation's output, and a host role that may write the site, because\n * the copy lands there.\n *\n * DRAFTS ONLY, by construction of the copy: a screen copy has no route, a\n * workflow copy no trigger, and nothing places a component, layout or form\n * copy until a person does.\n */\n\nexport const AI_DUPLICATE_RESOURCE_TOOL_NAME = 'duplicate_resource'\n\n/** The structured-output tool a generation offers the model. */\nexport const AI_DUPLICATE_RESOURCE_TOOL: AiTool = {\n name: AI_DUPLICATE_RESOURCE_TOOL_NAME,\n description:\n 'Copy a resource this site already has — a screen, an email design, a component, a layout, ' +\n 'a template, a form or a workflow — as a new draft to build from, instead of creating it again. ' +\n 'The copy is unpublished: it has no address, runs nothing and is placed nowhere until a person does.',\n inputSchema: {\n type: 'object',\n properties: {\n kind: {\n type: 'string',\n enum: [...DUPLICABLE_HOST_RESOURCE_KINDS],\n description: 'The kind of resource to copy.',\n },\n sourceId: {\n type: 'string',\n description: 'The id of the resource to copy, as the site inventory lists it.',\n },\n name: {\n type: ['string', 'null'],\n description: `The copy's name, at most ${DUPLICATE_NAME_MAX} characters, or null for \"Copy of\" the source's name.`,\n },\n },\n required: ['kind', 'sourceId', 'name'],\n additionalProperties: false,\n },\n strict: true,\n}\n\n/** A tool call's input, read. */\nexport type AiDuplicateResourceInput = {\n kind: DuplicableHostResourceKind\n sourceId: string\n name: string | null\n}\n\n/** What the tool answers: the copy, or a refusal the caller can relay. */\nexport type AiDuplicateResourceOutcome =\n | { ok: true; kind: DuplicableHostResourceKind; id: string; versionId: string | null; name: string }\n | { ok: false; status: number; error: string }\n\n/** Reads a tool call's input; `null` when it is not one this tool accepts. */\nexport function parseAiDuplicateResourceInput(\n input: Record<string, unknown>,\n): AiDuplicateResourceInput | null {\n const kind = input['kind']\n const sourceId = typeof input['sourceId'] === 'string' ? input['sourceId'].trim() : ''\n const name = input['name']\n if (!isDuplicableHostResourceKind(kind)) return null\n if (!sourceId || sourceId.length > 64 || sourceId.includes('/')) return null\n if (name !== null && typeof name !== 'string') return null\n const trimmed = typeof name === 'string' ? name.trim().slice(0, DUPLICATE_NAME_MAX) : ''\n return { kind, sourceId, name: trimmed || null }\n}\n\n/**\n * Makes the copy on `hostId` for the person the gate admitted. Refusals are\n * outcomes, never throws: a model's call that names a missing source or a\n * full band is an answer to relay, not a fault.\n */\nexport async function runAiDuplicateResourceTool(\n context: { gate: AiGateContext; hostId: string },\n input: Record<string, unknown>,\n): Promise<AiDuplicateResourceOutcome> {\n const { gate } = context\n const hostId = String(context.hostId ?? '').trim()\n const parsed = parseAiDuplicateResourceInput(input)\n if (!parsed) return { ok: false, status: 400, error: 'That is not a resource this site can copy' }\n if (!gate.reservation?.allowed) {\n return { ok: false, status: 429, error: 'This workspace cannot run AI requests right now' }\n }\n if (!hostId) return { ok: false, status: 400, error: 'Name the site to copy on' }\n\n const host = await gate.firestore.collection('hosts').doc(hostId).get()\n // A site of another workspace is a site this request cannot see.\n if (!host.exists || host.get('orgId') !== gate.orgId) {\n return { ok: false, status: 404, error: 'Unknown site' }\n }\n if (!gate.staff) {\n const membership = await getOrgForUser(gate.uid, gate.orgId)\n const permitted =\n membership?.orgId === gate.orgId &&\n (await memberHasPermissionOnHost(gate.orgId, hostId, membership.member, 'ai.generate'))\n if (!permitted) {\n return {\n ok: false,\n status: 403,\n error: `Your role does not include \"${orgPermissionLabel('ai.generate')}\" — ask an organization admin`,\n }\n }\n }\n const memberRole = ((host.get('memberRoles') ?? {}) as Record<string, unknown>)[gate.uid]\n if (!hostRoleCanWrite(memberRole)) {\n return { ok: false, status: 403, error: 'Editing requires the editor role' }\n }\n\n const result = await duplicateResource(parsed.kind, {\n orgId: gate.orgId,\n hostId,\n sourceId: parsed.sourceId,\n name: parsed.name,\n uid: gate.uid,\n email: gate.decoded.email ? String(gate.decoded.email) : null,\n org: gate.org as Record<string, unknown>,\n attemptKey: null,\n })\n if (result.ok === false) return result\n return {\n ok: true,\n kind: parsed.kind,\n id: result.id,\n versionId: result.versionId,\n name: result.name,\n }\n}\n"],"names":["DUPLICABLE_HOST_RESOURCE_KINDS","DUPLICATE_NAME_MAX","isDuplicableHostResourceKind","hostRoleCanWrite","orgPermissionLabel","duplicateResource","getOrgForUser","memberHasPermissionOnHost","AI_DUPLICATE_RESOURCE_TOOL_NAME","AI_DUPLICATE_RESOURCE_TOOL","name","description","inputSchema","type","properties","kind","enum","sourceId","required","additionalProperties","strict","parseAiDuplicateResourceInput","input","trim","length","includes","trimmed","slice","runAiDuplicateResourceTool","context","host","gate","hostId","String","parsed","ok","status","error","reservation","allowed","firestore","collection","doc","get","exists","orgId","staff","membership","uid","permitted","member","memberRole","result","email","decoded","org","attemptKey","id","versionId"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/tools/ai-duplicate-resource-tool.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n DUPLICABLE_HOST_RESOURCE_KINDS,\n DUPLICATE_NAME_MAX,\n isDuplicableHostResourceKind,\n type DuplicableHostResourceKind,\n} from '@aglyn/aglyn/app-utils/duplicate-resource'\nimport { hostRoleCanWrite } from '@aglyn/aglyn/app-utils/organizations'\nimport { orgPermissionLabel } from '@aglyn/aglyn/app-utils/org-permissions'\nimport { duplicateResource } from '@aglyn/tenant-data-admin/server/duplicate-resource'\nimport {\n getOrgForUser,\n memberHasPermissionOnHost,\n} from '@aglyn/tenant-data-admin/server/organizations'\nimport type { AiTool } from '../providers/contract'\nimport type { AiGateContext } from '../runtime/ai-gate'\n\n/**\n * The `duplicate_resource` tool (AGL-2936, AGL-2984): a model that would\n * rebuild something the site already has copies it instead, through core's\n * `duplicateResource` — the same copy the row menus and the console door\n * make, with the same uniqueness rules, the same band arithmetic and the same\n * activity rows.\n *\n * It spends no tokens of its own, so it calls no provider: it runs inside a\n * door or a job step that has already climbed `aiGateLadder`, and takes that\n * ladder's context as its proof — the caller is signed in and verified, the\n * release flag, entitlement and lockdown rungs admitted the request, and the\n * workspace's band and caps admitted a reservation. On top of that it asks\n * what a copy needs whichever door called it: `ai.generate`, because a copy\n * is a generation's output, and a host role that may write the site, because\n * the copy lands there.\n *\n * DRAFTS ONLY, by construction of the copy: a screen copy has no route, a\n * workflow copy no trigger, and nothing places a component, layout or form\n * copy until a person does.\n */\n\nexport const AI_DUPLICATE_RESOURCE_TOOL_NAME = 'duplicate_resource'\n\n/** The structured-output tool a generation offers the model. */\nexport const AI_DUPLICATE_RESOURCE_TOOL: AiTool = {\n name: AI_DUPLICATE_RESOURCE_TOOL_NAME,\n description:\n 'Copy a resource this site already has — a page (kind \"screen\"), an email design, a component, a layout, ' +\n 'a template, a form or a workflow — as a new draft to build from, instead of creating it again. ' +\n 'The copy is unpublished: it has no address, runs nothing and is placed nowhere until a person does.',\n inputSchema: {\n type: 'object',\n properties: {\n kind: {\n type: 'string',\n enum: [...DUPLICABLE_HOST_RESOURCE_KINDS],\n description: 'The kind of resource to copy.',\n },\n sourceId: {\n type: 'string',\n description: 'The id of the resource to copy, as the site inventory lists it.',\n },\n name: {\n type: ['string', 'null'],\n description: `The copy's name, at most ${DUPLICATE_NAME_MAX} characters, or null for \"Copy of\" the source's name.`,\n },\n },\n required: ['kind', 'sourceId', 'name'],\n additionalProperties: false,\n },\n strict: true,\n}\n\n/** A tool call's input, read. */\nexport type AiDuplicateResourceInput = {\n kind: DuplicableHostResourceKind\n sourceId: string\n name: string | null\n}\n\n/** What the tool answers: the copy, or a refusal the caller can relay. */\nexport type AiDuplicateResourceOutcome =\n | { ok: true; kind: DuplicableHostResourceKind; id: string; versionId: string | null; name: string }\n | { ok: false; status: number; error: string }\n\n/** Reads a tool call's input; `null` when it is not one this tool accepts. */\nexport function parseAiDuplicateResourceInput(\n input: Record<string, unknown>,\n): AiDuplicateResourceInput | null {\n const kind = input['kind']\n const sourceId = typeof input['sourceId'] === 'string' ? input['sourceId'].trim() : ''\n const name = input['name']\n if (!isDuplicableHostResourceKind(kind)) return null\n if (!sourceId || sourceId.length > 64 || sourceId.includes('/')) return null\n if (name !== null && typeof name !== 'string') return null\n const trimmed = typeof name === 'string' ? name.trim().slice(0, DUPLICATE_NAME_MAX) : ''\n return { kind, sourceId, name: trimmed || null }\n}\n\n/**\n * Makes the copy on `hostId` for the person the gate admitted. Refusals are\n * outcomes, never throws: a model's call that names a missing source or a\n * full band is an answer to relay, not a fault.\n */\nexport async function runAiDuplicateResourceTool(\n context: { gate: AiGateContext; hostId: string },\n input: Record<string, unknown>,\n): Promise<AiDuplicateResourceOutcome> {\n const { gate } = context\n const hostId = String(context.hostId ?? '').trim()\n const parsed = parseAiDuplicateResourceInput(input)\n if (!parsed) return { ok: false, status: 400, error: 'That is not a resource this site can copy' }\n if (!gate.reservation?.allowed) {\n return { ok: false, status: 429, error: 'This workspace cannot run AI requests right now' }\n }\n if (!hostId) return { ok: false, status: 400, error: 'Name the site to copy on' }\n\n const host = await gate.firestore.collection('hosts').doc(hostId).get()\n // A site of another workspace is a site this request cannot see.\n if (!host.exists || host.get('orgId') !== gate.orgId) {\n return { ok: false, status: 404, error: 'Unknown site' }\n }\n if (!gate.staff) {\n const membership = await getOrgForUser(gate.uid, gate.orgId)\n const permitted =\n membership?.orgId === gate.orgId &&\n (await memberHasPermissionOnHost(gate.orgId, hostId, membership.member, 'ai.generate'))\n if (!permitted) {\n return {\n ok: false,\n status: 403,\n error: `Your role does not include \"${orgPermissionLabel('ai.generate')}\" — ask an organization admin`,\n }\n }\n }\n const memberRole = ((host.get('memberRoles') ?? {}) as Record<string, unknown>)[gate.uid]\n if (!hostRoleCanWrite(memberRole)) {\n return { ok: false, status: 403, error: 'Editing requires the editor role' }\n }\n\n const result = await duplicateResource(parsed.kind, {\n orgId: gate.orgId,\n hostId,\n sourceId: parsed.sourceId,\n name: parsed.name,\n uid: gate.uid,\n email: gate.decoded.email ? String(gate.decoded.email) : null,\n org: gate.org as Record<string, unknown>,\n attemptKey: null,\n })\n if (result.ok === false) return result\n return {\n ok: true,\n kind: parsed.kind,\n id: result.id,\n versionId: result.versionId,\n name: result.name,\n }\n}\n"],"names":["DUPLICABLE_HOST_RESOURCE_KINDS","DUPLICATE_NAME_MAX","isDuplicableHostResourceKind","hostRoleCanWrite","orgPermissionLabel","duplicateResource","getOrgForUser","memberHasPermissionOnHost","AI_DUPLICATE_RESOURCE_TOOL_NAME","AI_DUPLICATE_RESOURCE_TOOL","name","description","inputSchema","type","properties","kind","enum","sourceId","required","additionalProperties","strict","parseAiDuplicateResourceInput","input","trim","length","includes","trimmed","slice","runAiDuplicateResourceTool","context","host","gate","hostId","String","parsed","ok","status","error","reservation","allowed","firestore","collection","doc","get","exists","orgId","staff","membership","uid","permitted","member","memberRole","result","email","decoded","org","attemptKey","id","versionId"],"mappings":"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"}
@@ -14,11 +14,12 @@
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
16
  */
17
+ import type { SeoFindingCode } from '@aglyn/aglyn/app-utils/seo-audit';
17
18
  import { type SeoListingFieldKey } from '@aglyn/aglyn/app-utils/seo-listing-fields';
18
- import type { AiSeoFieldValues, AiSeoFindingCode, AiSeoKeywordCoverage, AiSeoSiteFormField } from '../model/ai-seo';
19
+ import type { SeoPageFacts } from '@aglyn/aglyn/app-utils/seo-page-facts';
20
+ import type { AiSeoFieldValues, AiSeoSiteFormField } from '../model/ai-seo';
19
21
  import type { AiTool } from '../providers/contract';
20
22
  import type { AiDoctrineViolation } from '../runtime/ai-doctrine-validators';
21
- import type { AiSeoPageFacts } from '../runtime/seo-page-facts';
22
23
  /**
23
24
  * The strict tools SEO by AI answers through (AGL-2910), and the checks that
24
25
  * hold each answer to the editor it proposes for.
@@ -37,21 +38,8 @@ import type { AiSeoPageFacts } from '../runtime/seo-page-facts';
37
38
  * in the answer is broken, and otherwise the violations and the parts at
38
39
  * fault, for the generation call's one re-ask.
39
40
  */
40
- /** Target keywords one listing may be asked to cover. */
41
- export declare const AI_SEO_MAX_KEYWORDS = 5;
42
41
  /** The most times one keyword may appear, per field, before it reads as stuffing. */
43
42
  export declare const AI_SEO_KEYWORD_LIMITS: Readonly<Partial<Record<SeoListingFieldKey, number>>>;
44
- /** How many times `keyword` appears in `text` as whole words, any case. */
45
- export declare function aiSeoKeywordCount(text: string | null | undefined, keyword: string): number;
46
- /** Keywords as a person typed them: trimmed, deduplicated, capped. */
47
- export declare function aiSeoKeywordList(raw: unknown): string[];
48
- /** Where a page already says each target keyword. */
49
- export declare function aiSeoKeywordCoverage(keywords: readonly string[], sources: {
50
- title?: string | null;
51
- description?: string | null;
52
- h1?: string | null;
53
- body?: string | null;
54
- }): AiSeoKeywordCoverage[];
55
43
  export declare const AI_SEO_FIELDS_TOOL_NAME = "propose_search_listing";
56
44
  /** The fields a listing proposal must fill; the image description may be left out. */
57
45
  export declare const AI_SEO_REQUIRED_FIELDS: ReadonlySet<SeoListingFieldKey>;
@@ -87,7 +75,7 @@ export declare const AI_SEO_FIX_ALT_MAX_CHARS: number;
87
75
  /** Undescribed images one page of a batch gets descriptions for; the rest wait for the next audit. */
88
76
  export declare const AI_SEO_FIX_IMAGES_PER_PAGE = 3;
89
77
  /** The undescribed images a batch asks about for a page, in page order. */
90
- export declare function aiSeoFixImages(page: Pick<AiSeoBatchPage, 'facts'>): AiSeoPageFacts['imagesMissingAlt'];
78
+ export declare function aiSeoFixImages(page: Pick<AiSeoBatchPage, 'facts'>): SeoPageFacts['imagesMissingAlt'];
91
79
  /** The strict tool for one batch: a title, description, heading and image descriptions per page. */
92
80
  export declare function aiSeoFixesTool(screenIds: readonly string[]): AiTool;
93
81
  /** A batch page as the fixes prompt and check read it. */
@@ -95,7 +83,7 @@ export interface AiSeoBatchPage {
95
83
  screenId: string;
96
84
  path: string;
97
85
  name: string;
98
- codes: ReadonlySet<AiSeoFindingCode>;
86
+ codes: ReadonlySet<SeoFindingCode>;
99
87
  keywords: readonly string[];
100
88
  seo: {
101
89
  title?: string;
@@ -104,7 +92,7 @@ export interface AiSeoBatchPage {
104
92
  image?: string;
105
93
  imageAlt?: string;
106
94
  } | undefined;
107
- facts: AiSeoPageFacts;
95
+ facts: SeoPageFacts;
108
96
  }
109
97
  /** The findings that let a batch answer propose each value. */
110
98
  export declare const AI_SEO_FIX_CODES: {
@@ -14,6 +14,7 @@
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
16
  */ import { MEDIA_ALT_MAX_LENGTH } from "@aglyn/aglyn/app-utils/media-alt";
17
+ import { seoKeywordCount } from "@aglyn/aglyn/app-utils/seo-keywords";
17
18
  import { SEO_LISTING_FIELDS } from "@aglyn/aglyn/app-utils/seo-listing-fields";
18
19
  import { HostEntityType } from "@aglyn/aglyn/foundation/definitions/platform.types";
19
20
  import { AI_TEXT_LIMITS } from "../runtime/ai-palette.js";
@@ -35,45 +36,13 @@ import { AI_TEXT_LIMITS } from "../runtime/ai-palette.js";
35
36
  * in the answer is broken, and otherwise the violations and the parts at
36
37
  * fault, for the generation call's one re-ask.
37
38
  */ /* ------------------------------------------------------------------------ *
38
- * Keywords: coverage, and what stuffing looks like
39
- * ------------------------------------------------------------------------ */ /** Target keywords one listing may be asked to cover. */ export const AI_SEO_MAX_KEYWORDS = 5;
40
- /** The most times one keyword may appear, per field, before it reads as stuffing. */ export const AI_SEO_KEYWORD_LIMITS = {
39
+ * Keywords: what stuffing looks like. Coverage — whether a page says a
40
+ * keyword at all — is the platform's (`app-utils/seo-keywords`).
41
+ * ------------------------------------------------------------------------ */ /** The most times one keyword may appear, per field, before it reads as stuffing. */ export const AI_SEO_KEYWORD_LIMITS = {
41
42
  title: 1,
42
43
  description: 2,
43
44
  breadcrumb: 1
44
45
  };
45
- const escapeRegExp = (value)=>value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
46
- /** How many times `keyword` appears in `text` as whole words, any case. */ export function aiSeoKeywordCount(text, keyword) {
47
- const needle = keyword.trim().toLowerCase();
48
- if (!needle || !text) return 0;
49
- const pattern = new RegExp(`(^|[^\\p{L}\\p{N}])${escapeRegExp(needle).replace(/\s+/g, '\\s+')}(?=$|[^\\p{L}\\p{N}])`, 'giu');
50
- return [
51
- ...text.toLowerCase().matchAll(pattern)
52
- ].length;
53
- }
54
- /** Keywords as a person typed them: trimmed, deduplicated, capped. */ export function aiSeoKeywordList(raw) {
55
- const parts = Array.isArray(raw) ? raw.map(String) : String(raw != null ? raw : '').split(/[,\n]/);
56
- const seen = new Set();
57
- const out = [];
58
- for (const part of parts){
59
- const keyword = part.replace(/\s+/g, ' ').trim().slice(0, 60);
60
- const key = keyword.toLowerCase();
61
- if (!keyword || seen.has(key)) continue;
62
- seen.add(key);
63
- out.push(keyword);
64
- if (out.length >= AI_SEO_MAX_KEYWORDS) break;
65
- }
66
- return out;
67
- }
68
- /** Where a page already says each target keyword. */ export function aiSeoKeywordCoverage(keywords, sources) {
69
- return keywords.map((keyword)=>({
70
- keyword,
71
- inTitle: aiSeoKeywordCount(sources.title, keyword) > 0,
72
- inDescription: aiSeoKeywordCount(sources.description, keyword) > 0,
73
- inH1: aiSeoKeywordCount(sources.h1, keyword) > 0,
74
- inBody: aiSeoKeywordCount(sources.body, keyword) > 0
75
- }));
76
- }
77
46
  /** A word of four letters or more used three times in one short field reads as stuffing. */ function repeatedWord(text) {
78
47
  var _text_toLowerCase_match;
79
48
  const counts = new Map();
@@ -176,7 +145,7 @@ const normalizeTitle = (value)=>value.replace(/\s+/g, ' ').trim().toLowerCase();
176
145
  continue;
177
146
  }
178
147
  const limit = AI_SEO_KEYWORD_LIMITS[key];
179
- const stuffed = limit ? ((_context_keywords = context.keywords) != null ? _context_keywords : []).find((keyword)=>aiSeoKeywordCount(value, keyword) > limit) : undefined;
148
+ const stuffed = limit ? ((_context_keywords = context.keywords) != null ? _context_keywords : []).find((keyword)=>seoKeywordCount(value, keyword) > limit) : undefined;
180
149
  if (stuffed) {
181
150
  violations.push({
182
151
  rule: null,
@@ -353,7 +322,7 @@ const normalizeTitle = (value)=>value.replace(/\s+/g, ' ').trim().toLowerCase();
353
322
  broken.push(`title is ${title.length} characters; the limit is ${SEO_LISTING_FIELDS.title.maxLength}`);
354
323
  } else if (page.keywords.some((keyword)=>{
355
324
  var _AI_SEO_KEYWORD_LIMITS_title;
356
- return aiSeoKeywordCount(title, keyword) > ((_AI_SEO_KEYWORD_LIMITS_title = AI_SEO_KEYWORD_LIMITS.title) != null ? _AI_SEO_KEYWORD_LIMITS_title : 1);
325
+ return seoKeywordCount(title, keyword) > ((_AI_SEO_KEYWORD_LIMITS_title = AI_SEO_KEYWORD_LIMITS.title) != null ? _AI_SEO_KEYWORD_LIMITS_title : 1);
357
326
  })) {
358
327
  broken.push('title repeats a keyword');
359
328
  } else if (taken.has(normalizeTitle(title))) {
@@ -369,7 +338,7 @@ const normalizeTitle = (value)=>value.replace(/\s+/g, ' ').trim().toLowerCase();
369
338
  broken.push(`description is ${description.length} characters; the limit is ${SEO_LISTING_FIELDS.description.maxLength}`);
370
339
  } else if (page.keywords.some((keyword)=>{
371
340
  var _AI_SEO_KEYWORD_LIMITS_description;
372
- return aiSeoKeywordCount(description, keyword) > ((_AI_SEO_KEYWORD_LIMITS_description = AI_SEO_KEYWORD_LIMITS.description) != null ? _AI_SEO_KEYWORD_LIMITS_description : 2);
341
+ return seoKeywordCount(description, keyword) > ((_AI_SEO_KEYWORD_LIMITS_description = AI_SEO_KEYWORD_LIMITS.description) != null ? _AI_SEO_KEYWORD_LIMITS_description : 2);
373
342
  })) {
374
343
  broken.push('description repeats a keyword');
375
344
  } else {