@aglyn/plugins-ai 1.0.0-beta.229 → 1.0.0-beta.231
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.
- package/README.md +3 -3
- package/package.json +17 -17
- package/src/lib/activity/ai-activity-actions.js +7 -1
- package/src/lib/activity/ai-activity-actions.js.map +1 -1
- package/src/lib/components/ai-assist-provider-on-host.component.d.ts +66 -0
- package/src/lib/components/ai-assist-provider-on-host.component.js +78 -0
- package/src/lib/components/ai-assist-provider-on-host.component.js.map +1 -0
- package/src/lib/components/ai-assist-provider.component.js +9 -10
- package/src/lib/components/ai-assist-provider.component.js.map +1 -1
- package/src/lib/components/ai-automation-zones.d.ts +38 -0
- package/src/lib/components/ai-automation-zones.js +5 -1
- package/src/lib/components/ai-automation-zones.js.map +1 -1
- package/src/lib/components/ai-brief-dialog.component.d.ts +20 -2
- package/src/lib/components/ai-brief-dialog.component.js +87 -8
- package/src/lib/components/ai-brief-dialog.component.js.map +1 -1
- package/src/lib/components/ai-campaign-create.component.d.ts +54 -0
- package/src/lib/components/ai-campaign-create.component.js +270 -0
- package/src/lib/components/ai-campaign-create.component.js.map +1 -0
- package/src/lib/components/ai-describe-automation.component.d.ts +8 -3
- package/src/lib/components/ai-describe-automation.component.js +15 -6
- package/src/lib/components/ai-describe-automation.component.js.map +1 -1
- package/src/lib/components/ai-describe-button.component.d.ts +20 -8
- package/src/lib/components/ai-describe-button.component.js +19 -36
- package/src/lib/components/ai-describe-button.component.js.map +1 -1
- package/src/lib/components/ai-describe-email.component.d.ts +31 -0
- package/src/lib/components/ai-describe-email.component.js +22 -0
- package/src/lib/components/ai-describe-email.component.js.map +1 -0
- package/src/lib/components/ai-describe-org-automation.component.d.ts +36 -0
- package/src/lib/components/ai-describe-org-automation.component.js +238 -0
- package/src/lib/components/ai-describe-org-automation.component.js.map +1 -0
- package/src/lib/components/ai-experiment-cards.component.d.ts +9 -1
- package/src/lib/components/ai-experiment-cards.component.js +104 -18
- package/src/lib/components/ai-experiment-cards.component.js.map +1 -1
- package/src/lib/components/ai-explain-automation.component.js +1 -5
- package/src/lib/components/ai-explain-automation.component.js.map +1 -1
- package/src/lib/components/ai-funnel-zones.component.d.ts +61 -0
- package/src/lib/components/ai-funnel-zones.component.js +178 -0
- package/src/lib/components/ai-funnel-zones.component.js.map +1 -0
- package/src/lib/components/ai-insight-card.component.d.ts +42 -0
- package/src/lib/components/ai-insight-card.component.js +137 -0
- package/src/lib/components/ai-insight-card.component.js.map +1 -0
- package/src/lib/components/ai-insight-dialog.component.d.ts +6 -0
- package/src/lib/components/ai-insight-dialog.component.js +9 -3
- package/src/lib/components/ai-insight-dialog.component.js.map +1 -1
- package/src/lib/components/ai-job-plan.component.d.ts +7 -2
- package/src/lib/components/ai-job-plan.component.js +38 -6
- package/src/lib/components/ai-job-plan.component.js.map +1 -1
- package/src/lib/components/ai-job-requests.d.ts +17 -1
- package/src/lib/components/ai-job-requests.js +39 -3
- package/src/lib/components/ai-job-requests.js.map +1 -1
- package/src/lib/components/ai-logic-zones.d.ts +64 -0
- package/src/lib/components/ai-logic-zones.js +18 -0
- package/src/lib/components/ai-logic-zones.js.map +1 -0
- package/src/lib/components/ai-logic.component.d.ts +77 -0
- package/src/lib/components/ai-logic.component.js +553 -0
- package/src/lib/components/ai-logic.component.js.map +1 -0
- package/src/lib/components/ai-marketing-insight.component.d.ts +30 -0
- package/src/lib/components/ai-marketing-insight.component.js +80 -0
- package/src/lib/components/ai-marketing-insight.component.js.map +1 -0
- package/src/lib/components/ai-media-create.component.d.ts +55 -0
- package/src/lib/components/ai-media-create.component.js +433 -0
- package/src/lib/components/ai-media-create.component.js.map +1 -0
- package/src/lib/components/ai-overlay-cards.component.d.ts +83 -0
- package/src/lib/components/ai-overlay-cards.component.js +505 -0
- package/src/lib/components/ai-overlay-cards.component.js.map +1 -0
- package/src/lib/components/ai-permissions-on-host.component.d.ts +3 -46
- package/src/lib/components/ai-permissions-on-host.component.js +5 -53
- package/src/lib/components/ai-permissions-on-host.component.js.map +1 -1
- package/src/lib/components/ai-product-zones.d.ts +5 -0
- package/src/lib/components/ai-product-zones.js +1 -1
- package/src/lib/components/ai-product-zones.js.map +1 -1
- package/src/lib/components/ai-products-create-button.component.d.ts +42 -0
- package/src/lib/components/ai-products-create-button.component.js +67 -0
- package/src/lib/components/ai-products-create-button.component.js.map +1 -0
- package/src/lib/components/ai-products-hub-card.component.js +100 -81
- package/src/lib/components/ai-products-hub-card.component.js.map +1 -1
- package/src/lib/components/ai-revise-automation.component.d.ts +16 -0
- package/src/lib/components/ai-revise-automation.component.js +207 -0
- package/src/lib/components/ai-revise-automation.component.js.map +1 -0
- package/src/lib/components/ai-site-build-page.component.js +160 -24
- package/src/lib/components/ai-site-build-page.component.js.map +1 -1
- package/src/lib/components/ai-upsell-dialog.component.d.ts +53 -0
- package/src/lib/components/ai-upsell-dialog.component.js +177 -0
- package/src/lib/components/ai-upsell-dialog.component.js.map +1 -0
- package/src/lib/components/assist-build-card.component.d.ts +42 -0
- package/src/lib/components/assist-build-card.component.js +183 -0
- package/src/lib/components/assist-build-card.component.js.map +1 -0
- package/src/lib/components/assist-edit-canvas.d.ts +2 -0
- package/src/lib/components/assist-edit-canvas.js +56 -1
- package/src/lib/components/assist-edit-canvas.js.map +1 -1
- package/src/lib/components/assist-jobs-drawer.component.js +48 -3
- package/src/lib/components/assist-jobs-drawer.component.js.map +1 -1
- package/src/lib/components/assist-panel.component.js +51 -2
- package/src/lib/components/assist-panel.component.js.map +1 -1
- package/src/lib/components/use-ai-job-run.d.ts +3 -14
- package/src/lib/components/use-ai-job-run.js +0 -50
- package/src/lib/components/use-ai-job-run.js.map +1 -1
- package/src/lib/jobs/ai-build-capabilities.d.ts +58 -0
- package/src/lib/jobs/ai-build-capabilities.js +256 -0
- package/src/lib/jobs/ai-build-capabilities.js.map +1 -0
- package/src/lib/jobs/ai-build-unit-outcome.d.ts +52 -0
- package/src/lib/jobs/ai-build-unit-outcome.js +140 -0
- package/src/lib/jobs/ai-build-unit-outcome.js.map +1 -0
- package/src/lib/jobs/ai-job-auto-confirm.d.ts +8 -0
- package/src/lib/jobs/ai-job-auto-confirm.js +30 -6
- package/src/lib/jobs/ai-job-auto-confirm.js.map +1 -1
- package/src/lib/jobs/ai-job-build-step.d.ts +144 -0
- package/src/lib/jobs/ai-job-build-step.js +668 -0
- package/src/lib/jobs/ai-job-build-step.js.map +1 -0
- package/src/lib/jobs/ai-job-draft-ids.js +9 -1
- package/src/lib/jobs/ai-job-draft-ids.js.map +1 -1
- package/src/lib/jobs/ai-job-drafts.d.ts +14 -4
- package/src/lib/jobs/ai-job-drafts.js +25 -4
- package/src/lib/jobs/ai-job-drafts.js.map +1 -1
- package/src/lib/jobs/ai-job-generation.d.ts +8 -0
- package/src/lib/jobs/ai-job-generation.js +13 -2
- package/src/lib/jobs/ai-job-generation.js.map +1 -1
- package/src/lib/jobs/ai-job-insight-step.js +8 -1
- package/src/lib/jobs/ai-job-insight-step.js.map +1 -1
- package/src/lib/jobs/ai-job-layout-language.d.ts +73 -0
- package/src/lib/jobs/ai-job-layout-language.js +194 -0
- package/src/lib/jobs/ai-job-layout-language.js.map +1 -0
- package/src/lib/jobs/ai-job-layout-site-pages.d.ts +13 -2
- package/src/lib/jobs/ai-job-layout-site-pages.js +18 -2
- package/src/lib/jobs/ai-job-layout-site-pages.js.map +1 -1
- package/src/lib/jobs/ai-job-layout-step.d.ts +5 -0
- package/src/lib/jobs/ai-job-layout-step.js +105 -5
- package/src/lib/jobs/ai-job-layout-step.js.map +1 -1
- package/src/lib/jobs/ai-job-logic-step.d.ts +107 -0
- package/src/lib/jobs/ai-job-logic-step.js +395 -0
- package/src/lib/jobs/ai-job-logic-step.js.map +1 -0
- package/src/lib/jobs/ai-job-overlay-copy.d.ts +70 -0
- package/src/lib/jobs/ai-job-overlay-copy.js +178 -0
- package/src/lib/jobs/ai-job-overlay-copy.js.map +1 -0
- package/src/lib/jobs/ai-job-page-language.d.ts +149 -0
- package/src/lib/jobs/ai-job-page-language.js +355 -0
- package/src/lib/jobs/ai-job-page-language.js.map +1 -0
- package/src/lib/jobs/ai-job-page-sections.d.ts +37 -1
- package/src/lib/jobs/ai-job-page-sections.js +93 -11
- package/src/lib/jobs/ai-job-page-sections.js.map +1 -1
- package/src/lib/jobs/ai-job-page-step.d.ts +4 -4
- package/src/lib/jobs/ai-job-page-step.js +111 -20
- package/src/lib/jobs/ai-job-page-step.js.map +1 -1
- package/src/lib/jobs/ai-job-plan-step.d.ts +27 -2
- package/src/lib/jobs/ai-job-plan-step.js +87 -13
- package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
- package/src/lib/jobs/ai-job-site-step.d.ts +19 -1
- package/src/lib/jobs/ai-job-site-step.js +220 -61
- package/src/lib/jobs/ai-job-site-step.js.map +1 -1
- package/src/lib/jobs/ai-job-text-step.d.ts +22 -1
- package/src/lib/jobs/ai-job-text-step.js +12 -0
- package/src/lib/jobs/ai-job-text-step.js.map +1 -1
- package/src/lib/jobs/ai-job-workflow-step.d.ts +54 -4
- package/src/lib/jobs/ai-job-workflow-step.js +320 -15
- package/src/lib/jobs/ai-job-workflow-step.js.map +1 -1
- package/src/lib/jobs/ai-jobs.d.ts +80 -2
- package/src/lib/jobs/ai-jobs.js +391 -18
- package/src/lib/jobs/ai-jobs.js.map +1 -1
- package/src/lib/jobs/ai-layout-full-height.d.ts +40 -0
- package/src/lib/jobs/ai-layout-full-height.js +82 -0
- package/src/lib/jobs/ai-layout-full-height.js.map +1 -0
- package/src/lib/jobs/ai-layout-site-facts.d.ts +54 -0
- package/src/lib/jobs/ai-layout-site-facts.js +153 -0
- package/src/lib/jobs/ai-layout-site-facts.js.map +1 -0
- package/src/lib/jobs/ai-logic-records.d.ts +40 -0
- package/src/lib/jobs/ai-logic-records.js +55 -0
- package/src/lib/jobs/ai-logic-records.js.map +1 -0
- package/src/lib/jobs/ai-site-publish.d.ts +7 -0
- package/src/lib/jobs/ai-site-publish.js +9 -0
- package/src/lib/jobs/ai-site-publish.js.map +1 -1
- package/src/lib/jobs/ai-workflow-records.d.ts +4 -0
- package/src/lib/jobs/ai-workflow-records.js +40 -1
- package/src/lib/jobs/ai-workflow-records.js.map +1 -1
- package/src/lib/layout-language/ai-layout-compiler.d.ts +145 -0
- package/src/lib/layout-language/ai-layout-compiler.js +1148 -0
- package/src/lib/layout-language/ai-layout-compiler.js.map +1 -0
- package/src/lib/layout-language/ai-layout-copy.d.ts +73 -0
- package/src/lib/layout-language/ai-layout-copy.js +137 -0
- package/src/lib/layout-language/ai-layout-copy.js.map +1 -0
- package/src/lib/layout-language/ai-layout-frame.d.ts +52 -0
- package/src/lib/layout-language/ai-layout-frame.js +457 -0
- package/src/lib/layout-language/ai-layout-frame.js.map +1 -0
- package/src/lib/layout-language/ai-layout-gaps.d.ts +63 -0
- package/src/lib/layout-language/ai-layout-gaps.js +345 -0
- package/src/lib/layout-language/ai-layout-gaps.js.map +1 -0
- package/src/lib/layout-language/ai-layout-language.d.ts +161 -0
- package/src/lib/layout-language/ai-layout-language.js +446 -0
- package/src/lib/layout-language/ai-layout-language.js.map +1 -0
- package/src/lib/layout-language/ai-layout-links.d.ts +80 -0
- package/src/lib/layout-language/ai-layout-links.js +109 -0
- package/src/lib/layout-language/ai-layout-links.js.map +1 -0
- package/src/lib/layout-language/ai-layout-store.d.ts +39 -0
- package/src/lib/layout-language/ai-layout-store.js +69 -0
- package/src/lib/layout-language/ai-layout-store.js.map +1 -0
- package/src/lib/layout-language/ai-layout-tree.d.ts +59 -0
- package/src/lib/layout-language/ai-layout-tree.js +84 -0
- package/src/lib/layout-language/ai-layout-tree.js.map +1 -0
- package/src/lib/lazy-widget.d.ts +45 -0
- package/src/lib/lazy-widget.js +53 -0
- package/src/lib/lazy-widget.js.map +1 -0
- package/src/lib/model/ai-automation-draft.d.ts +18 -0
- package/src/lib/model/ai-automation-draft.js +86 -0
- package/src/lib/model/ai-automation-draft.js.map +1 -1
- package/src/lib/model/ai-build-job.d.ts +201 -0
- package/src/lib/model/ai-build-job.js +472 -0
- package/src/lib/model/ai-build-job.js.map +1 -0
- package/src/lib/model/ai-build-plan.d.ts +61 -0
- package/src/lib/model/ai-build-plan.js +154 -9
- package/src/lib/model/ai-build-plan.js.map +1 -1
- package/src/lib/model/ai-build-progress.d.ts +73 -0
- package/src/lib/model/ai-build-progress.js +170 -0
- package/src/lib/model/ai-build-progress.js.map +1 -0
- package/src/lib/model/ai-host-publish-context.d.ts +51 -0
- package/src/lib/model/ai-host-publish-context.js +104 -0
- package/src/lib/model/ai-host-publish-context.js.map +1 -0
- package/src/lib/model/ai-insight.d.ts +20 -10
- package/src/lib/model/ai-insight.js +72 -9
- package/src/lib/model/ai-insight.js.map +1 -1
- package/src/lib/model/ai-job-activity.d.ts +22 -0
- package/src/lib/model/ai-job-activity.js +25 -2
- package/src/lib/model/ai-job-activity.js.map +1 -1
- package/src/lib/model/ai-job-failure-copy.d.ts +38 -1
- package/src/lib/model/ai-job-failure-copy.js +56 -1
- package/src/lib/model/ai-job-failure-copy.js.map +1 -1
- package/src/lib/model/ai-job-notice.d.ts +3 -1
- package/src/lib/model/ai-job-notice.js +31 -9
- package/src/lib/model/ai-job-notice.js.map +1 -1
- package/src/lib/model/ai-jobs.types.d.ts +99 -2
- package/src/lib/model/ai-jobs.types.js +3 -1
- package/src/lib/model/ai-jobs.types.js.map +1 -1
- package/src/lib/model/ai-logic-job.d.ts +108 -0
- package/src/lib/model/ai-logic-job.js +327 -0
- package/src/lib/model/ai-logic-job.js.map +1 -0
- package/src/lib/model/ai-media-credits.d.ts +52 -0
- package/src/lib/model/ai-media-credits.js +61 -0
- package/src/lib/model/ai-media-credits.js.map +1 -0
- package/src/lib/model/ai-overlay-copy.d.ts +114 -0
- package/src/lib/model/ai-overlay-copy.js +175 -0
- package/src/lib/model/ai-overlay-copy.js.map +1 -0
- package/src/lib/model/ai-site-build-progress.d.ts +25 -4
- package/src/lib/model/ai-site-build-progress.js +194 -11
- package/src/lib/model/ai-site-build-progress.js.map +1 -1
- package/src/lib/model/ai-site-job.d.ts +8 -0
- package/src/lib/model/ai-site-job.js +9 -0
- package/src/lib/model/ai-site-job.js.map +1 -1
- package/src/lib/model/ai-site-start-seo.d.ts +1 -1
- package/src/lib/model/ai-site-start.d.ts +5 -2
- package/src/lib/model/ai-site-start.js +5 -2
- package/src/lib/model/ai-site-start.js.map +1 -1
- package/src/lib/model/ai-svg.d.ts +69 -0
- package/src/lib/model/ai-svg.js +265 -0
- package/src/lib/model/ai-svg.js.map +1 -0
- package/src/lib/model/ai-usage-by-user.d.ts +3 -2
- package/src/lib/model/ai-usage-by-user.js.map +1 -1
- package/src/lib/model/ai-workflow-job.d.ts +92 -4
- package/src/lib/model/ai-workflow-job.js +124 -7
- package/src/lib/model/ai-workflow-job.js.map +1 -1
- package/src/lib/model/assist-build.d.ts +58 -0
- package/src/lib/model/assist-build.js +52 -0
- package/src/lib/model/assist-build.js.map +1 -0
- package/src/lib/model/assist-edit.d.ts +48 -3
- package/src/lib/model/assist-edit.js +48 -4
- package/src/lib/model/assist-edit.js.map +1 -1
- package/src/lib/plugin-config.js +2 -0
- package/src/lib/plugin-config.js.map +1 -1
- package/src/lib/plugin.js +308 -47
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/providers/catalog.d.ts +82 -5
- package/src/lib/providers/catalog.js +83 -12
- package/src/lib/providers/catalog.js.map +1 -1
- package/src/lib/providers/image-contract.d.ts +116 -0
- package/src/lib/providers/image-contract.js +74 -0
- package/src/lib/providers/image-contract.js.map +1 -0
- package/src/lib/providers/model-choice.js +11 -0
- package/src/lib/providers/model-choice.js.map +1 -1
- package/src/lib/providers/routing.js +18 -0
- package/src/lib/providers/routing.js.map +1 -1
- package/src/lib/providers/vertex-image.d.ts +121 -0
- package/src/lib/providers/vertex-image.js +312 -0
- package/src/lib/providers/vertex-image.js.map +1 -0
- package/src/lib/runtime/ai-doctrine-validators.d.ts +101 -0
- package/src/lib/runtime/ai-doctrine-validators.js +525 -4
- package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
- package/src/lib/runtime/ai-doctrine.js +21 -6
- package/src/lib/runtime/ai-doctrine.js.map +1 -1
- package/src/lib/runtime/ai-eval-live.js +4 -0
- package/src/lib/runtime/ai-eval-live.js.map +1 -1
- package/src/lib/runtime/ai-eval.d.ts +15 -1
- package/src/lib/runtime/ai-eval.js +52 -1
- package/src/lib/runtime/ai-eval.js.map +1 -1
- package/src/lib/runtime/ai-icon-library.d.ts +257 -0
- package/src/lib/runtime/ai-icon-library.js +254 -0
- package/src/lib/runtime/ai-icon-library.js.map +1 -0
- package/src/lib/runtime/ai-node-tree.d.ts +9 -0
- package/src/lib/runtime/ai-node-tree.js +30 -4
- package/src/lib/runtime/ai-node-tree.js.map +1 -1
- package/src/lib/runtime/ai-palette.d.ts +6 -0
- package/src/lib/runtime/ai-palette.generated.js +70 -0
- package/src/lib/runtime/ai-palette.generated.js.map +1 -1
- package/src/lib/runtime/ai-palette.js.map +1 -1
- package/src/lib/runtime/experiment-variant-copy.d.ts +34 -0
- package/src/lib/runtime/experiment-variant-copy.js +102 -0
- package/src/lib/runtime/experiment-variant-copy.js.map +1 -0
- package/src/lib/server/ai-experiment-versions.d.ts +51 -0
- package/src/lib/server/ai-experiment-versions.js +270 -0
- package/src/lib/server/ai-experiment-versions.js.map +1 -0
- package/src/lib/server/ai-jobs-resume.js +74 -4
- package/src/lib/server/ai-jobs-resume.js.map +1 -1
- package/src/lib/server/ai-media-image.d.ts +66 -0
- package/src/lib/server/ai-media-image.js +521 -0
- package/src/lib/server/ai-media-image.js.map +1 -0
- package/src/lib/server/ai-media-svg.d.ts +91 -0
- package/src/lib/server/ai-media-svg.js +186 -0
- package/src/lib/server/ai-media-svg.js.map +1 -0
- package/src/lib/server/assist-build.d.ts +54 -0
- package/src/lib/server/assist-build.js +121 -0
- package/src/lib/server/assist-build.js.map +1 -0
- package/src/lib/server/assist-chat.js +108 -12
- package/src/lib/server/assist-chat.js.map +1 -1
- package/src/lib/server/assist-edit.js +213 -3
- package/src/lib/server/assist-edit.js.map +1 -1
- package/src/lib/server/assist-view-context.js +242 -4
- package/src/lib/server/assist-view-context.js.map +1 -1
- package/src/lib/server.js +26 -0
- package/src/lib/server.js.map +1 -1
- package/src/lib/tools/ai-logic-tool.d.ts +27 -0
- package/src/lib/tools/ai-logic-tool.js +207 -0
- package/src/lib/tools/ai-logic-tool.js.map +1 -0
- package/src/lib/tools/ai-overlay-tool.d.ts +35 -0
- package/src/lib/tools/ai-overlay-tool.js +107 -0
- package/src/lib/tools/ai-overlay-tool.js.map +1 -0
- package/src/lib/tools/ai-workflow-tool.d.ts +17 -2
- package/src/lib/tools/ai-workflow-tool.js +47 -14
- package/src/lib/tools/ai-workflow-tool.js.map +1 -1
- package/src/lib/usage/assist-job-refund.d.ts +28 -1
- package/src/lib/usage/assist-job-refund.js +44 -9
- package/src/lib/usage/assist-job-refund.js.map +1 -1
- package/src/lib/usage/assist-usage.d.ts +5 -0
- package/src/lib/usage/assist-usage.js +2 -0
- package/src/lib/usage/assist-usage.js.map +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/runtime/ai-palette.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 type { JsonLinealOrder } from '@aglyn/aglyn/app-utils/lineal-order'\n\n/**\n * The element palette as a model sees it (AGL-2905).\n *\n * `ai-palette.generated.ts` is produced from the registered plugin bundles by\n * `libs/plugins/ai/scripts/generate-ai-palette.mts` and holds the data; this module\n * holds the shapes that data is typed against and the small readers over it,\n * so a regeneration never rewrites a type.\n */\n\n/** The document kinds a model is asked to compose. */\nexport type AiSurface = 'screen' | 'email' | 'form' | 'layout' | 'component'\n\nexport const AI_SURFACE_NAMES: readonly AiSurface[] = [\n 'screen',\n 'email',\n 'form',\n 'layout',\n 'component',\n]\n\n/**\n * The subset of JSON Schema a prop declaration is expressed in. Enough to\n * validate what an attribute field can persist; nothing the field editor\n * cannot itself model.\n */\nexport interface AiPropSchema {\n type?: 'string' | 'number' | 'integer' | 'boolean'\n enum?: readonly string[]\n minLength?: number\n maxLength?: number\n minimum?: number\n maximum?: number\n pattern?: string\n description?: string\n}\n\nexport interface AiPropsSchema {\n type: 'object'\n properties: Record<string, AiPropSchema>\n required: readonly string[]\n additionalProperties: false\n}\n\n/**\n * What a string prop MEANS, which decides how the validator checks it.\n *\n * - `text` — copy the visitor reads; held to a length ceiling.\n * - `url` — a navigable address: an `https:` URL or a root-relative path.\n * - `screen` — a screen id, checked against the screens the host has.\n * - `media` — an image or video source: a DAM reference or an `https:` URL.\n */\nexport type AiPropRole = 'text' | 'url' | 'screen' | 'media'\n\n/**\n * A `restrictChildren` / `restrictParent` directive as generated data: the\n * same tuple the schema declares, with the flag spelled as the string the\n * enum holds so a JSON emitter can write it.\n */\nexport type AiLinealOrder = JsonLinealOrder\n\nexport interface AiPaletteEntry {\n pluginId: string\n kind: 'element' | 'plaintext' | 'markdown'\n category: string\n displayName: string\n /** One sentence, from the schema's own description. */\n summary: string\n /** Whether the editor lets anything be dropped inside this element. */\n acceptsChildren: boolean\n restrictChildren?: AiLinealOrder\n restrictParent?: AiLinealOrder\n propsSchema: AiPropsSchema\n /** Roles of the string props the validator treats specially. */\n propRoles: Record<string, AiPropRole>\n /**\n * The attribute field kind each declared prop is edited with, as its\n * `FieldComponentType` value (`switch`, `select`, `screen-select`, …): what\n * the editor reads to decide which component properties a field can be\n * bound to (AGL-2908).\n */\n propFields: Record<string, string>\n /** Length ceilings for `text` props, in characters. */\n textLimits: Record<string, number>\n /** Names of the presets that place this component. */\n presets: readonly string[]\n}\n\n/**\n * What every NODE can do, as against what one component declares (AGL-3156).\n *\n * Generated from the node capabilities core declares, beside the per-component\n * schemas rather than out of them: repeating is not a Stack's feature, so no\n * component schema names it and a palette built from schemas alone advertises\n * it on nothing. Declared ONCE for the whole palette — a model may write these\n * props on any element the surface allows.\n */\nexport interface AiNodeCapability {\n /** Stable id, as core declares it. */\n id: string\n displayName: string\n /** One sentence, for the prompt catalog. */\n summary: string\n propsSchema: AiPropsSchema\n propRoles: Record<string, AiPropRole>\n propFields: Record<string, string>\n textLimits: Record<string, number>\n /** Props offered only on a node that holds a child list. */\n childrenOnlyProps: readonly string[]\n /**\n * Declared attributes the palette deliberately does not offer: a picker\n * that chooses a record the site holds, which a model cannot name from a\n * description. Recorded so a field left off on purpose reads differently\n * from one that went missing.\n */\n omittedProps: readonly string[]\n}\n\n/**\n * The capability props a node with this child contract may carry, merged into\n * one map the prop sanitizer can read beside the element's own declarations.\n */\nexport function nodeCapabilityProps(\n capabilities: Readonly<Record<string, AiNodeCapability>>,\n acceptsChildren: boolean,\n): {\n properties: Record<string, AiPropSchema>\n propRoles: Record<string, AiPropRole>\n textLimits: Record<string, number>\n} {\n const properties: Record<string, AiPropSchema> = {}\n const propRoles: Record<string, AiPropRole> = {}\n const textLimits: Record<string, number> = {}\n for (const capability of Object.values(capabilities)) {\n for (const [name, schema] of Object.entries(\n capability.propsSchema.properties,\n )) {\n if (!acceptsChildren && capability.childrenOnlyProps.includes(name))\n continue\n properties[name] = schema\n const role = capability.propRoles[name]\n if (role) propRoles[name] = role\n const limit = capability.textLimits[name]\n if (limit !== undefined) textLimits[name] = limit\n }\n }\n return { properties, propRoles, textLimits }\n}\n\n/**\n * What a node capability declared in core and one carried by the palette\n * disagree about (AGL-3156).\n *\n * `check:ai-palette` pins the generated file to the generator's output for its\n * inputs, which cannot notice a capability leaving those inputs: when repeat\n * stopped being a component's attribute, the generator regenerated cleanly and\n * the palette simply stopped mentioning it. This reads the core declarations\n * instead and asks whether the palette still accounts for each one, so the\n * next generic capability cannot vanish the same way.\n *\n * `offeredFieldKinds` is every field kind the palette DOES carry somewhere,\n * derived from the generated entries rather than restated here: a capability\n * attribute drawn with one of those kinds has no excuse for being omitted.\n */\nexport function auditNodeCapabilities(\n declared: ReadonlyArray<{\n id: string\n label: string\n attributes: ReadonlyArray<{ name: string; component: string }>\n }>,\n carried: Readonly<Record<string, AiNodeCapability>>,\n offeredFieldKinds: ReadonlySet<string>,\n): string[] {\n const problems: string[] = []\n for (const capability of declared) {\n const entry = carried[capability.id]\n if (!entry) {\n problems.push(\n `the palette carries no node capability \"${capability.id}\": a model cannot use it at all`,\n )\n continue\n }\n if (!Object.keys(entry.propsSchema.properties).length) {\n problems.push(\n `node capability \"${capability.id}\" is carried with no prop a model can write`,\n )\n }\n for (const attribute of capability.attributes) {\n if (entry.propsSchema.properties[attribute.name]) continue\n if (!entry.omittedProps.includes(attribute.name)) {\n problems.push(\n `node capability \"${capability.id}\" declares \"${attribute.name}\", which the palette neither offers nor records as omitted`,\n )\n continue\n }\n if (offeredFieldKinds.has(attribute.component)) {\n problems.push(\n `node capability \"${capability.id}\" omits \"${attribute.name}\", whose \"${attribute.component}\" field the palette offers elsewhere`,\n )\n }\n }\n }\n for (const id of Object.keys(carried)) {\n if (!declared.some((capability) => capability.id === id)) {\n problems.push(\n `the palette carries node capability \"${id}\", which core no longer declares`,\n )\n }\n }\n return problems\n}\n\nexport interface AiSurfaceDefinition {\n /** The component id at the top of a tree for this surface. */\n root: string\n /** Every component id a node in this surface may carry. */\n allow: readonly string[]\n}\n\n/**\n * Theme vocabulary an `sx` value may name, read off the theme sources so the\n * validator's allowlist is derived rather than typed by hand.\n */\nexport interface AiSxTokens {\n /** Dotted palette paths such as `primary.main`. */\n palette: readonly string[]\n /** Responsive object keys. */\n breakpoints: readonly string[]\n /** Values `typography` may take. */\n typographyVariants: readonly string[]\n}\n\n/**\n * Text ceilings per role of copy (AGL-2905). A headline is one line of a\n * hero, body copy is a paragraph or two, and a button label has to fit a\n * button.\n */\nexport const AI_TEXT_LIMITS = {\n headline: 120,\n body: 2_000,\n button: 40,\n /** A short label, title or caption. */\n label: 200,\n} as const\n\n/**\n * Prompt budget per surface catalog, in characters at four per token.\n * Asserted by a spec so a component that grows its description cannot\n * silently push a system prompt past what the route pays for.\n */\nexport const AI_PALETTE_CATALOG_MAX_TOKENS = 2_500\nexport const AI_PALETTE_CATALOG_MAX_CHARS = AI_PALETTE_CATALOG_MAX_TOKENS * 4\n\n/** The model's own token estimate: characters over four. */\nexport function estimateCatalogTokens(catalog: string): number {\n return Math.ceil(catalog.length / 4)\n}\n\n/**\n * The documents a generator emits (AGL-2935). Each is composed on one\n * surface of the palette and held to its own budget below.\n */\nexport type AiOutputKind =\n | 'page'\n | 'template'\n | 'component'\n | 'layout'\n | 'form'\n | 'email'\n\nexport const AI_OUTPUT_KINDS: readonly AiOutputKind[] = [\n 'page',\n 'template',\n 'component',\n 'layout',\n 'form',\n 'email',\n]\n\n/** The surface each output kind is composed on. */\nexport const AI_OUTPUT_SURFACE: Record<AiOutputKind, AiSurface> = {\n page: 'screen',\n template: 'screen',\n component: 'component',\n layout: 'layout',\n form: 'form',\n email: 'email',\n}\n\n/**\n * What the rule-17 scorer measures on an emitted tree:\n *\n * - `nodes` — how many elements the tree carries;\n * - `bytes` — the stored node map, in the unit the save ceiling uses\n * (`nodeMapBytes`, msgpack);\n * - `imageBytes` — what the placed library images request at the slot's\n * variant, from each asset's recorded size;\n * - `embeds` — third-party players and frames the output loads;\n * - `fontFamilies` — families named beyond the theme's own;\n * - `emailHtmlBytes` — an email's rendered HTML, which is what a mail\n * client clips.\n */\nexport type AiBudgetMetric =\n | 'nodes'\n | 'bytes'\n | 'imageBytes'\n | 'embeds'\n | 'fontFamilies'\n | 'emailHtmlBytes'\n\nexport type AiOutputBudget = Record<Exclude<AiBudgetMetric, 'emailHtmlBytes'>, number> & {\n emailHtmlBytes?: number\n}\n\n/**\n * The size past which Gmail clips a message and hides the rest behind a\n * \"View entire message\" link — the unsubscribe footer included.\n */\nexport const AI_EMAIL_CLIP_BYTES = 102_000\n\n/**\n * The budget per output kind (AGL-2935, rule 17), beside the catalog budget\n * above for the same reason: a generator that grows its output past what\n * its kind is for is caught by a number, not noticed by a reader.\n *\n * A page is a few hundred elements and tens of kilobytes of stored map; a\n * reusable component is one repeated block; a layout is the chrome every\n * page loads, so it carries the least imagery; a form is fields and a\n * button; an email is held to the clipping threshold on its rendered HTML.\n * No kind names a font beyond the theme's.\n *\n * A third-party player is a page's or a component's to carry, and only where\n * a confirmed plan lists it (rule 16). A template is rendered for every\n * record it serves and a layout around every page, so neither carries one\n * (AGL-3433): an embed there loads a stranger's code on every page of a\n * collection or a site.\n */\nexport const AI_OUTPUT_BUDGETS: Record<AiOutputKind, AiOutputBudget> = {\n page: {\n nodes: 400,\n bytes: 60_000,\n imageBytes: 1_500_000,\n embeds: 1,\n fontFamilies: 0,\n },\n template: {\n nodes: 400,\n bytes: 60_000,\n imageBytes: 1_500_000,\n embeds: 0,\n fontFamilies: 0,\n },\n component: {\n nodes: 80,\n bytes: 12_000,\n imageBytes: 500_000,\n embeds: 1,\n fontFamilies: 0,\n },\n layout: {\n nodes: 160,\n bytes: 24_000,\n imageBytes: 200_000,\n embeds: 0,\n fontFamilies: 0,\n },\n form: {\n nodes: 60,\n bytes: 8_000,\n imageBytes: 0,\n embeds: 0,\n fontFamilies: 0,\n },\n email: {\n nodes: 150,\n bytes: 24_000,\n imageBytes: 1_000_000,\n embeds: 0,\n fontFamilies: 0,\n emailHtmlBytes: AI_EMAIL_CLIP_BYTES,\n },\n}\n\n/**\n * What a first visit to a generated output is estimated to transfer — the\n * weight the proposal shows before anything is applied (rule 17). A page\n * starts from the platform's measured page weight; a component or a layout\n * reports only what it adds to a page; an email is not a page load.\n */\nexport interface AiLoadEstimate {\n /** The measured weight of a published page before this output's own bytes; 0 for a fragment. */\n pageBytes: number\n /** The stored node map, in the save's own bytes. */\n documentBytes: number\n /** What the placed library images request at their slots. */\n imageBytes: number\n /** Images the estimate could not size: no recorded size, or not from the library. */\n imagesUnmeasured: number\n embeds: number\n totalBytes: number\n}\n\n/** Typography variants that read as headlines, for the `children` ceiling. */\nexport function isHeadlineVariant(variant: unknown): boolean {\n return typeof variant === 'string' && /^(display\\w*|h[1-6]|heading)$/.test(variant)\n}\n"],"names":["AI_SURFACE_NAMES","nodeCapabilityProps","capabilities","acceptsChildren","properties","propRoles","textLimits","capability","Object","values","name","schema","entries","propsSchema","childrenOnlyProps","includes","role","limit","undefined","auditNodeCapabilities","declared","carried","offeredFieldKinds","problems","entry","id","push","keys","length","attribute","attributes","omittedProps","has","component","some","AI_TEXT_LIMITS","headline","body","button","label","AI_PALETTE_CATALOG_MAX_TOKENS","AI_PALETTE_CATALOG_MAX_CHARS","estimateCatalogTokens","catalog","Math","ceil","AI_OUTPUT_KINDS","AI_OUTPUT_SURFACE","page","template","layout","form","email","AI_EMAIL_CLIP_BYTES","AI_OUTPUT_BUDGETS","nodes","bytes","imageBytes","embeds","fontFamilies","emailHtmlBytes","isHeadlineVariant","variant","test"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/runtime/ai-palette.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 type { JsonLinealOrder } from '@aglyn/aglyn/app-utils/lineal-order'\n\n/**\n * The element palette as a model sees it (AGL-2905).\n *\n * `ai-palette.generated.ts` is produced from the registered plugin bundles by\n * `libs/plugins/ai/scripts/generate-ai-palette.mts` and holds the data; this module\n * holds the shapes that data is typed against and the small readers over it,\n * so a regeneration never rewrites a type.\n */\n\n/** The document kinds a model is asked to compose. */\nexport type AiSurface = 'screen' | 'email' | 'form' | 'layout' | 'component'\n\nexport const AI_SURFACE_NAMES: readonly AiSurface[] = [\n 'screen',\n 'email',\n 'form',\n 'layout',\n 'component',\n]\n\n/**\n * The subset of JSON Schema a prop declaration is expressed in. Enough to\n * validate what an attribute field can persist; nothing the field editor\n * cannot itself model.\n */\nexport interface AiPropSchema {\n type?: 'string' | 'number' | 'integer' | 'boolean'\n enum?: readonly string[]\n minLength?: number\n maxLength?: number\n minimum?: number\n maximum?: number\n pattern?: string\n description?: string\n}\n\nexport interface AiPropsSchema {\n type: 'object'\n properties: Record<string, AiPropSchema>\n required: readonly string[]\n additionalProperties: false\n}\n\n/**\n * What a string prop MEANS, which decides how the validator checks it.\n *\n * - `text` — copy the visitor reads; held to a length ceiling.\n * - `url` — a navigable address: an `https:` URL or a root-relative path.\n * - `screen` — a screen id, checked against the screens the host has.\n * - `media` — an image or video source: a DAM reference or an `https:` URL.\n */\nexport type AiPropRole = 'text' | 'url' | 'screen' | 'media'\n\n/**\n * A `restrictChildren` / `restrictParent` directive as generated data: the\n * same tuple the schema declares, with the flag spelled as the string the\n * enum holds so a JSON emitter can write it.\n */\nexport type AiLinealOrder = JsonLinealOrder\n\nexport interface AiPaletteEntry {\n pluginId: string\n kind: 'element' | 'plaintext' | 'markdown'\n category: string\n displayName: string\n /** One sentence, from the schema's own description. */\n summary: string\n /** Whether the editor lets anything be dropped inside this element. */\n acceptsChildren: boolean\n restrictChildren?: AiLinealOrder\n restrictParent?: AiLinealOrder\n propsSchema: AiPropsSchema\n /** Roles of the string props the validator treats specially. */\n propRoles: Record<string, AiPropRole>\n /**\n * The attribute field kind each declared prop is edited with, as its\n * `FieldComponentType` value (`switch`, `select`, `screen-select`, …): what\n * the editor reads to decide which component properties a field can be\n * bound to (AGL-2908).\n */\n propFields: Record<string, string>\n /** Length ceilings for `text` props, in characters. */\n textLimits: Record<string, number>\n /** Names of the presets that place this component. */\n presets: readonly string[]\n}\n\n/**\n * What every NODE can do, as against what one component declares (AGL-3156).\n *\n * Generated from the node capabilities core declares, beside the per-component\n * schemas rather than out of them: repeating is not a Stack's feature, so no\n * component schema names it and a palette built from schemas alone advertises\n * it on nothing. Declared ONCE for the whole palette — a model may write these\n * props on any element the surface allows.\n */\nexport interface AiNodeCapability {\n /** Stable id, as core declares it. */\n id: string\n displayName: string\n /** One sentence, for the prompt catalog. */\n summary: string\n propsSchema: AiPropsSchema\n propRoles: Record<string, AiPropRole>\n propFields: Record<string, string>\n textLimits: Record<string, number>\n /** Props offered only on a node that holds a child list. */\n childrenOnlyProps: readonly string[]\n /**\n * Declared attributes the palette deliberately does not offer: a picker\n * that chooses a record the site holds, which a model cannot name from a\n * description. Recorded so a field left off on purpose reads differently\n * from one that went missing.\n */\n omittedProps: readonly string[]\n}\n\n/**\n * The capability props a node with this child contract may carry, merged into\n * one map the prop sanitizer can read beside the element's own declarations.\n */\nexport function nodeCapabilityProps(\n capabilities: Readonly<Record<string, AiNodeCapability>>,\n acceptsChildren: boolean,\n): {\n properties: Record<string, AiPropSchema>\n propRoles: Record<string, AiPropRole>\n textLimits: Record<string, number>\n} {\n const properties: Record<string, AiPropSchema> = {}\n const propRoles: Record<string, AiPropRole> = {}\n const textLimits: Record<string, number> = {}\n for (const capability of Object.values(capabilities)) {\n for (const [name, schema] of Object.entries(\n capability.propsSchema.properties,\n )) {\n if (!acceptsChildren && capability.childrenOnlyProps.includes(name))\n continue\n properties[name] = schema\n const role = capability.propRoles[name]\n if (role) propRoles[name] = role\n const limit = capability.textLimits[name]\n if (limit !== undefined) textLimits[name] = limit\n }\n }\n return { properties, propRoles, textLimits }\n}\n\n/**\n * What a node capability declared in core and one carried by the palette\n * disagree about (AGL-3156).\n *\n * `check:ai-palette` pins the generated file to the generator's output for its\n * inputs, which cannot notice a capability leaving those inputs: when repeat\n * stopped being a component's attribute, the generator regenerated cleanly and\n * the palette simply stopped mentioning it. This reads the core declarations\n * instead and asks whether the palette still accounts for each one, so the\n * next generic capability cannot vanish the same way.\n *\n * `offeredFieldKinds` is every field kind the palette DOES carry somewhere,\n * derived from the generated entries rather than restated here: a capability\n * attribute drawn with one of those kinds has no excuse for being omitted.\n */\nexport function auditNodeCapabilities(\n declared: ReadonlyArray<{\n id: string\n label: string\n attributes: ReadonlyArray<{ name: string; component: string }>\n }>,\n carried: Readonly<Record<string, AiNodeCapability>>,\n offeredFieldKinds: ReadonlySet<string>,\n): string[] {\n const problems: string[] = []\n for (const capability of declared) {\n const entry = carried[capability.id]\n if (!entry) {\n problems.push(\n `the palette carries no node capability \"${capability.id}\": a model cannot use it at all`,\n )\n continue\n }\n if (!Object.keys(entry.propsSchema.properties).length) {\n problems.push(\n `node capability \"${capability.id}\" is carried with no prop a model can write`,\n )\n }\n for (const attribute of capability.attributes) {\n if (entry.propsSchema.properties[attribute.name]) continue\n if (!entry.omittedProps.includes(attribute.name)) {\n problems.push(\n `node capability \"${capability.id}\" declares \"${attribute.name}\", which the palette neither offers nor records as omitted`,\n )\n continue\n }\n if (offeredFieldKinds.has(attribute.component)) {\n problems.push(\n `node capability \"${capability.id}\" omits \"${attribute.name}\", whose \"${attribute.component}\" field the palette offers elsewhere`,\n )\n }\n }\n }\n for (const id of Object.keys(carried)) {\n if (!declared.some((capability) => capability.id === id)) {\n problems.push(\n `the palette carries node capability \"${id}\", which core no longer declares`,\n )\n }\n }\n return problems\n}\n\nexport interface AiSurfaceDefinition {\n /** The component id at the top of a tree for this surface. */\n root: string\n /** Every component id a node in this surface may carry. */\n allow: readonly string[]\n /**\n * Component ids admitted only in a tree the platform's own code composed\n * (AGL-3660), never listed in the surface's catalog: see\n * `AiNodeTreeContext.codeBuilt`.\n */\n codeOnly?: readonly string[]\n}\n\n/**\n * Theme vocabulary an `sx` value may name, read off the theme sources so the\n * validator's allowlist is derived rather than typed by hand.\n */\nexport interface AiSxTokens {\n /** Dotted palette paths such as `primary.main`. */\n palette: readonly string[]\n /** Responsive object keys. */\n breakpoints: readonly string[]\n /** Values `typography` may take. */\n typographyVariants: readonly string[]\n}\n\n/**\n * Text ceilings per role of copy (AGL-2905). A headline is one line of a\n * hero, body copy is a paragraph or two, and a button label has to fit a\n * button.\n */\nexport const AI_TEXT_LIMITS = {\n headline: 120,\n body: 2_000,\n button: 40,\n /** A short label, title or caption. */\n label: 200,\n} as const\n\n/**\n * Prompt budget per surface catalog, in characters at four per token.\n * Asserted by a spec so a component that grows its description cannot\n * silently push a system prompt past what the route pays for.\n */\nexport const AI_PALETTE_CATALOG_MAX_TOKENS = 2_500\nexport const AI_PALETTE_CATALOG_MAX_CHARS = AI_PALETTE_CATALOG_MAX_TOKENS * 4\n\n/** The model's own token estimate: characters over four. */\nexport function estimateCatalogTokens(catalog: string): number {\n return Math.ceil(catalog.length / 4)\n}\n\n/**\n * The documents a generator emits (AGL-2935). Each is composed on one\n * surface of the palette and held to its own budget below.\n */\nexport type AiOutputKind =\n | 'page'\n | 'template'\n | 'component'\n | 'layout'\n | 'form'\n | 'email'\n\nexport const AI_OUTPUT_KINDS: readonly AiOutputKind[] = [\n 'page',\n 'template',\n 'component',\n 'layout',\n 'form',\n 'email',\n]\n\n/** The surface each output kind is composed on. */\nexport const AI_OUTPUT_SURFACE: Record<AiOutputKind, AiSurface> = {\n page: 'screen',\n template: 'screen',\n component: 'component',\n layout: 'layout',\n form: 'form',\n email: 'email',\n}\n\n/**\n * What the rule-17 scorer measures on an emitted tree:\n *\n * - `nodes` — how many elements the tree carries;\n * - `bytes` — the stored node map, in the unit the save ceiling uses\n * (`nodeMapBytes`, msgpack);\n * - `imageBytes` — what the placed library images request at the slot's\n * variant, from each asset's recorded size;\n * - `embeds` — third-party players and frames the output loads;\n * - `fontFamilies` — families named beyond the theme's own;\n * - `emailHtmlBytes` — an email's rendered HTML, which is what a mail\n * client clips.\n */\nexport type AiBudgetMetric =\n | 'nodes'\n | 'bytes'\n | 'imageBytes'\n | 'embeds'\n | 'fontFamilies'\n | 'emailHtmlBytes'\n\nexport type AiOutputBudget = Record<Exclude<AiBudgetMetric, 'emailHtmlBytes'>, number> & {\n emailHtmlBytes?: number\n}\n\n/**\n * The size past which Gmail clips a message and hides the rest behind a\n * \"View entire message\" link — the unsubscribe footer included.\n */\nexport const AI_EMAIL_CLIP_BYTES = 102_000\n\n/**\n * The budget per output kind (AGL-2935, rule 17), beside the catalog budget\n * above for the same reason: a generator that grows its output past what\n * its kind is for is caught by a number, not noticed by a reader.\n *\n * A page is a few hundred elements and tens of kilobytes of stored map; a\n * reusable component is one repeated block; a layout is the chrome every\n * page loads, so it carries the least imagery; a form is fields and a\n * button; an email is held to the clipping threshold on its rendered HTML.\n * No kind names a font beyond the theme's.\n *\n * A third-party player is a page's or a component's to carry, and only where\n * a confirmed plan lists it (rule 16). A template is rendered for every\n * record it serves and a layout around every page, so neither carries one\n * (AGL-3433): an embed there loads a stranger's code on every page of a\n * collection or a site.\n */\nexport const AI_OUTPUT_BUDGETS: Record<AiOutputKind, AiOutputBudget> = {\n page: {\n nodes: 400,\n bytes: 60_000,\n imageBytes: 1_500_000,\n embeds: 1,\n fontFamilies: 0,\n },\n template: {\n nodes: 400,\n bytes: 60_000,\n imageBytes: 1_500_000,\n embeds: 0,\n fontFamilies: 0,\n },\n component: {\n nodes: 80,\n bytes: 12_000,\n imageBytes: 500_000,\n embeds: 1,\n fontFamilies: 0,\n },\n layout: {\n nodes: 160,\n bytes: 24_000,\n imageBytes: 200_000,\n embeds: 0,\n fontFamilies: 0,\n },\n form: {\n nodes: 60,\n bytes: 8_000,\n imageBytes: 0,\n embeds: 0,\n fontFamilies: 0,\n },\n email: {\n nodes: 150,\n bytes: 24_000,\n imageBytes: 1_000_000,\n embeds: 0,\n fontFamilies: 0,\n emailHtmlBytes: AI_EMAIL_CLIP_BYTES,\n },\n}\n\n/**\n * What a first visit to a generated output is estimated to transfer — the\n * weight the proposal shows before anything is applied (rule 17). A page\n * starts from the platform's measured page weight; a component or a layout\n * reports only what it adds to a page; an email is not a page load.\n */\nexport interface AiLoadEstimate {\n /** The measured weight of a published page before this output's own bytes; 0 for a fragment. */\n pageBytes: number\n /** The stored node map, in the save's own bytes. */\n documentBytes: number\n /** What the placed library images request at their slots. */\n imageBytes: number\n /** Images the estimate could not size: no recorded size, or not from the library. */\n imagesUnmeasured: number\n embeds: number\n totalBytes: number\n}\n\n/** Typography variants that read as headlines, for the `children` ceiling. */\nexport function isHeadlineVariant(variant: unknown): boolean {\n return typeof variant === 'string' && /^(display\\w*|h[1-6]|heading)$/.test(variant)\n}\n"],"names":["AI_SURFACE_NAMES","nodeCapabilityProps","capabilities","acceptsChildren","properties","propRoles","textLimits","capability","Object","values","name","schema","entries","propsSchema","childrenOnlyProps","includes","role","limit","undefined","auditNodeCapabilities","declared","carried","offeredFieldKinds","problems","entry","id","push","keys","length","attribute","attributes","omittedProps","has","component","some","AI_TEXT_LIMITS","headline","body","button","label","AI_PALETTE_CATALOG_MAX_TOKENS","AI_PALETTE_CATALOG_MAX_CHARS","estimateCatalogTokens","catalog","Math","ceil","AI_OUTPUT_KINDS","AI_OUTPUT_SURFACE","page","template","layout","form","email","AI_EMAIL_CLIP_BYTES","AI_OUTPUT_BUDGETS","nodes","bytes","imageBytes","embeds","fontFamilies","emailHtmlBytes","isHeadlineVariant","variant","test"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAgBD,OAAO,MAAMA,mBAAyC;IACpD;IACA;IACA;IACA;IACA;CACD,CAAA;AAmGD;;;CAGC,GACD,OAAO,SAASC,oBACdC,YAAwD,EACxDC,eAAwB;IAMxB,MAAMC,aAA2C,CAAC;IAClD,MAAMC,YAAwC,CAAC;IAC/C,MAAMC,aAAqC,CAAC;IAC5C,KAAK,MAAMC,cAAcC,OAAOC,MAAM,CAACP,cAAe;QACpD,KAAK,MAAM,CAACQ,MAAMC,OAAO,IAAIH,OAAOI,OAAO,CACzCL,WAAWM,WAAW,CAACT,UAAU,EAChC;YACD,IAAI,CAACD,mBAAmBI,WAAWO,iBAAiB,CAACC,QAAQ,CAACL,OAC5D;YACFN,UAAU,CAACM,KAAK,GAAGC;YACnB,MAAMK,OAAOT,WAAWF,SAAS,CAACK,KAAK;YACvC,IAAIM,MAAMX,SAAS,CAACK,KAAK,GAAGM;YAC5B,MAAMC,QAAQV,WAAWD,UAAU,CAACI,KAAK;YACzC,IAAIO,UAAUC,WAAWZ,UAAU,CAACI,KAAK,GAAGO;QAC9C;IACF;IACA,OAAO;QAAEb;QAAYC;QAAWC;IAAW;AAC7C;AAEA;;;;;;;;;;;;;;CAcC,GACD,OAAO,SAASa,sBACdC,QAIE,EACFC,OAAmD,EACnDC,iBAAsC;IAEtC,MAAMC,WAAqB,EAAE;IAC7B,KAAK,MAAMhB,cAAca,SAAU;QACjC,MAAMI,QAAQH,OAAO,CAACd,WAAWkB,EAAE,CAAC;QACpC,IAAI,CAACD,OAAO;YACVD,SAASG,IAAI,CACX,CAAC,wCAAwC,EAAEnB,WAAWkB,EAAE,CAAC,+BAA+B,CAAC;YAE3F;QACF;QACA,IAAI,CAACjB,OAAOmB,IAAI,CAACH,MAAMX,WAAW,CAACT,UAAU,EAAEwB,MAAM,EAAE;YACrDL,SAASG,IAAI,CACX,CAAC,iBAAiB,EAAEnB,WAAWkB,EAAE,CAAC,2CAA2C,CAAC;QAElF;QACA,KAAK,MAAMI,aAAatB,WAAWuB,UAAU,CAAE;YAC7C,IAAIN,MAAMX,WAAW,CAACT,UAAU,CAACyB,UAAUnB,IAAI,CAAC,EAAE;YAClD,IAAI,CAACc,MAAMO,YAAY,CAAChB,QAAQ,CAACc,UAAUnB,IAAI,GAAG;gBAChDa,SAASG,IAAI,CACX,CAAC,iBAAiB,EAAEnB,WAAWkB,EAAE,CAAC,YAAY,EAAEI,UAAUnB,IAAI,CAAC,0DAA0D,CAAC;gBAE5H;YACF;YACA,IAAIY,kBAAkBU,GAAG,CAACH,UAAUI,SAAS,GAAG;gBAC9CV,SAASG,IAAI,CACX,CAAC,iBAAiB,EAAEnB,WAAWkB,EAAE,CAAC,SAAS,EAAEI,UAAUnB,IAAI,CAAC,UAAU,EAAEmB,UAAUI,SAAS,CAAC,oCAAoC,CAAC;YAErI;QACF;IACF;IACA,KAAK,MAAMR,MAAMjB,OAAOmB,IAAI,CAACN,SAAU;QACrC,IAAI,CAACD,SAASc,IAAI,CAAC,CAAC3B,aAAeA,WAAWkB,EAAE,KAAKA,KAAK;YACxDF,SAASG,IAAI,CACX,CAAC,qCAAqC,EAAED,GAAG,gCAAgC,CAAC;QAEhF;IACF;IACA,OAAOF;AACT;AA4BA;;;;CAIC,GACD,OAAO,MAAMY,iBAAiB;IAC5BC,UAAU;IACVC,MAAM;IACNC,QAAQ;IACR,qCAAqC,GACrCC,OAAO;AACT,EAAU;AAEV;;;;CAIC,GACD,OAAO,MAAMC,gCAAgC,KAAK;AAClD,OAAO,MAAMC,+BAA+BD,gCAAgC,EAAC;AAE7E,0DAA0D,GAC1D,OAAO,SAASE,sBAAsBC,OAAe;IACnD,OAAOC,KAAKC,IAAI,CAACF,QAAQf,MAAM,GAAG;AACpC;AAcA,OAAO,MAAMkB,kBAA2C;IACtD;IACA;IACA;IACA;IACA;IACA;CACD,CAAA;AAED,iDAAiD,GACjD,OAAO,MAAMC,oBAAqD;IAChEC,MAAM;IACNC,UAAU;IACVhB,WAAW;IACXiB,QAAQ;IACRC,MAAM;IACNC,OAAO;AACT,EAAC;AA2BD;;;CAGC,GACD,OAAO,MAAMC,sBAAsB,OAAO;AAE1C;;;;;;;;;;;;;;;;CAgBC,GACD,OAAO,MAAMC,oBAA0D;IACrEN,MAAM;QACJO,OAAO;QACPC,OAAO;QACPC,YAAY;QACZC,QAAQ;QACRC,cAAc;IAChB;IACAV,UAAU;QACRM,OAAO;QACPC,OAAO;QACPC,YAAY;QACZC,QAAQ;QACRC,cAAc;IAChB;IACA1B,WAAW;QACTsB,OAAO;QACPC,OAAO;QACPC,YAAY;QACZC,QAAQ;QACRC,cAAc;IAChB;IACAT,QAAQ;QACNK,OAAO;QACPC,OAAO;QACPC,YAAY;QACZC,QAAQ;QACRC,cAAc;IAChB;IACAR,MAAM;QACJI,OAAO;QACPC,OAAO;QACPC,YAAY;QACZC,QAAQ;QACRC,cAAc;IAChB;IACAP,OAAO;QACLG,OAAO;QACPC,OAAO;QACPC,YAAY;QACZC,QAAQ;QACRC,cAAc;QACdC,gBAAgBP;IAClB;AACF,EAAC;AAqBD,4EAA4E,GAC5E,OAAO,SAASQ,kBAAkBC,OAAgB;IAChD,OAAO,OAAOA,YAAY,YAAY,gCAAgCC,IAAI,CAACD;AAC7E"}
|
|
@@ -0,0 +1,34 @@
|
|
|
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
|
+
type StoredNode = Record<string, unknown> & {
|
|
18
|
+
props?: Record<string, unknown>;
|
|
19
|
+
nodes?: unknown;
|
|
20
|
+
};
|
|
21
|
+
export interface AiExperimentVariantCopy {
|
|
22
|
+
headline: string;
|
|
23
|
+
body: string;
|
|
24
|
+
}
|
|
25
|
+
export interface AiExperimentVariantCopyResult {
|
|
26
|
+
nodes: Record<string, StoredNode>;
|
|
27
|
+
/** Which of the two the copy changed. */
|
|
28
|
+
changed: Array<'headline' | 'body'>;
|
|
29
|
+
/** Why nothing changed, when nothing did; customer-safe. */
|
|
30
|
+
reason: string | null;
|
|
31
|
+
}
|
|
32
|
+
/** Put one variant's copy into a copy of `nodes`, inside the region rooted at `regionRootId`. */
|
|
33
|
+
export declare function applyAiExperimentVariantCopy(nodes: Record<string, unknown>, copy: AiExperimentVariantCopy, regionRootId: string | null): AiExperimentVariantCopyResult;
|
|
34
|
+
export {};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
+
/**
|
|
3
|
+
* @license
|
|
4
|
+
* Copyright 2026 Aglyn LLC
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
* you may not use this file except in compliance with the License.
|
|
8
|
+
* You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
* See the License for the specific language governing permissions and
|
|
16
|
+
* limitations under the License.
|
|
17
|
+
*/ import { AI_TEXT_LIMITS } from "./ai-palette.js";
|
|
18
|
+
/**
|
|
19
|
+
* One A/B variant's copy, put into a COPY of a page's node map (AGL-3603),
|
|
20
|
+
* which the caller stores as a new unpublished version for the variant to
|
|
21
|
+
* pin. Nothing here reads or writes a document, and the map it is handed is
|
|
22
|
+
* never changed.
|
|
23
|
+
*
|
|
24
|
+
* The copy lands in the region the test varies: the whole page's content for
|
|
25
|
+
* a page test, the one section's subtree for a section test. Within it the
|
|
26
|
+
* first heading takes the variant's headline and the first plain text after
|
|
27
|
+
* it takes the body — the same two places the copy under test was read from
|
|
28
|
+
* when a person pasted "the headline and copy as they stand". Rich text is
|
|
29
|
+
* never rewritten, structure is never changed, and a region with no plain
|
|
30
|
+
* heading or text to take the copy says so rather than guessing.
|
|
31
|
+
*/ /** The text elements a variant's copy may be written into, as the SEO fixes read them. */ const TEXT_COMPONENTS = new Set([
|
|
32
|
+
'muiTypography',
|
|
33
|
+
'muiInlineText',
|
|
34
|
+
'muiListItemText'
|
|
35
|
+
]);
|
|
36
|
+
const HEADING = /^h[1-6]$/;
|
|
37
|
+
const isRecord = (value)=>typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
38
|
+
/** A text element's own plain text, or `null` for one a variant may not rewrite. */ function plainText(node) {
|
|
39
|
+
if (!node || !TEXT_COMPONENTS.has(String(node['componentId']))) return null;
|
|
40
|
+
const props = isRecord(node.props) ? node.props : {};
|
|
41
|
+
if (typeof props['html'] === 'string' && props['html'].trim()) return null;
|
|
42
|
+
return typeof props['children'] === 'string' ? props['children'] : null;
|
|
43
|
+
}
|
|
44
|
+
function isHeading(node) {
|
|
45
|
+
var _props_component, _props_variant;
|
|
46
|
+
const props = isRecord(node.props) ? node.props : {};
|
|
47
|
+
return HEADING.test(String((_props_component = props['component']) != null ? _props_component : '')) || HEADING.test(String((_props_variant = props['variant']) != null ? _props_variant : ''));
|
|
48
|
+
}
|
|
49
|
+
/** The region's elements in document order, the region's root first. */ function walk(nodes, rootId) {
|
|
50
|
+
const order = [];
|
|
51
|
+
const seen = new Set();
|
|
52
|
+
const visit = (id)=>{
|
|
53
|
+
if (seen.has(id) || !nodes[id]) return;
|
|
54
|
+
seen.add(id);
|
|
55
|
+
order.push(id);
|
|
56
|
+
const children = Array.isArray(nodes[id].nodes) ? nodes[id].nodes : [];
|
|
57
|
+
for (const child of children)if (typeof child === 'string') visit(child);
|
|
58
|
+
};
|
|
59
|
+
visit(rootId);
|
|
60
|
+
return order;
|
|
61
|
+
}
|
|
62
|
+
const line = (value, limit)=>value.replace(/\s+/g, ' ').trim().slice(0, limit).trim();
|
|
63
|
+
/** Put one variant's copy into a copy of `nodes`, inside the region rooted at `regionRootId`. */ export function applyAiExperimentVariantCopy(nodes, copy, regionRootId) {
|
|
64
|
+
const result = JSON.parse(JSON.stringify(nodes));
|
|
65
|
+
if (!regionRootId || !result[regionRootId]) {
|
|
66
|
+
return {
|
|
67
|
+
nodes: result,
|
|
68
|
+
changed: [],
|
|
69
|
+
reason: 'The part of the page under test is no longer there.'
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
const order = walk(result, regionRootId);
|
|
73
|
+
const headline = line(copy.headline, AI_TEXT_LIMITS.headline);
|
|
74
|
+
const body = copy.body.replace(/\r\n?/g, '\n').trim().slice(0, AI_TEXT_LIMITS.body).trim();
|
|
75
|
+
const changed = [];
|
|
76
|
+
const headingAt = order.findIndex((id)=>plainText(result[id]) !== null && isHeading(result[id]));
|
|
77
|
+
if (headline && headingAt >= 0) {
|
|
78
|
+
var _node_props;
|
|
79
|
+
const node = result[order[headingAt]];
|
|
80
|
+
node.props = _extends({}, (_node_props = node.props) != null ? _node_props : {}, {
|
|
81
|
+
children: headline
|
|
82
|
+
});
|
|
83
|
+
changed.push('headline');
|
|
84
|
+
}
|
|
85
|
+
// The body is the first plain text after the heading, or in the region when it has none.
|
|
86
|
+
const bodyAt = order.findIndex((id, index)=>index > headingAt && plainText(result[id]) !== null && !isHeading(result[id]));
|
|
87
|
+
if (body && bodyAt >= 0) {
|
|
88
|
+
var _node_props1;
|
|
89
|
+
const node = result[order[bodyAt]];
|
|
90
|
+
node.props = _extends({}, (_node_props1 = node.props) != null ? _node_props1 : {}, {
|
|
91
|
+
children: body
|
|
92
|
+
});
|
|
93
|
+
changed.push('body');
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
nodes: result,
|
|
97
|
+
changed,
|
|
98
|
+
reason: changed.length ? null : 'This part of the page has no plain heading or text for the variant’s copy.'
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
//# sourceMappingURL=experiment-variant-copy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/runtime/experiment-variant-copy.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 { AI_TEXT_LIMITS } from './ai-palette'\n\n/**\n * One A/B variant's copy, put into a COPY of a page's node map (AGL-3603),\n * which the caller stores as a new unpublished version for the variant to\n * pin. Nothing here reads or writes a document, and the map it is handed is\n * never changed.\n *\n * The copy lands in the region the test varies: the whole page's content for\n * a page test, the one section's subtree for a section test. Within it the\n * first heading takes the variant's headline and the first plain text after\n * it takes the body — the same two places the copy under test was read from\n * when a person pasted \"the headline and copy as they stand\". Rich text is\n * never rewritten, structure is never changed, and a region with no plain\n * heading or text to take the copy says so rather than guessing.\n */\n\n/** The text elements a variant's copy may be written into, as the SEO fixes read them. */\nconst TEXT_COMPONENTS: ReadonlySet<string> = new Set(['muiTypography', 'muiInlineText', 'muiListItemText'])\n\nconst HEADING = /^h[1-6]$/\n\ntype StoredNode = Record<string, unknown> & { props?: Record<string, unknown>; nodes?: unknown }\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value)\n\nexport interface AiExperimentVariantCopy {\n headline: string\n body: string\n}\n\nexport interface AiExperimentVariantCopyResult {\n nodes: Record<string, StoredNode>\n /** Which of the two the copy changed. */\n changed: Array<'headline' | 'body'>\n /** Why nothing changed, when nothing did; customer-safe. */\n reason: string | null\n}\n\n/** A text element's own plain text, or `null` for one a variant may not rewrite. */\nfunction plainText(node: StoredNode | undefined): string | null {\n if (!node || !TEXT_COMPONENTS.has(String(node['componentId']))) return null\n const props = isRecord(node.props) ? node.props : {}\n if (typeof props['html'] === 'string' && props['html'].trim()) return null\n return typeof props['children'] === 'string' ? props['children'] : null\n}\n\nfunction isHeading(node: StoredNode): boolean {\n const props = isRecord(node.props) ? node.props : {}\n return HEADING.test(String(props['component'] ?? '')) || HEADING.test(String(props['variant'] ?? ''))\n}\n\n/** The region's elements in document order, the region's root first. */\nfunction walk(nodes: Record<string, StoredNode>, rootId: string): string[] {\n const order: string[] = []\n const seen = new Set<string>()\n const visit = (id: string) => {\n if (seen.has(id) || !nodes[id]) return\n seen.add(id)\n order.push(id)\n const children = Array.isArray(nodes[id].nodes) ? (nodes[id].nodes as unknown[]) : []\n for (const child of children) if (typeof child === 'string') visit(child)\n }\n visit(rootId)\n return order\n}\n\nconst line = (value: string, limit: number) => value.replace(/\\s+/g, ' ').trim().slice(0, limit).trim()\n\n/** Put one variant's copy into a copy of `nodes`, inside the region rooted at `regionRootId`. */\nexport function applyAiExperimentVariantCopy(\n nodes: Record<string, unknown>,\n copy: AiExperimentVariantCopy,\n regionRootId: string | null,\n): AiExperimentVariantCopyResult {\n const result = JSON.parse(JSON.stringify(nodes)) as Record<string, StoredNode>\n if (!regionRootId || !result[regionRootId]) {\n return { nodes: result, changed: [], reason: 'The part of the page under test is no longer there.' }\n }\n const order = walk(result, regionRootId)\n const headline = line(copy.headline, AI_TEXT_LIMITS.headline)\n const body = copy.body.replace(/\\r\\n?/g, '\\n').trim().slice(0, AI_TEXT_LIMITS.body).trim()\n const changed: AiExperimentVariantCopyResult['changed'] = []\n\n const headingAt = order.findIndex((id) => plainText(result[id]) !== null && isHeading(result[id]))\n if (headline && headingAt >= 0) {\n const node = result[order[headingAt]]\n node.props = { ...(node.props ?? {}), children: headline }\n changed.push('headline')\n }\n // The body is the first plain text after the heading, or in the region when it has none.\n const bodyAt = order.findIndex(\n (id, index) => index > headingAt && plainText(result[id]) !== null && !isHeading(result[id]),\n )\n if (body && bodyAt >= 0) {\n const node = result[order[bodyAt]]\n node.props = { ...(node.props ?? {}), children: body }\n changed.push('body')\n }\n return {\n nodes: result,\n changed,\n reason: changed.length ? null : 'This part of the page has no plain heading or text for the variant’s copy.',\n }\n}\n"],"names":["AI_TEXT_LIMITS","TEXT_COMPONENTS","Set","HEADING","isRecord","value","Array","isArray","plainText","node","has","String","props","trim","isHeading","test","walk","nodes","rootId","order","seen","visit","id","add","push","children","child","line","limit","replace","slice","applyAiExperimentVariantCopy","copy","regionRootId","result","JSON","parse","stringify","changed","reason","headline","body","headingAt","findIndex","bodyAt","index","length"],"mappings":"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"}
|
|
@@ -0,0 +1,51 @@
|
|
|
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
|
+
import type { AiJob } from '../model/ai-jobs.types';
|
|
18
|
+
import { type AiExperimentVariantsProposalView } from '../model/ai-experiment-proposal';
|
|
19
|
+
/** The id a job's variant version is stored under. */
|
|
20
|
+
export declare function aiExperimentVersionId(jobId: string, index: number): string;
|
|
21
|
+
/** What a version made from a variant is named in the versions list. */
|
|
22
|
+
export declare function aiExperimentVersionName(name: string, index: number): string;
|
|
23
|
+
export interface AiExperimentVersionsInput {
|
|
24
|
+
hostId: string;
|
|
25
|
+
screenId: string;
|
|
26
|
+
/** A section test's element; `null` for a page test. */
|
|
27
|
+
nodeId: string | null;
|
|
28
|
+
job: Pick<AiJob, '$id'>;
|
|
29
|
+
proposal: AiExperimentVariantsProposalView;
|
|
30
|
+
uid: string;
|
|
31
|
+
now?: Date;
|
|
32
|
+
}
|
|
33
|
+
export interface AiExperimentVersionsResult {
|
|
34
|
+
/** One per variant past the control, in order. */
|
|
35
|
+
versions: Array<{
|
|
36
|
+
index: number;
|
|
37
|
+
name: string;
|
|
38
|
+
versionId: string;
|
|
39
|
+
changed: string[];
|
|
40
|
+
}>;
|
|
41
|
+
skipped: Array<{
|
|
42
|
+
index: number;
|
|
43
|
+
reason: string;
|
|
44
|
+
}>;
|
|
45
|
+
/** Why no version could be made at all, customer-safe; `null` when one could. */
|
|
46
|
+
refusal: string | null;
|
|
47
|
+
}
|
|
48
|
+
/** Make the versions. Exported for the spec, which drives it against a Firestore double. */
|
|
49
|
+
export declare function createAiExperimentVersions(firestore: FirebaseFirestore.Firestore, input: AiExperimentVersionsInput): Promise<AiExperimentVersionsResult>;
|
|
50
|
+
export declare function POST(request: Request): Promise<Response>;
|
|
51
|
+
export declare const dynamic = "force-dynamic";
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
+
/**
|
|
3
|
+
* @license
|
|
4
|
+
* Copyright 2026 Aglyn LLC
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
* you may not use this file except in compliance with the License.
|
|
8
|
+
* You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
* See the License for the specific language governing permissions and
|
|
16
|
+
* limitations under the License.
|
|
17
|
+
*/ // lockdown-423: via libs/plugins/ai/src/lib/server/ai-jobs-gate.ts
|
|
18
|
+
// The gate carries the org verdict; the handler adds the site's own below,
|
|
19
|
+
// with the host document in hand.
|
|
20
|
+
import { checkEntitlement, encodeStoredNodes, hostRoleCanWrite } from "@aglyn/aglyn/server";
|
|
21
|
+
import { pageContentRootId } from "@aglyn/aglyn/app-utils/page-markdown";
|
|
22
|
+
import { decodeStoredNodes } from "@aglyn/aglyn/app-utils/stored-nodes";
|
|
23
|
+
import { lockdownRefusal } from "@aglyn/tenant-data-admin/server/lockdown";
|
|
24
|
+
import { getAiJob, writeAiJobAudit } from "../jobs/ai-jobs.js";
|
|
25
|
+
import { readAiExperimentVariants } from "../model/ai-experiment-proposal.js";
|
|
26
|
+
import { applyAiExperimentVariantCopy } from "../runtime/experiment-variant-copy.js";
|
|
27
|
+
import { AI_SEO_APPLY_VERSIONING_COPY } from "./ai-seo-apply.js";
|
|
28
|
+
import { aiJobsGate } from "./ai-jobs-gate.js";
|
|
29
|
+
/**
|
|
30
|
+
* A page's or a section's A/B variants as draft versions (AGL-3603):
|
|
31
|
+
* `POST /api/ai/experiments/versions { orgId, hostId, jobId, screenId, nodeId? }`.
|
|
32
|
+
*
|
|
33
|
+
* An `experiment` job proposes the variants' copy. For an email that copy IS
|
|
34
|
+
* the variant, and the editor holds it. For a page or a section the variant is
|
|
35
|
+
* a version of the page, which the editor only pins — so this door makes one
|
|
36
|
+
* per proposed variant past the first: the page's PUBLISHED version is
|
|
37
|
+
* copied, the variant's headline and body are put into the region the test
|
|
38
|
+
* varies (`runtime/experiment-variant-copy.ts`), and the copy is stored as a
|
|
39
|
+
* new unpublished version named for the variant.
|
|
40
|
+
*
|
|
41
|
+
* ## Nothing goes live
|
|
42
|
+
*
|
|
43
|
+
* The published version, the screen's `versionId` and the experiment document
|
|
44
|
+
* are never touched. The A/B testing editor pins each new version to its
|
|
45
|
+
* variant as an unsaved change; the person reviews the versions, saves the
|
|
46
|
+
* test and starts it. The first proposed variant is the copy as it stands, so
|
|
47
|
+
* it is the control and pins the published version, as the editor's default
|
|
48
|
+
* already does.
|
|
49
|
+
*
|
|
50
|
+
* ## The rungs
|
|
51
|
+
*
|
|
52
|
+
* The jobs read gate (membership, the release flag, `aiGenerative`, the org
|
|
53
|
+
* lockdown); then the site — the job's and the org's — a role that writes the
|
|
54
|
+
* site's content, the site's own lockdown verdict, and the `versioning`
|
|
55
|
+
* entitlement, because a page that already has a version gains retained
|
|
56
|
+
* history here, which is what that entitlement sells. Nothing spends a token.
|
|
57
|
+
*
|
|
58
|
+
* Idempotent: each version's id is derived from the job and the variant, so
|
|
59
|
+
* a second press finds the versions the first made.
|
|
60
|
+
*/ const ID_CHARS = /^[A-Za-z0-9_-]{1,100}$/;
|
|
61
|
+
/** The id a job's variant version is stored under. */ export function aiExperimentVersionId(jobId, index) {
|
|
62
|
+
return `ab-${jobId}-${index}`;
|
|
63
|
+
}
|
|
64
|
+
/** What a version made from a variant is named in the versions list. */ export function aiExperimentVersionName(name, index) {
|
|
65
|
+
const label = name.replace(/\s+/g, ' ').trim() || `Variant ${index + 1}`;
|
|
66
|
+
return `A/B: ${label}`.slice(0, 80);
|
|
67
|
+
}
|
|
68
|
+
const str = (value)=>typeof value === 'string' ? value.trim() : '';
|
|
69
|
+
/** Make the versions. Exported for the spec, which drives it against a Firestore double. */ export async function createAiExperimentVersions(firestore, input) {
|
|
70
|
+
var _input_now, _input_nodeId;
|
|
71
|
+
const now = (_input_now = input.now) != null ? _input_now : new Date();
|
|
72
|
+
const screenRef = firestore.collection('hosts').doc(input.hostId).collection('screens').doc(input.screenId);
|
|
73
|
+
const screen = await screenRef.get();
|
|
74
|
+
const empty = (refusal)=>({
|
|
75
|
+
versions: [],
|
|
76
|
+
skipped: [],
|
|
77
|
+
refusal
|
|
78
|
+
});
|
|
79
|
+
if (!screen.exists || screen.get('deletedAt')) return empty('The page under test no longer exists.');
|
|
80
|
+
const publishedId = str(screen.get('versionId'));
|
|
81
|
+
const source = publishedId ? await screenRef.collection('versions').doc(publishedId).get() : null;
|
|
82
|
+
const sourceData = (source == null ? void 0 : source.exists) ? source.data() : null;
|
|
83
|
+
const nodes = sourceData ? decodeStoredNodes(sourceData['nodes']) : null;
|
|
84
|
+
if (!sourceData || !nodes) return empty('The page has no published version to start from.');
|
|
85
|
+
const rootId = typeof sourceData['rootId'] === 'string' ? sourceData['rootId'] : undefined;
|
|
86
|
+
const regionRootId = (_input_nodeId = input.nodeId) != null ? _input_nodeId : pageContentRootId(nodes, rootId);
|
|
87
|
+
const versions = [];
|
|
88
|
+
const skipped = [];
|
|
89
|
+
for (const [index, variant] of input.proposal.variants.entries()){
|
|
90
|
+
// The first variant is the copy as it stands: the control, on the published version.
|
|
91
|
+
if (index === 0) continue;
|
|
92
|
+
const versionId = aiExperimentVersionId(input.job.$id, index);
|
|
93
|
+
const name = aiExperimentVersionName(variant.name, index);
|
|
94
|
+
const versionRef = screenRef.collection('versions').doc(versionId);
|
|
95
|
+
const earlier = await versionRef.get();
|
|
96
|
+
if (earlier.exists && !earlier.get('deletedAt')) {
|
|
97
|
+
versions.push({
|
|
98
|
+
index,
|
|
99
|
+
name: str(earlier.get('displayName')) || name,
|
|
100
|
+
versionId,
|
|
101
|
+
changed: []
|
|
102
|
+
});
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const result = applyAiExperimentVariantCopy(nodes, {
|
|
106
|
+
headline: variant.headline,
|
|
107
|
+
body: variant.body
|
|
108
|
+
}, regionRootId);
|
|
109
|
+
if (!result.changed.length) {
|
|
110
|
+
var _result_reason;
|
|
111
|
+
skipped.push({
|
|
112
|
+
index,
|
|
113
|
+
reason: (_result_reason = result.reason) != null ? _result_reason : 'Nothing on the page took the variant’s copy.'
|
|
114
|
+
});
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
// Compressed at rest, as every write of a version's nodes is.
|
|
118
|
+
const packed = encodeStoredNodes(result.nodes);
|
|
119
|
+
await versionRef.set(_extends({}, sourceData, {
|
|
120
|
+
nodes: packed ? Buffer.from(packed) : result.nodes,
|
|
121
|
+
displayName: name,
|
|
122
|
+
createdAt: now,
|
|
123
|
+
updatedAt: now,
|
|
124
|
+
createdBy: input.uid
|
|
125
|
+
}));
|
|
126
|
+
versions.push({
|
|
127
|
+
index,
|
|
128
|
+
name,
|
|
129
|
+
versionId,
|
|
130
|
+
changed: result.changed
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
versions,
|
|
135
|
+
skipped,
|
|
136
|
+
refusal: null
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
export async function POST(request) {
|
|
140
|
+
var _body_orgId, _host_memberRoles, _job_outputs;
|
|
141
|
+
var _find;
|
|
142
|
+
let body;
|
|
143
|
+
try {
|
|
144
|
+
var _ref;
|
|
145
|
+
body = (_ref = await request.json()) != null ? _ref : {};
|
|
146
|
+
} catch (unused) {
|
|
147
|
+
body = {};
|
|
148
|
+
}
|
|
149
|
+
const gate = await aiJobsGate(request, String((_body_orgId = body['orgId']) != null ? _body_orgId : ''));
|
|
150
|
+
if (gate instanceof Response) return gate;
|
|
151
|
+
const hostId = str(body['hostId']);
|
|
152
|
+
const jobId = str(body['jobId']);
|
|
153
|
+
const screenId = str(body['screenId']);
|
|
154
|
+
const nodeId = str(body['nodeId']) || null;
|
|
155
|
+
if (!ID_CHARS.test(hostId) || !ID_CHARS.test(jobId) || !ID_CHARS.test(screenId)) {
|
|
156
|
+
return Response.json({
|
|
157
|
+
error: 'Name the site, the page under test and the variants to make'
|
|
158
|
+
}, {
|
|
159
|
+
status: 400
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
if (nodeId !== null && !ID_CHARS.test(nodeId)) {
|
|
163
|
+
return Response.json({
|
|
164
|
+
error: 'That is not a section of the page'
|
|
165
|
+
}, {
|
|
166
|
+
status: 400
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
const { firestore, orgId, uid, staff, org, decoded } = gate;
|
|
170
|
+
const hostSnapshot = await firestore.collection('hosts').doc(hostId).get();
|
|
171
|
+
const host = hostSnapshot.exists ? hostSnapshot.data() : null;
|
|
172
|
+
if (!host || host['orgId'] !== orgId) {
|
|
173
|
+
return Response.json({
|
|
174
|
+
error: 'Unknown site'
|
|
175
|
+
}, {
|
|
176
|
+
status: 404
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
const role = ((_host_memberRoles = host['memberRoles']) != null ? _host_memberRoles : {})[uid];
|
|
180
|
+
if (!staff && !hostRoleCanWrite(role)) {
|
|
181
|
+
return Response.json({
|
|
182
|
+
error: 'Editing requires the editor role'
|
|
183
|
+
}, {
|
|
184
|
+
status: 403
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
const locked = await lockdownRefusal({
|
|
188
|
+
request,
|
|
189
|
+
staff,
|
|
190
|
+
uid,
|
|
191
|
+
org: org,
|
|
192
|
+
host: host
|
|
193
|
+
});
|
|
194
|
+
if (locked) return locked;
|
|
195
|
+
if (!checkEntitlement(org, 'versioning')) {
|
|
196
|
+
return Response.json({
|
|
197
|
+
error: AI_SEO_APPLY_VERSIONING_COPY
|
|
198
|
+
}, {
|
|
199
|
+
status: 403
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
const job = await getAiJob(firestore, orgId, jobId);
|
|
203
|
+
if (!job || job.kind !== 'experiment' || job.hostId !== hostId) {
|
|
204
|
+
return Response.json({
|
|
205
|
+
error: 'Unknown variants'
|
|
206
|
+
}, {
|
|
207
|
+
status: 404
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
if (job.status !== 'done') {
|
|
211
|
+
return Response.json({
|
|
212
|
+
error: 'The variants are still being written'
|
|
213
|
+
}, {
|
|
214
|
+
status: 409
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
const proposal = readAiExperimentVariants((_find = ((_job_outputs = job.outputs) != null ? _job_outputs : []).find((output)=>output.resource === 'experiment')) == null ? void 0 : _find.proposal);
|
|
218
|
+
if (!proposal || proposal.target === 'email') {
|
|
219
|
+
return Response.json({
|
|
220
|
+
error: 'These variants are not a page’s or a section’s'
|
|
221
|
+
}, {
|
|
222
|
+
status: 409
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
if (proposal.target === 'section' && !nodeId) {
|
|
226
|
+
return Response.json({
|
|
227
|
+
error: 'Pick the section under test first'
|
|
228
|
+
}, {
|
|
229
|
+
status: 400
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
const result = await createAiExperimentVersions(firestore, {
|
|
233
|
+
hostId,
|
|
234
|
+
screenId,
|
|
235
|
+
nodeId: proposal.target === 'section' ? nodeId : null,
|
|
236
|
+
job,
|
|
237
|
+
proposal,
|
|
238
|
+
uid
|
|
239
|
+
});
|
|
240
|
+
if (result.refusal) return Response.json({
|
|
241
|
+
error: result.refusal
|
|
242
|
+
}, {
|
|
243
|
+
status: 409
|
|
244
|
+
});
|
|
245
|
+
const made = result.versions.filter((entry)=>entry.changed.length);
|
|
246
|
+
if (made.length) {
|
|
247
|
+
var _decoded_email;
|
|
248
|
+
await writeAiJobAudit(firestore, {
|
|
249
|
+
action: 'ai.job.apply',
|
|
250
|
+
actorUid: uid,
|
|
251
|
+
actorEmail: (_decoded_email = decoded.email) != null ? _decoded_email : null,
|
|
252
|
+
orgId,
|
|
253
|
+
jobId,
|
|
254
|
+
after: {
|
|
255
|
+
hostId,
|
|
256
|
+
screenId,
|
|
257
|
+
versions: made.map((entry)=>entry.versionId)
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
return Response.json(result, {
|
|
262
|
+
status: 200,
|
|
263
|
+
headers: {
|
|
264
|
+
'Cache-Control': 'no-store'
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
export const dynamic = 'force-dynamic';
|
|
269
|
+
|
|
270
|
+
//# sourceMappingURL=ai-experiment-versions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/server/ai-experiment-versions.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\n// lockdown-423: via libs/plugins/ai/src/lib/server/ai-jobs-gate.ts\n// The gate carries the org verdict; the handler adds the site's own below,\n// with the host document in hand.\n\nimport {\n checkEntitlement,\n encodeStoredNodes,\n hostRoleCanWrite,\n} from '@aglyn/aglyn/server'\nimport { pageContentRootId } from '@aglyn/aglyn/app-utils/page-markdown'\nimport { decodeStoredNodes } from '@aglyn/aglyn/app-utils/stored-nodes'\nimport { lockdownRefusal } from '@aglyn/tenant-data-admin/server/lockdown'\nimport { getAiJob, writeAiJobAudit } from '../jobs/ai-jobs'\nimport type { AiJob } from '../model/ai-jobs.types'\nimport {\n readAiExperimentVariants,\n type AiExperimentVariantsProposalView,\n} from '../model/ai-experiment-proposal'\nimport { applyAiExperimentVariantCopy } from '../runtime/experiment-variant-copy'\nimport { AI_SEO_APPLY_VERSIONING_COPY } from './ai-seo-apply'\nimport { aiJobsGate } from './ai-jobs-gate'\n\n/**\n * A page's or a section's A/B variants as draft versions (AGL-3603):\n * `POST /api/ai/experiments/versions { orgId, hostId, jobId, screenId, nodeId? }`.\n *\n * An `experiment` job proposes the variants' copy. For an email that copy IS\n * the variant, and the editor holds it. For a page or a section the variant is\n * a version of the page, which the editor only pins — so this door makes one\n * per proposed variant past the first: the page's PUBLISHED version is\n * copied, the variant's headline and body are put into the region the test\n * varies (`runtime/experiment-variant-copy.ts`), and the copy is stored as a\n * new unpublished version named for the variant.\n *\n * ## Nothing goes live\n *\n * The published version, the screen's `versionId` and the experiment document\n * are never touched. The A/B testing editor pins each new version to its\n * variant as an unsaved change; the person reviews the versions, saves the\n * test and starts it. The first proposed variant is the copy as it stands, so\n * it is the control and pins the published version, as the editor's default\n * already does.\n *\n * ## The rungs\n *\n * The jobs read gate (membership, the release flag, `aiGenerative`, the org\n * lockdown); then the site — the job's and the org's — a role that writes the\n * site's content, the site's own lockdown verdict, and the `versioning`\n * entitlement, because a page that already has a version gains retained\n * history here, which is what that entitlement sells. Nothing spends a token.\n *\n * Idempotent: each version's id is derived from the job and the variant, so\n * a second press finds the versions the first made.\n */\n\nconst ID_CHARS = /^[A-Za-z0-9_-]{1,100}$/\n\n/** The id a job's variant version is stored under. */\nexport function aiExperimentVersionId(jobId: string, index: number): string {\n return `ab-${jobId}-${index}`\n}\n\n/** What a version made from a variant is named in the versions list. */\nexport function aiExperimentVersionName(name: string, index: number): string {\n const label = name.replace(/\\s+/g, ' ').trim() || `Variant ${index + 1}`\n return `A/B: ${label}`.slice(0, 80)\n}\n\nexport interface AiExperimentVersionsInput {\n hostId: string\n screenId: string\n /** A section test's element; `null` for a page test. */\n nodeId: string | null\n job: Pick<AiJob, '$id'>\n proposal: AiExperimentVariantsProposalView\n uid: string\n now?: Date\n}\n\nexport interface AiExperimentVersionsResult {\n /** One per variant past the control, in order. */\n versions: Array<{ index: number; name: string; versionId: string; changed: string[] }>\n skipped: Array<{ index: number; reason: string }>\n /** Why no version could be made at all, customer-safe; `null` when one could. */\n refusal: string | null\n}\n\nconst str = (value: unknown): string => (typeof value === 'string' ? value.trim() : '')\n\n/** Make the versions. Exported for the spec, which drives it against a Firestore double. */\nexport async function createAiExperimentVersions(\n firestore: FirebaseFirestore.Firestore,\n input: AiExperimentVersionsInput,\n): Promise<AiExperimentVersionsResult> {\n const now = input.now ?? new Date()\n const screenRef = firestore.collection('hosts').doc(input.hostId).collection('screens').doc(input.screenId)\n const screen = await screenRef.get()\n const empty = (refusal: string): AiExperimentVersionsResult => ({ versions: [], skipped: [], refusal })\n if (!screen.exists || screen.get('deletedAt')) return empty('The page under test no longer exists.')\n const publishedId = str(screen.get('versionId'))\n const source = publishedId ? await screenRef.collection('versions').doc(publishedId).get() : null\n const sourceData = source?.exists ? (source.data() as Record<string, unknown>) : null\n const nodes = sourceData ? decodeStoredNodes<Record<string, unknown>>(sourceData['nodes']) : null\n if (!sourceData || !nodes) return empty('The page has no published version to start from.')\n const rootId = typeof sourceData['rootId'] === 'string' ? sourceData['rootId'] : undefined\n const regionRootId = input.nodeId ?? pageContentRootId(nodes as never, rootId)\n\n const versions: AiExperimentVersionsResult['versions'] = []\n const skipped: AiExperimentVersionsResult['skipped'] = []\n for (const [index, variant] of input.proposal.variants.entries()) {\n // The first variant is the copy as it stands: the control, on the published version.\n if (index === 0) continue\n const versionId = aiExperimentVersionId(input.job.$id, index)\n const name = aiExperimentVersionName(variant.name, index)\n const versionRef = screenRef.collection('versions').doc(versionId)\n const earlier = await versionRef.get()\n if (earlier.exists && !earlier.get('deletedAt')) {\n versions.push({ index, name: str(earlier.get('displayName')) || name, versionId, changed: [] })\n continue\n }\n const result = applyAiExperimentVariantCopy(\n nodes,\n { headline: variant.headline, body: variant.body },\n regionRootId,\n )\n if (!result.changed.length) {\n skipped.push({ index, reason: result.reason ?? 'Nothing on the page took the variant’s copy.' })\n continue\n }\n // Compressed at rest, as every write of a version's nodes is.\n const packed = encodeStoredNodes(result.nodes)\n await versionRef.set({\n ...sourceData,\n nodes: packed ? Buffer.from(packed) : result.nodes,\n displayName: name,\n createdAt: now,\n updatedAt: now,\n createdBy: input.uid,\n })\n versions.push({ index, name, versionId, changed: result.changed })\n }\n return { versions, skipped, refusal: null }\n}\n\nexport async function POST(request: Request): Promise<Response> {\n let body: Record<string, unknown>\n try {\n body = ((await request.json()) ?? {}) as Record<string, unknown>\n } catch {\n body = {}\n }\n const gate = await aiJobsGate(request, String(body['orgId'] ?? ''))\n if (gate instanceof Response) return gate\n const hostId = str(body['hostId'])\n const jobId = str(body['jobId'])\n const screenId = str(body['screenId'])\n const nodeId = str(body['nodeId']) || null\n if (!ID_CHARS.test(hostId) || !ID_CHARS.test(jobId) || !ID_CHARS.test(screenId)) {\n return Response.json({ error: 'Name the site, the page under test and the variants to make' }, { status: 400 })\n }\n if (nodeId !== null && !ID_CHARS.test(nodeId)) {\n return Response.json({ error: 'That is not a section of the page' }, { status: 400 })\n }\n const { firestore, orgId, uid, staff, org, decoded } = gate\n\n const hostSnapshot = await firestore.collection('hosts').doc(hostId).get()\n const host = hostSnapshot.exists ? (hostSnapshot.data() as Record<string, unknown>) : null\n if (!host || host['orgId'] !== orgId) {\n return Response.json({ error: 'Unknown site' }, { status: 404 })\n }\n const role = ((host['memberRoles'] ?? {}) as Record<string, unknown>)[uid]\n if (!staff && !hostRoleCanWrite(role)) {\n return Response.json({ error: 'Editing requires the editor role' }, { status: 403 })\n }\n const locked = await lockdownRefusal({\n request,\n staff,\n uid,\n org: org as Record<string, unknown>,\n host: host as never,\n })\n if (locked) return locked\n if (!checkEntitlement(org, 'versioning')) {\n return Response.json({ error: AI_SEO_APPLY_VERSIONING_COPY }, { status: 403 })\n }\n\n const job = await getAiJob(firestore, orgId, jobId)\n if (!job || job.kind !== 'experiment' || job.hostId !== hostId) {\n return Response.json({ error: 'Unknown variants' }, { status: 404 })\n }\n if (job.status !== 'done') {\n return Response.json({ error: 'The variants are still being written' }, { status: 409 })\n }\n const proposal = readAiExperimentVariants(\n (job.outputs ?? []).find((output) => output.resource === 'experiment')?.proposal,\n )\n if (!proposal || proposal.target === 'email') {\n return Response.json({ error: 'These variants are not a page’s or a section’s' }, { status: 409 })\n }\n if (proposal.target === 'section' && !nodeId) {\n return Response.json({ error: 'Pick the section under test first' }, { status: 400 })\n }\n\n const result = await createAiExperimentVersions(firestore, {\n hostId,\n screenId,\n nodeId: proposal.target === 'section' ? nodeId : null,\n job,\n proposal,\n uid,\n })\n if (result.refusal) return Response.json({ error: result.refusal }, { status: 409 })\n const made = result.versions.filter((entry) => entry.changed.length)\n if (made.length) {\n await writeAiJobAudit(firestore, {\n action: 'ai.job.apply',\n actorUid: uid,\n actorEmail: decoded.email ?? null,\n orgId,\n jobId,\n after: { hostId, screenId, versions: made.map((entry) => entry.versionId) },\n })\n }\n return Response.json(result, { status: 200, headers: { 'Cache-Control': 'no-store' } })\n}\n\nexport const dynamic = 'force-dynamic'\n"],"names":["checkEntitlement","encodeStoredNodes","hostRoleCanWrite","pageContentRootId","decodeStoredNodes","lockdownRefusal","getAiJob","writeAiJobAudit","readAiExperimentVariants","applyAiExperimentVariantCopy","AI_SEO_APPLY_VERSIONING_COPY","aiJobsGate","ID_CHARS","aiExperimentVersionId","jobId","index","aiExperimentVersionName","name","label","replace","trim","slice","str","value","createAiExperimentVersions","firestore","input","now","Date","screenRef","collection","doc","hostId","screenId","screen","get","empty","refusal","versions","skipped","exists","publishedId","source","sourceData","data","nodes","rootId","undefined","regionRootId","nodeId","variant","proposal","variants","entries","versionId","job","$id","versionRef","earlier","push","changed","result","headline","body","length","reason","packed","set","Buffer","from","displayName","createdAt","updatedAt","createdBy","uid","POST","request","host","json","gate","String","Response","test","error","status","orgId","staff","org","decoded","hostSnapshot","role","locked","kind","outputs","find","output","resource","target","made","filter","entry","action","actorUid","actorEmail","email","after","map","headers","dynamic"],"mappings":"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"}
|