@aglyn/plugins-ai 1.0.0-beta.218 → 1.0.0-beta.220
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/package.json +18 -18
- package/src/index.d.ts +1 -0
- package/src/index.js.map +1 -1
- package/src/lib/activity/ai-activity-actions.js +8 -1
- package/src/lib/activity/ai-activity-actions.js.map +1 -1
- package/src/lib/billing/ai-month-meter.d.ts +51 -0
- package/src/lib/billing/ai-month-meter.js +89 -0
- package/src/lib/billing/ai-month-meter.js.map +1 -0
- package/src/lib/billing/ai-overage-close.d.ts +11 -33
- package/src/lib/billing/ai-overage-close.js +4 -3
- package/src/lib/billing/ai-overage-close.js.map +1 -1
- package/src/lib/billing/ai-overage-cutover.d.ts +28 -12
- package/src/lib/billing/ai-overage-cutover.js +1 -28
- package/src/lib/billing/ai-overage-cutover.js.map +1 -1
- package/src/lib/billing/ai-overage-ledger.js +1 -1
- package/src/lib/billing/ai-overage-ledger.js.map +1 -1
- package/src/lib/billing/ai-overage-standing.js +1 -1
- package/src/lib/billing/ai-overage-standing.js.map +1 -1
- package/src/lib/components/ai-allotment-editor.component.js +1 -1
- package/src/lib/components/ai-allotment-editor.component.js.map +1 -1
- package/src/lib/components/ai-assist-provider.component.js +5 -4
- package/src/lib/components/ai-assist-provider.component.js.map +1 -1
- package/src/lib/components/ai-automation-zones.d.ts +63 -0
- package/src/lib/components/ai-automation-zones.js +31 -0
- package/src/lib/components/ai-automation-zones.js.map +1 -0
- package/src/lib/components/ai-brief-dialog.component.js +3 -3
- package/src/lib/components/ai-brief-dialog.component.js.map +1 -1
- package/src/lib/components/ai-describe-automation.component.d.ts +1 -1
- package/src/lib/components/ai-describe-automation.component.js.map +1 -1
- package/src/lib/components/ai-describe-button.component.d.ts +6 -3
- package/src/lib/components/ai-describe-button.component.js +4 -1
- package/src/lib/components/ai-describe-button.component.js.map +1 -1
- package/src/lib/components/ai-experiment-cards.component.js +1 -1
- package/src/lib/components/ai-experiment-cards.component.js.map +1 -1
- package/src/lib/components/ai-explain-automation.component.d.ts +1 -1
- package/src/lib/components/ai-explain-automation.component.js.map +1 -1
- package/src/lib/components/ai-job-plan.component.d.ts +8 -0
- package/src/lib/components/ai-job-plan.component.js +21 -4
- package/src/lib/components/ai-job-plan.component.js.map +1 -1
- package/src/lib/components/ai-permissions-on-host.component.d.ts +12 -2
- package/src/lib/components/ai-permissions-on-host.component.js +12 -3
- package/src/lib/components/ai-permissions-on-host.component.js.map +1 -1
- package/src/lib/components/ai-product-copy-card.component.d.ts +1 -1
- package/src/lib/components/ai-product-copy-card.component.js.map +1 -1
- package/src/lib/components/ai-product-import-option.component.d.ts +1 -1
- package/src/lib/components/ai-product-import-option.component.js.map +1 -1
- package/src/lib/components/ai-product-zones.d.ts +124 -0
- package/src/lib/components/ai-product-zones.js +32 -0
- package/src/lib/components/ai-product-zones.js.map +1 -0
- package/src/lib/components/ai-products-hub-card.component.d.ts +1 -1
- package/src/lib/components/ai-products-hub-card.component.js.map +1 -1
- package/src/lib/components/ai-products-proposals.component.d.ts +1 -1
- package/src/lib/components/ai-products-proposals.component.js.map +1 -1
- package/src/lib/components/ai-seo-audit-card.component.js +35 -92
- package/src/lib/components/ai-seo-audit-card.component.js.map +1 -1
- package/src/lib/components/ai-seo-fields-card.component.js.map +1 -1
- package/src/lib/components/ai-theme-proposal-card.component.js.map +1 -1
- package/src/lib/components/assist-jobs-drawer.component.js +21 -15
- package/src/lib/components/assist-jobs-drawer.component.js.map +1 -1
- package/src/lib/components/assist-panel.component.js +2 -2
- package/src/lib/components/assist-panel.component.js.map +1 -1
- package/src/lib/components/assist-signals-page.component.js +2 -2
- package/src/lib/components/assist-signals-page.component.js.map +1 -1
- package/src/lib/components/billing-ai-allotments.component.js +1 -1
- package/src/lib/components/billing-ai-allotments.component.js.map +1 -1
- package/src/lib/components/billing-assist-overage-card.component.js +1 -1
- package/src/lib/components/billing-assist-overage-card.component.js.map +1 -1
- package/src/lib/constants.d.ts +5 -0
- package/src/lib/constants.js +4 -0
- package/src/lib/constants.js.map +1 -1
- package/src/lib/declarations.js +2 -0
- package/src/lib/declarations.js.map +1 -1
- package/src/lib/declarations.server.js +24 -10
- package/src/lib/declarations.server.js.map +1 -1
- package/src/lib/insights/ai-dataset-catalog.d.ts +49 -0
- package/src/lib/insights/ai-dataset-catalog.js +57 -0
- package/src/lib/insights/ai-dataset-catalog.js.map +1 -0
- package/src/lib/insights/ai-figure-readers.d.ts +6 -39
- package/src/lib/insights/ai-figure-readers.js +2 -391
- package/src/lib/insights/ai-figure-readers.js.map +1 -1
- package/src/lib/insights/ai-insight-digest.d.ts +10 -1
- package/src/lib/insights/ai-insight-digest.js +16 -4
- package/src/lib/insights/ai-insight-digest.js.map +1 -1
- package/src/lib/insights/ai-insight-readers.d.ts +1 -1
- package/src/lib/insights/ai-insight-readers.js.map +1 -1
- package/src/lib/jobs/ai-email-bindings.d.ts +28 -21
- package/src/lib/jobs/ai-email-bindings.js +75 -42
- package/src/lib/jobs/ai-email-bindings.js.map +1 -1
- package/src/lib/jobs/ai-job-campaign-step.d.ts +0 -2
- package/src/lib/jobs/ai-job-campaign-step.js +5 -10
- package/src/lib/jobs/ai-job-campaign-step.js.map +1 -1
- package/src/lib/jobs/ai-job-component-step.d.ts +6 -1
- package/src/lib/jobs/ai-job-component-step.js +32 -9
- package/src/lib/jobs/ai-job-component-step.js.map +1 -1
- package/src/lib/jobs/ai-job-draft-ids.js +11 -0
- package/src/lib/jobs/ai-job-draft-ids.js.map +1 -1
- package/src/lib/jobs/ai-job-drafts.js +1 -1
- package/src/lib/jobs/ai-job-drafts.js.map +1 -1
- package/src/lib/jobs/ai-job-email-step.d.ts +5 -3
- package/src/lib/jobs/ai-job-email-step.js +11 -4
- package/src/lib/jobs/ai-job-email-step.js.map +1 -1
- package/src/lib/jobs/ai-job-experiment-step.d.ts +1 -1
- package/src/lib/jobs/ai-job-experiment-step.js +1 -1
- package/src/lib/jobs/ai-job-experiment-step.js.map +1 -1
- package/src/lib/jobs/ai-job-form-step.d.ts +17 -1
- package/src/lib/jobs/ai-job-form-step.js +60 -8
- package/src/lib/jobs/ai-job-form-step.js.map +1 -1
- package/src/lib/jobs/ai-job-generation.d.ts +22 -1
- package/src/lib/jobs/ai-job-generation.js +63 -3
- package/src/lib/jobs/ai-job-generation.js.map +1 -1
- package/src/lib/jobs/ai-job-insight-step.js +1 -1
- package/src/lib/jobs/ai-job-insight-step.js.map +1 -1
- package/src/lib/jobs/ai-job-layout-step.d.ts +15 -4
- package/src/lib/jobs/ai-job-layout-step.js +46 -13
- package/src/lib/jobs/ai-job-layout-step.js.map +1 -1
- package/src/lib/jobs/ai-job-page-sections.d.ts +4 -2
- package/src/lib/jobs/ai-job-page-sections.js +7 -4
- package/src/lib/jobs/ai-job-page-sections.js.map +1 -1
- package/src/lib/jobs/ai-job-page-step.js +7 -2
- package/src/lib/jobs/ai-job-page-step.js.map +1 -1
- package/src/lib/jobs/ai-job-plan-conformance.d.ts +5 -0
- package/src/lib/jobs/ai-job-plan-conformance.js +40 -1
- package/src/lib/jobs/ai-job-plan-conformance.js.map +1 -1
- package/src/lib/jobs/ai-job-plan-creations.d.ts +80 -0
- package/src/lib/jobs/ai-job-plan-creations.js +199 -0
- package/src/lib/jobs/ai-job-plan-creations.js.map +1 -0
- package/src/lib/jobs/ai-job-plan-step.d.ts +11 -2
- package/src/lib/jobs/ai-job-plan-step.js +49 -23
- package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
- package/src/lib/jobs/ai-job-plugin-drafts.d.ts +14 -6
- package/src/lib/jobs/ai-job-plugin-drafts.js +12 -4
- package/src/lib/jobs/ai-job-plugin-drafts.js.map +1 -1
- package/src/lib/jobs/ai-job-products-step.d.ts +7 -0
- package/src/lib/jobs/ai-job-products-step.js +54 -32
- package/src/lib/jobs/ai-job-products-step.js.map +1 -1
- package/src/lib/jobs/ai-job-seo-step.d.ts +3 -24
- package/src/lib/jobs/ai-job-seo-step.js +35 -108
- package/src/lib/jobs/ai-job-seo-step.js.map +1 -1
- package/src/lib/jobs/ai-job-site-step.d.ts +2 -0
- package/src/lib/jobs/ai-job-site-step.js +6 -6
- package/src/lib/jobs/ai-job-site-step.js.map +1 -1
- package/src/lib/jobs/ai-job-template-step.d.ts +9 -6
- package/src/lib/jobs/ai-job-template-step.js +73 -15
- package/src/lib/jobs/ai-job-template-step.js.map +1 -1
- package/src/lib/jobs/ai-job-theme-step.d.ts +1 -1
- package/src/lib/jobs/ai-job-theme-step.js +1 -1
- package/src/lib/jobs/ai-job-theme-step.js.map +1 -1
- package/src/lib/jobs/ai-job-workflow-step.js +17 -9
- package/src/lib/jobs/ai-job-workflow-step.js.map +1 -1
- package/src/lib/jobs/ai-jobs.js +1 -1
- package/src/lib/jobs/ai-jobs.js.map +1 -1
- package/src/lib/jobs/ai-workflow-records.d.ts +20 -11
- package/src/lib/jobs/ai-workflow-records.js +80 -43
- package/src/lib/jobs/ai-workflow-records.js.map +1 -1
- package/src/lib/model/ai-allotments.d.ts +27 -6
- package/src/lib/model/ai-allotments.js +54 -14
- package/src/lib/model/ai-allotments.js.map +1 -1
- package/src/lib/model/ai-automation-outline.d.ts +21 -2
- package/src/lib/model/ai-automation-outline.js.map +1 -1
- package/src/lib/model/ai-build-plan.d.ts +57 -0
- package/src/lib/model/ai-build-plan.js +116 -7
- package/src/lib/model/ai-build-plan.js.map +1 -1
- package/src/lib/model/ai-job-creations.d.ts +55 -0
- package/src/lib/model/ai-job-creations.js +85 -0
- package/src/lib/model/ai-job-creations.js.map +1 -0
- package/src/lib/model/ai-page-job.d.ts +3 -2
- package/src/lib/model/ai-page-job.js +4 -3
- package/src/lib/model/ai-page-job.js.map +1 -1
- package/src/lib/model/ai-seo.d.ts +11 -51
- package/src/lib/model/ai-seo.js +0 -17
- package/src/lib/model/ai-seo.js.map +1 -1
- package/src/lib/model/ai-site-inventory.js +1 -1
- package/src/lib/model/ai-site-inventory.js.map +1 -1
- package/src/lib/model/ai-site-job.d.ts +4 -2
- package/src/lib/model/ai-site-job.js +8 -4
- package/src/lib/model/ai-site-job.js.map +1 -1
- package/src/lib/model/ai-template-subjects.d.ts +10 -0
- package/src/lib/model/ai-template-subjects.js +16 -2
- package/src/lib/model/ai-template-subjects.js.map +1 -1
- package/src/lib/model/ai-theme-proposal.d.ts +1 -1
- package/src/lib/model/ai-theme-proposal.js +1 -1
- package/src/lib/model/ai-theme-proposal.js.map +1 -1
- package/src/lib/model/ai-tokens.js +1 -1
- package/src/lib/model/ai-tokens.js.map +1 -1
- package/src/lib/model/ai-usage-by-user.js +1 -1
- package/src/lib/model/ai-usage-by-user.js.map +1 -1
- package/src/lib/model/ai-workflow-job.d.ts +1 -3
- package/src/lib/model/ai-workflow-job.js +1 -2
- package/src/lib/model/ai-workflow-job.js.map +1 -1
- package/src/lib/plan-entitlements.d.ts +149 -0
- package/src/lib/plan-entitlements.js +168 -0
- package/src/lib/plan-entitlements.js.map +1 -0
- package/src/lib/plugin.js +1 -1
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/providers/catalog.js +1 -1
- package/src/lib/providers/catalog.js.map +1 -1
- package/src/lib/providers/model-choice.js +1 -1
- package/src/lib/providers/model-choice.js.map +1 -1
- package/src/lib/runtime/ai-device-audit.d.ts +1 -1
- package/src/lib/runtime/ai-device-audit.js +1 -1
- package/src/lib/runtime/ai-device-audit.js.map +1 -1
- package/src/lib/runtime/ai-doctrine-validators.d.ts +25 -4
- package/src/lib/runtime/ai-doctrine-validators.js +178 -21
- package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
- package/src/lib/runtime/ai-doctrine.js +5 -5
- package/src/lib/runtime/ai-doctrine.js.map +1 -1
- package/src/lib/runtime/ai-eval-live.js +30 -15
- package/src/lib/runtime/ai-eval-live.js.map +1 -1
- package/src/lib/runtime/ai-eval.d.ts +9 -2
- package/src/lib/runtime/ai-eval.js +29 -2
- package/src/lib/runtime/ai-eval.js.map +1 -1
- package/src/lib/runtime/ai-gate.js +1 -1
- package/src/lib/runtime/ai-gate.js.map +1 -1
- package/src/lib/runtime/ai-node-tree.js +3 -3
- package/src/lib/runtime/ai-node-tree.js.map +1 -1
- package/src/lib/runtime/ai-palette.d.ts +7 -1
- package/src/lib/runtime/ai-palette.generated.d.ts +1 -1
- package/src/lib/runtime/ai-palette.generated.js +16 -13
- package/src/lib/runtime/ai-palette.generated.js.map +1 -1
- package/src/lib/runtime/ai-palette.js +7 -1
- package/src/lib/runtime/ai-palette.js.map +1 -1
- package/src/lib/runtime/seo-audit.d.ts +13 -63
- package/src/lib/runtime/seo-audit.js +22 -207
- package/src/lib/runtime/seo-audit.js.map +1 -1
- package/src/lib/runtime/site-inventory.js +29 -23
- package/src/lib/runtime/site-inventory.js.map +1 -1
- package/src/lib/server/ai-admin-org.js +1 -1
- package/src/lib/server/ai-admin-org.js.map +1 -1
- package/src/lib/server/ai-admin-orgs-spend.js +1 -1
- package/src/lib/server/ai-admin-orgs-spend.js.map +1 -1
- package/src/lib/server/ai-admin-signals.js +1 -1
- package/src/lib/server/ai-admin-signals.js.map +1 -1
- package/src/lib/server/ai-allotments.js +1 -1
- package/src/lib/server/ai-allotments.js.map +1 -1
- package/src/lib/server/ai-assist-prompts.js +3 -3
- package/src/lib/server/ai-assist-prompts.js.map +1 -1
- package/src/lib/server/ai-assist.js +2 -2
- package/src/lib/server/ai-assist.js.map +1 -1
- package/src/lib/server/assist-chat.d.ts +1 -1
- package/src/lib/server/assist-chat.js +13 -12
- package/src/lib/server/assist-chat.js.map +1 -1
- package/src/lib/server/assist-view-context.js +23 -23
- package/src/lib/server/assist-view-context.js.map +1 -1
- package/src/lib/server/billing-credits.d.ts +5 -0
- package/src/lib/server/billing-credits.js +8 -2
- package/src/lib/server/billing-credits.js.map +1 -1
- package/src/lib/server/billing-overage.js +2 -2
- package/src/lib/server/billing-overage.js.map +1 -1
- package/src/lib/server.d.ts +1 -0
- package/src/lib/server.js.map +1 -1
- package/src/lib/testing/stand-in-automation-index.d.ts +20 -0
- package/src/lib/testing/stand-in-automation-index.js +102 -0
- package/src/lib/testing/stand-in-automation-index.js.map +1 -0
- package/src/lib/testing/stand-in-dataset-index.d.ts +20 -0
- package/src/lib/testing/stand-in-dataset-index.js +80 -0
- package/src/lib/testing/stand-in-dataset-index.js.map +1 -0
- package/src/lib/testing/stand-in-list-send-time.d.ts +4 -0
- package/src/lib/testing/stand-in-list-send-time.js +77 -0
- package/src/lib/testing/stand-in-list-send-time.js.map +1 -0
- package/src/lib/testing/stand-in-pipeline-index.d.ts +32 -0
- package/src/lib/testing/stand-in-pipeline-index.js +59 -0
- package/src/lib/testing/stand-in-pipeline-index.js.map +1 -0
- package/src/lib/testing/stand-in-product-index.d.ts +20 -0
- package/src/lib/testing/stand-in-product-index.js +105 -0
- package/src/lib/testing/stand-in-product-index.js.map +1 -0
- package/src/lib/tools/ai-component-tool.js +2 -2
- package/src/lib/tools/ai-component-tool.js.map +1 -1
- package/src/lib/tools/ai-duplicate-resource-tool.js +1 -1
- package/src/lib/tools/ai-duplicate-resource-tool.js.map +1 -1
- package/src/lib/tools/ai-seo-tool.d.ts +6 -18
- package/src/lib/tools/ai-seo-tool.js +7 -38
- package/src/lib/tools/ai-seo-tool.js.map +1 -1
- package/src/lib/tools/ai-workflow-tool.d.ts +1 -1
- package/src/lib/tools/ai-workflow-tool.js +1 -1
- package/src/lib/tools/ai-workflow-tool.js.map +1 -1
- package/src/lib/usage/ai-allotment-alerts.d.ts +10 -7
- package/src/lib/usage/ai-allotment-alerts.js +39 -15
- package/src/lib/usage/ai-allotment-alerts.js.map +1 -1
- package/src/lib/usage/ai-quota-checks.d.ts +37 -0
- package/src/lib/usage/ai-quota-checks.js +69 -0
- package/src/lib/usage/ai-quota-checks.js.map +1 -0
- package/src/lib/usage/ai-usage-alerts.js +22 -10
- package/src/lib/usage/ai-usage-alerts.js.map +1 -1
- package/src/lib/usage/ai-usage-by-user.js +1 -1
- package/src/lib/usage/ai-usage-by-user.js.map +1 -1
- package/src/lib/usage/assist-ceiling.d.ts +2 -2
- package/src/lib/usage/assist-ceiling.js +4 -4
- package/src/lib/usage/assist-ceiling.js.map +1 -1
- package/src/lib/usage/assist-credits.d.ts +494 -0
- package/src/lib/usage/assist-credits.js +524 -0
- package/src/lib/usage/assist-credits.js.map +1 -0
- package/src/lib/usage/assist-free-taste.d.ts +1 -1
- package/src/lib/usage/assist-free-taste.js +3 -2
- package/src/lib/usage/assist-free-taste.js.map +1 -1
- package/src/lib/usage/assist-signal-mining.js +1 -1
- package/src/lib/usage/assist-signal-mining.js.map +1 -1
- package/src/lib/usage/assist-spend-guards.d.ts +153 -0
- package/src/lib/usage/assist-spend-guards.js +182 -0
- package/src/lib/usage/assist-spend-guards.js.map +1 -0
- package/src/lib/usage/assist-usage.d.ts +1 -1
- package/src/lib/usage/assist-usage.js +1 -1
- package/src/lib/usage/assist-usage.js.map +1 -1
- package/src/lib/usage/staff-org-ai.js +1 -1
- package/src/lib/usage/staff-org-ai.js.map +1 -1
- package/src/lib/usage-axes.d.ts +49 -0
- package/src/lib/usage-axes.js +108 -0
- package/src/lib/usage-axes.js.map +1 -0
- package/src/lib/runtime/seo-page-facts.d.ts +0 -61
- package/src/lib/runtime/seo-page-facts.js +0 -150
- package/src/lib/runtime/seo-page-facts.js.map +0 -1
|
@@ -29,9 +29,9 @@ import type { AiJobAdmissionContext, AiJobAdmissionRefusal } from './ai-job-admi
|
|
|
29
29
|
* caller, so a caller establishes, before it asks for anything:
|
|
30
30
|
*
|
|
31
31
|
* 1. a site is named, and it is the job's own org's;
|
|
32
|
-
* 2. the owning plugin
|
|
33
|
-
*
|
|
34
|
-
*
|
|
32
|
+
* 2. the owning plugin — whichever registered the writer; this plugin names
|
|
33
|
+
* none — runs on that site, switched on for the workspace, not switched
|
|
34
|
+
* off for the site, and past its release flag;
|
|
35
35
|
* 3. whatever the kind checks of its own, such as the plan a campaign needs;
|
|
36
36
|
* 4. that the member the drafts are for may create one of each, which only
|
|
37
37
|
* the owner can answer, and does, through `refusal`.
|
|
@@ -46,12 +46,18 @@ import type { AiJobAdmissionContext, AiJobAdmissionRefusal } from './ai-job-admi
|
|
|
46
46
|
export type AiPluginDraftWriterLookup = (resource: string) => PluginResourceDraftWriter | null;
|
|
47
47
|
/** The registered writer for a resource, or `null` when no plugin writes it here. */
|
|
48
48
|
export declare const aiPluginDraftWriter: AiPluginDraftWriterLookup;
|
|
49
|
-
/**
|
|
49
|
+
/**
|
|
50
|
+
* The plugin that registered the writer for a resource, or `null` when none
|
|
51
|
+
* did in this process. The owner is whoever writes the resource — the
|
|
52
|
+
* registry knows it, so this plugin names none (AGL-3080).
|
|
53
|
+
*/
|
|
54
|
+
export type AiPluginDraftOwnerLookup = (resource: string) => string | null;
|
|
55
|
+
/** The registered writer's plugin, from the core's registry. */
|
|
56
|
+
export declare const aiPluginDraftOwner: AiPluginDraftOwnerLookup;
|
|
57
|
+
/** One draft a job kind needs written. */
|
|
50
58
|
export interface AiPluginDraftNeed {
|
|
51
59
|
/** The resource name the owner registered its writer under. */
|
|
52
60
|
resource: string;
|
|
53
|
-
/** The owning plugin, as `plugins.config.json` names it. */
|
|
54
|
-
pluginId: string;
|
|
55
61
|
/** What the site's plugin list calls it, for the sentence a person reads. */
|
|
56
62
|
label: string;
|
|
57
63
|
}
|
|
@@ -64,6 +70,8 @@ export interface AiPluginDraftAdmissionInput {
|
|
|
64
70
|
ownCheck?: (hostId: string) => Promise<AiJobAdmissionRefusal | null>;
|
|
65
71
|
/** How a writer is found; the registry otherwise. */
|
|
66
72
|
writerFor?: AiPluginDraftWriterLookup;
|
|
73
|
+
/** Which plugin owns a resource's writer; the registry otherwise. */
|
|
74
|
+
ownerFor?: AiPluginDraftOwnerLookup;
|
|
67
75
|
/** The instant the owner's allowance is read against; now otherwise. */
|
|
68
76
|
now?: Date;
|
|
69
77
|
}
|
|
@@ -22,6 +22,11 @@ import { resolveOrgIdForHost } from "@aglyn/tenant-data-admin/server/organizatio
|
|
|
22
22
|
var _pluginResourceDraftWriter;
|
|
23
23
|
return (_ref = (_pluginResourceDraftWriter = pluginResourceDraftWriter(resource)) == null ? void 0 : _pluginResourceDraftWriter.writer) != null ? _ref : null;
|
|
24
24
|
};
|
|
25
|
+
/** The registered writer's plugin, from the core's registry. */ export const aiPluginDraftOwner = (resource)=>{
|
|
26
|
+
var _ref;
|
|
27
|
+
var _pluginResourceDraftWriter;
|
|
28
|
+
return (_ref = (_pluginResourceDraftWriter = pluginResourceDraftWriter(resource)) == null ? void 0 : _pluginResourceDraftWriter.pluginId) != null ? _ref : null;
|
|
29
|
+
};
|
|
25
30
|
/** What a person is told when the plugin that owns a draft does not run here. */ export function aiPluginDraftUnavailable(label) {
|
|
26
31
|
return `Turn on ${label} for this site before starting the job.`;
|
|
27
32
|
}
|
|
@@ -29,7 +34,7 @@ import { resolveOrgIdForHost } from "@aglyn/tenant-data-admin/server/organizatio
|
|
|
29
34
|
* Whether a job of this kind may start on this site for this member; `null`
|
|
30
35
|
* admits it. Nothing here writes, and nothing here spends.
|
|
31
36
|
*/ export async function aiPluginDraftAdmissionRefusal(context, input) {
|
|
32
|
-
var _input_writerFor, _data, _input_now;
|
|
37
|
+
var _input_writerFor, _input_ownerFor, _data, _input_now;
|
|
33
38
|
const { hostId } = context;
|
|
34
39
|
if (!hostId) {
|
|
35
40
|
return {
|
|
@@ -43,18 +48,21 @@ import { resolveOrgIdForHost } from "@aglyn/tenant-data-admin/server/organizatio
|
|
|
43
48
|
error: 'Unknown site'
|
|
44
49
|
};
|
|
45
50
|
const writerFor = (_input_writerFor = input.writerFor) != null ? _input_writerFor : aiPluginDraftWriter;
|
|
51
|
+
const ownerFor = (_input_ownerFor = input.ownerFor) != null ? _input_ownerFor : aiPluginDraftOwner;
|
|
46
52
|
const host = (_data = (await context.firestore.collection('hosts').doc(hostId).get()).data()) != null ? _data : null;
|
|
47
53
|
const org = context.org;
|
|
54
|
+
const owners = input.drafts.map((draft)=>ownerFor(draft.resource));
|
|
48
55
|
// One release-flag read for every owner at once: the answer is per org, and
|
|
49
56
|
// a plugin subtracted here is one the site cannot reach at all.
|
|
50
|
-
const released = new Set(await filterEnabledPluginsByReleaseFlags(
|
|
57
|
+
const released = new Set(await filterEnabledPluginsByReleaseFlags(owners.filter((owner)=>Boolean(owner)), {
|
|
51
58
|
orgId: context.orgId,
|
|
52
59
|
authorization: null
|
|
53
60
|
}));
|
|
54
61
|
const writers = [];
|
|
55
|
-
for (const draft of input.drafts){
|
|
62
|
+
for (const [index, draft] of input.drafts.entries()){
|
|
56
63
|
const writer = writerFor(draft.resource);
|
|
57
|
-
|
|
64
|
+
const owner = owners[index];
|
|
65
|
+
if (!writer || !owner || !released.has(owner) || !isHostPluginEnabled(org, host, owner)) {
|
|
58
66
|
return {
|
|
59
67
|
status: 403,
|
|
60
68
|
error: aiPluginDraftUnavailable(draft.label)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-plugin-drafts.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 { isHostPluginEnabled } from '@aglyn/aglyn/plugin-manager/enabled-plugins'\nimport {\n pluginResourceDraftWriter,\n type PluginResourceDraftWriter,\n} from '@aglyn/aglyn/plugin-manager/plugin-resource-drafts'\nimport { filterEnabledPluginsByReleaseFlags } from '@aglyn/tenant-data-admin/server/release-flags'\nimport { resolveOrgIdForHost } from '@aglyn/tenant-data-admin/server/organizations'\nimport type { AiJobKind } from '../model/ai-jobs.types'\nimport type { AiJobAdmissionContext, AiJobAdmissionRefusal } from './ai-job-admission'\n\n/**\n * Admitting a job whose drafts ANOTHER plugin writes (AGL-2912).\n *\n * `aiDraftAdmissionRefusal` admits the kinds this plugin writes itself, where\n * the allowance is one it can read. An email job writes no document of its\n * own: the email plugin owns the email design and the marketing plugin owns\n * the campaign, and each registers a writer on the `plugin-resource-drafts`\n * seam. So the question this answers is different, and it is the caller's\n * half of that seam's contract — the seam carries no authorization of the\n * caller, so a caller establishes, before it asks for anything:\n *\n * 1. a site is named, and it is the job's own org's;\n * 2. the owning plugin runs on that site — switched on for the workspace,\n * not switched off for the site, and past its release flag — and has\n * actually registered its writer in this process;\n * 3. whatever the kind checks of its own, such as the plan a campaign needs;\n * 4. that the member the drafts are for may create one of each, which only\n * the owner can answer, and does, through `refusal`.\n *\n * Asked at the create door before the job exists and again at the resume door\n * before a confirmed plan runs, so a plugin switched off, or an allowance used\n * up, in between is caught before the job spends. A refusal is the owner's own\n * sentence wherever there is one, so a person reads what the console would\n * have told them.\n */\n\n/** How a resource's draft writer is found; the core's registry by default. */\nexport type AiPluginDraftWriterLookup = (resource: string) => PluginResourceDraftWriter | null\n\n/** The registered writer for a resource, or `null` when no plugin writes it here. */\nexport const aiPluginDraftWriter: AiPluginDraftWriterLookup = (resource) =>\n pluginResourceDraftWriter(resource)?.writer ?? null\n\n/** One draft a job kind needs written, and who owns it. */\nexport interface AiPluginDraftNeed {\n /** The resource name the owner registered its writer under. */\n resource: string\n /** The owning plugin, as `plugins.config.json` names it. */\n pluginId: string\n /** What the site's plugin list calls it, for the sentence a person reads. */\n label: string\n}\n\nexport interface AiPluginDraftAdmissionInput {\n /** The job kind, named in the refusal that asks for a site. */\n kind: AiJobKind\n /** Every draft the kind writes, each checked in turn. */\n drafts: readonly AiPluginDraftNeed[]\n /** The kind's own check, asked once the site and its plugins are known good. */\n ownCheck?: (hostId: string) => Promise<AiJobAdmissionRefusal | null>\n /** How a writer is found; the registry otherwise. */\n writerFor?: AiPluginDraftWriterLookup\n /** The instant the owner's allowance is read against; now otherwise. */\n now?: Date\n}\n\n/** What a person is told when the plugin that owns a draft does not run here. */\nexport function aiPluginDraftUnavailable(label: string): string {\n return `Turn on ${label} for this site before starting the job.`\n}\n\n/**\n * Whether a job of this kind may start on this site for this member; `null`\n * admits it. Nothing here writes, and nothing here spends.\n */\nexport async function aiPluginDraftAdmissionRefusal(\n context: AiJobAdmissionContext,\n input: AiPluginDraftAdmissionInput,\n): Promise<AiJobAdmissionRefusal | null> {\n const { hostId } = context\n if (!hostId) {\n return { status: 400, error: `Open the site the ${input.kind} is for before starting the job` }\n }\n const owner = await resolveOrgIdForHost(hostId)\n if (!owner || owner !== context.orgId) return { status: 404, error: 'Unknown site' }\n\n const writerFor = input.writerFor ?? aiPluginDraftWriter\n const host = (await context.firestore.collection('hosts').doc(hostId).get()).data() ?? null\n const org = context.org as { enabledPlugins?: string[] } | null\n // One release-flag read for every owner at once: the answer is per org, and\n // a plugin subtracted here is one the site cannot reach at all.\n const released = new Set(\n await filterEnabledPluginsByReleaseFlags(\n input.drafts.map((draft) => draft.pluginId),\n { orgId: context.orgId, authorization: null },\n ),\n )\n const writers: PluginResourceDraftWriter[] = []\n for (const draft of input.drafts) {\n const writer = writerFor(draft.resource)\n if (\n !writer ||\n !released.has(draft.pluginId) ||\n !isHostPluginEnabled(org, host, draft.pluginId)\n ) {\n return { status: 403, error: aiPluginDraftUnavailable(draft.label) }\n }\n writers.push(writer)\n }\n\n const own = await input.ownCheck?.(hostId)\n if (own) return own\n\n // Whether this member may create one of each, in the owner's own words. A\n // door that names no member — the machine re-checking a job it stored —\n // asks nothing here, because there is nobody to ask about.\n const uid = context.uid\n if (!uid) return null\n const now = input.now ?? new Date()\n for (const writer of writers) {\n const refusal = await writer.refusal({ orgId: context.orgId, hostId, uid, org, now })\n // The seam's 409 is \"one already exists\", which for an admission is a\n // room-to-create answer rather than a fault the door can act on.\n if (refusal) return { status: refusal.status === 409 ? 403 : refusal.status, error: refusal.error }\n }\n return null\n}\n"],"names":["isHostPluginEnabled","pluginResourceDraftWriter","filterEnabledPluginsByReleaseFlags","resolveOrgIdForHost","aiPluginDraftWriter","resource","writer","aiPluginDraftUnavailable","label","aiPluginDraftAdmissionRefusal","context","input","hostId","status","error","kind","owner","orgId","writerFor","host","firestore","collection","doc","get","data","org","released","Set","drafts","map","draft","pluginId","authorization","writers","has","push","own","ownCheck","uid","now","Date","refusal"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,mBAAmB,QAAQ,8CAA6C;AACjF,SACEC,yBAAyB,QAEpB,qDAAoD;AAC3D,SAASC,kCAAkC,QAAQ,gDAA+C;AAClG,SAASC,mBAAmB,QAAQ,gDAA+C;AAiCnF,mFAAmF,GACnF,OAAO,MAAMC,sBAAiD,CAACC;;QAC7DJ;oBAAAA,6BAAAA,0BAA0BI,8BAA1BJ,2BAAqCK,MAAM,mBAAI;EAAI;AAyBrD,+EAA+E,GAC/E,OAAO,SAASC,yBAAyBC,KAAa;IACpD,OAAO,CAAC,QAAQ,EAAEA,MAAM,uCAAuC,CAAC;AAClE;AAEA;;;CAGC,GACD,OAAO,eAAeC,8BACpBC,OAA8B,EAC9BC,KAAkC;QAShBA,kBACL,OA+BDA;IAvCZ,MAAM,EAAEC,MAAM,EAAE,GAAGF;IACnB,IAAI,CAACE,QAAQ;QACX,OAAO;YAAEC,QAAQ;YAAKC,OAAO,CAAC,kBAAkB,EAAEH,MAAMI,IAAI,CAAC,+BAA+B,CAAC;QAAC;IAChG;IACA,MAAMC,QAAQ,MAAMb,oBAAoBS;IACxC,IAAI,CAACI,SAASA,UAAUN,QAAQO,KAAK,EAAE,OAAO;QAAEJ,QAAQ;QAAKC,OAAO;IAAe;IAEnF,MAAMI,aAAYP,mBAAAA,MAAMO,SAAS,YAAfP,mBAAmBP;IACrC,MAAMe,QAAO,QAAA,AAAC,CAAA,MAAMT,QAAQU,SAAS,CAACC,UAAU,CAAC,SAASC,GAAG,CAACV,QAAQW,GAAG,EAAC,EAAGC,IAAI,cAApE,QAA0E;IACvF,MAAMC,MAAMf,QAAQe,GAAG;IACvB,4EAA4E;IAC5E,gEAAgE;IAChE,MAAMC,WAAW,IAAIC,IACnB,MAAMzB,mCACJS,MAAMiB,MAAM,CAACC,GAAG,CAAC,CAACC,QAAUA,MAAMC,QAAQ,GAC1C;QAAEd,OAAOP,QAAQO,KAAK;QAAEe,eAAe;IAAK;IAGhD,MAAMC,UAAuC,EAAE;IAC/C,KAAK,MAAMH,SAASnB,MAAMiB,MAAM,CAAE;QAChC,MAAMtB,SAASY,UAAUY,MAAMzB,QAAQ;QACvC,IACE,CAACC,UACD,CAACoB,SAASQ,GAAG,CAACJ,MAAMC,QAAQ,KAC5B,CAAC/B,oBAAoByB,KAAKN,MAAMW,MAAMC,QAAQ,GAC9C;YACA,OAAO;gBAAElB,QAAQ;gBAAKC,OAAOP,yBAAyBuB,MAAMtB,KAAK;YAAE;QACrE;QACAyB,QAAQE,IAAI,CAAC7B;IACf;IAEA,MAAM8B,MAAM,OAAMzB,MAAM0B,QAAQ,oBAAd1B,MAAM0B,QAAQ,MAAd1B,OAAiBC;IACnC,IAAIwB,KAAK,OAAOA;IAEhB,0EAA0E;IAC1E,wEAAwE;IACxE,2DAA2D;IAC3D,MAAME,MAAM5B,QAAQ4B,GAAG;IACvB,IAAI,CAACA,KAAK,OAAO;IACjB,MAAMC,OAAM5B,aAAAA,MAAM4B,GAAG,YAAT5B,aAAa,IAAI6B;IAC7B,KAAK,MAAMlC,UAAU2B,QAAS;QAC5B,MAAMQ,UAAU,MAAMnC,OAAOmC,OAAO,CAAC;YAAExB,OAAOP,QAAQO,KAAK;YAAEL;YAAQ0B;YAAKb;YAAKc;QAAI;QACnF,sEAAsE;QACtE,iEAAiE;QACjE,IAAIE,SAAS,OAAO;YAAE5B,QAAQ4B,QAAQ5B,MAAM,KAAK,MAAM,MAAM4B,QAAQ5B,MAAM;YAAEC,OAAO2B,QAAQ3B,KAAK;QAAC;IACpG;IACA,OAAO;AACT"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-plugin-drafts.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 { isHostPluginEnabled } from '@aglyn/aglyn/plugin-manager/enabled-plugins'\nimport {\n pluginResourceDraftWriter,\n type PluginResourceDraftWriter,\n} from '@aglyn/aglyn/plugin-manager/plugin-resource-drafts'\nimport { filterEnabledPluginsByReleaseFlags } from '@aglyn/tenant-data-admin/server/release-flags'\nimport { resolveOrgIdForHost } from '@aglyn/tenant-data-admin/server/organizations'\nimport type { AiJobKind } from '../model/ai-jobs.types'\nimport type { AiJobAdmissionContext, AiJobAdmissionRefusal } from './ai-job-admission'\n\n/**\n * Admitting a job whose drafts ANOTHER plugin writes (AGL-2912).\n *\n * `aiDraftAdmissionRefusal` admits the kinds this plugin writes itself, where\n * the allowance is one it can read. An email job writes no document of its\n * own: the email plugin owns the email design and the marketing plugin owns\n * the campaign, and each registers a writer on the `plugin-resource-drafts`\n * seam. So the question this answers is different, and it is the caller's\n * half of that seam's contract — the seam carries no authorization of the\n * caller, so a caller establishes, before it asks for anything:\n *\n * 1. a site is named, and it is the job's own org's;\n * 2. the owning plugin — whichever registered the writer; this plugin names\n * none — runs on that site, switched on for the workspace, not switched\n * off for the site, and past its release flag;\n * 3. whatever the kind checks of its own, such as the plan a campaign needs;\n * 4. that the member the drafts are for may create one of each, which only\n * the owner can answer, and does, through `refusal`.\n *\n * Asked at the create door before the job exists and again at the resume door\n * before a confirmed plan runs, so a plugin switched off, or an allowance used\n * up, in between is caught before the job spends. A refusal is the owner's own\n * sentence wherever there is one, so a person reads what the console would\n * have told them.\n */\n\n/** How a resource's draft writer is found; the core's registry by default. */\nexport type AiPluginDraftWriterLookup = (resource: string) => PluginResourceDraftWriter | null\n\n/** The registered writer for a resource, or `null` when no plugin writes it here. */\nexport const aiPluginDraftWriter: AiPluginDraftWriterLookup = (resource) =>\n pluginResourceDraftWriter(resource)?.writer ?? null\n\n/**\n * The plugin that registered the writer for a resource, or `null` when none\n * did in this process. The owner is whoever writes the resource — the\n * registry knows it, so this plugin names none (AGL-3080).\n */\nexport type AiPluginDraftOwnerLookup = (resource: string) => string | null\n\n/** The registered writer's plugin, from the core's registry. */\nexport const aiPluginDraftOwner: AiPluginDraftOwnerLookup = (resource) =>\n pluginResourceDraftWriter(resource)?.pluginId ?? null\n\n/** One draft a job kind needs written. */\nexport interface AiPluginDraftNeed {\n /** The resource name the owner registered its writer under. */\n resource: string\n /** What the site's plugin list calls it, for the sentence a person reads. */\n label: string\n}\n\nexport interface AiPluginDraftAdmissionInput {\n /** The job kind, named in the refusal that asks for a site. */\n kind: AiJobKind\n /** Every draft the kind writes, each checked in turn. */\n drafts: readonly AiPluginDraftNeed[]\n /** The kind's own check, asked once the site and its plugins are known good. */\n ownCheck?: (hostId: string) => Promise<AiJobAdmissionRefusal | null>\n /** How a writer is found; the registry otherwise. */\n writerFor?: AiPluginDraftWriterLookup\n /** Which plugin owns a resource's writer; the registry otherwise. */\n ownerFor?: AiPluginDraftOwnerLookup\n /** The instant the owner's allowance is read against; now otherwise. */\n now?: Date\n}\n\n/** What a person is told when the plugin that owns a draft does not run here. */\nexport function aiPluginDraftUnavailable(label: string): string {\n return `Turn on ${label} for this site before starting the job.`\n}\n\n/**\n * Whether a job of this kind may start on this site for this member; `null`\n * admits it. Nothing here writes, and nothing here spends.\n */\nexport async function aiPluginDraftAdmissionRefusal(\n context: AiJobAdmissionContext,\n input: AiPluginDraftAdmissionInput,\n): Promise<AiJobAdmissionRefusal | null> {\n const { hostId } = context\n if (!hostId) {\n return { status: 400, error: `Open the site the ${input.kind} is for before starting the job` }\n }\n const owner = await resolveOrgIdForHost(hostId)\n if (!owner || owner !== context.orgId) return { status: 404, error: 'Unknown site' }\n\n const writerFor = input.writerFor ?? aiPluginDraftWriter\n const ownerFor = input.ownerFor ?? aiPluginDraftOwner\n const host = (await context.firestore.collection('hosts').doc(hostId).get()).data() ?? null\n const org = context.org as { enabledPlugins?: string[] } | null\n const owners = input.drafts.map((draft) => ownerFor(draft.resource))\n // One release-flag read for every owner at once: the answer is per org, and\n // a plugin subtracted here is one the site cannot reach at all.\n const released = new Set(\n await filterEnabledPluginsByReleaseFlags(\n owners.filter((owner): owner is string => Boolean(owner)),\n { orgId: context.orgId, authorization: null },\n ),\n )\n const writers: PluginResourceDraftWriter[] = []\n for (const [index, draft] of input.drafts.entries()) {\n const writer = writerFor(draft.resource)\n const owner = owners[index]\n if (\n !writer ||\n !owner ||\n !released.has(owner) ||\n !isHostPluginEnabled(org, host, owner)\n ) {\n return { status: 403, error: aiPluginDraftUnavailable(draft.label) }\n }\n writers.push(writer)\n }\n\n const own = await input.ownCheck?.(hostId)\n if (own) return own\n\n // Whether this member may create one of each, in the owner's own words. A\n // door that names no member — the machine re-checking a job it stored —\n // asks nothing here, because there is nobody to ask about.\n const uid = context.uid\n if (!uid) return null\n const now = input.now ?? new Date()\n for (const writer of writers) {\n const refusal = await writer.refusal({ orgId: context.orgId, hostId, uid, org, now })\n // The seam's 409 is \"one already exists\", which for an admission is a\n // room-to-create answer rather than a fault the door can act on.\n if (refusal) return { status: refusal.status === 409 ? 403 : refusal.status, error: refusal.error }\n }\n return null\n}\n"],"names":["isHostPluginEnabled","pluginResourceDraftWriter","filterEnabledPluginsByReleaseFlags","resolveOrgIdForHost","aiPluginDraftWriter","resource","writer","aiPluginDraftOwner","pluginId","aiPluginDraftUnavailable","label","aiPluginDraftAdmissionRefusal","context","input","hostId","status","error","kind","owner","orgId","writerFor","ownerFor","host","firestore","collection","doc","get","data","org","owners","drafts","map","draft","released","Set","filter","Boolean","authorization","writers","index","entries","has","push","own","ownCheck","uid","now","Date","refusal"],"mappings":"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"}
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
|
+
import { type PluginRecordIndex } from '@aglyn/aglyn/plugin-manager/plugin-record-index';
|
|
17
18
|
import type { AiJob } from '../model/ai-jobs.types';
|
|
18
19
|
import { type AiProductPhotoRead } from '../model/ai-products';
|
|
19
20
|
import { type AiJobAdmission } from './ai-job-admission';
|
|
@@ -88,6 +89,12 @@ export interface AiJobProductsStepDeps {
|
|
|
88
89
|
image?: AiProductImageSeams;
|
|
89
90
|
/** Whether a model reads pictures; the provider's own descriptor otherwise. */
|
|
90
91
|
readsImages?: (model: string) => boolean;
|
|
92
|
+
/**
|
|
93
|
+
* Where the site's categories are read; the `productCategory` index the
|
|
94
|
+
* plugin that keeps them publishes otherwise. The eval harness hands its
|
|
95
|
+
* own, over the categories a case names.
|
|
96
|
+
*/
|
|
97
|
+
categoryIndex?: PluginRecordIndex | null;
|
|
91
98
|
}
|
|
92
99
|
/** The step, with its reads replaceable where a spec needs them. */
|
|
93
100
|
export declare function createAiJobProductsStep(deps?: AiJobProductsStepDeps): AiJobStepRunner;
|
|
@@ -15,6 +15,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
15
15
|
* See the License for the specific language governing permissions and
|
|
16
16
|
* limitations under the License.
|
|
17
17
|
*/ import { checkEntitlement } from "@aglyn/aglyn/app-utils/plan-entitlements";
|
|
18
|
+
import { pluginRecordIndex } from "@aglyn/aglyn/plugin-manager/plugin-record-index";
|
|
18
19
|
import { isHostPluginEnabled } from "@aglyn/aglyn/plugin-manager/enabled-plugins";
|
|
19
20
|
import { resolveOrgIdForHost } from "@aglyn/tenant-data-admin/server/organizations";
|
|
20
21
|
import { filterEnabledPluginsByReleaseFlags } from "@aglyn/tenant-data-admin/server/release-flags";
|
|
@@ -76,7 +77,6 @@ export const AI_PRODUCTS_DECLINED_COPY = 'The model declined to write copy for t
|
|
|
76
77
|
unreadable: 'The photo could not be read, so the copy is written from the product’s words.',
|
|
77
78
|
model: 'The photo was not read: the model this job ran on does not read pictures.'
|
|
78
79
|
};
|
|
79
|
-
/** The commerce plugin, as `plugins.config.json` names it. */ const COMMERCE_PLUGIN_ID = 'commerce';
|
|
80
80
|
/**
|
|
81
81
|
* ASSUMED: how long a catalog or categories pass reads the site before it
|
|
82
82
|
* asks: the host document and, for categories, the category names.
|
|
@@ -135,40 +135,54 @@ const str = (value)=>typeof value === 'string' ? value.trim() : '';
|
|
|
135
135
|
name: str(seo['title']) || str(host['displayName']) || subdomain || ''
|
|
136
136
|
};
|
|
137
137
|
}
|
|
138
|
-
/**
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
138
|
+
/**
|
|
139
|
+
* The site's categories by name, as many as a request lists — from the
|
|
140
|
+
* `productCategory` index the plugin that keeps them publishes (AGL-3080).
|
|
141
|
+
* None when no plugin keeps categories here.
|
|
142
|
+
*/ async function readCategories(hostId, given) {
|
|
143
|
+
var _pluginRecordIndex;
|
|
144
|
+
const index = given != null ? given : (_pluginRecordIndex = pluginRecordIndex('productCategory')) == null ? void 0 : _pluginRecordIndex.index;
|
|
145
|
+
if (!index) return [];
|
|
146
|
+
const { records } = await index.list({
|
|
147
|
+
hostId,
|
|
148
|
+
limit: AI_PRODUCT_CATEGORY_LIST_MAX
|
|
149
|
+
});
|
|
150
|
+
return records.map((record)=>({
|
|
151
|
+
id: record.id,
|
|
152
|
+
name: record.name
|
|
153
|
+
})).sort((a, b)=>a.name.localeCompare(b.name));
|
|
144
154
|
}
|
|
145
|
-
/**
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
+
/**
|
|
156
|
+
* A saved product's facts, as a bulk pass reads them; `null` when it is gone
|
|
157
|
+
* — read through the `product` index the plugin that keeps products publishes
|
|
158
|
+
* (AGL-3080), which answers live, named products only.
|
|
159
|
+
*/ async function readProductFacts(hostId, productId) {
|
|
160
|
+
var _pluginRecordIndex;
|
|
161
|
+
const record = await ((_pluginRecordIndex = pluginRecordIndex('product')) == null ? void 0 : _pluginRecordIndex.index.get({
|
|
162
|
+
hostId,
|
|
163
|
+
id: productId
|
|
164
|
+
}));
|
|
165
|
+
if (!record) return null;
|
|
166
|
+
const facts = record.facts;
|
|
167
|
+
const type = facts['type'];
|
|
168
|
+
const list = (value)=>Array.isArray(value) ? value : [];
|
|
155
169
|
return {
|
|
156
|
-
id:
|
|
157
|
-
name,
|
|
170
|
+
id: record.id,
|
|
171
|
+
name: record.name,
|
|
158
172
|
type: AI_PRODUCT_TYPES.includes(type) ? type : 'physical',
|
|
159
|
-
text: str(
|
|
160
|
-
tags: (
|
|
161
|
-
categoryIds: (
|
|
162
|
-
options: (
|
|
173
|
+
text: str(facts['description']).slice(0, AI_PRODUCT_TEXT_MAX_CHARS),
|
|
174
|
+
tags: list(facts['tags']).map(str).filter(Boolean),
|
|
175
|
+
categoryIds: list(facts['categoryIds']).map(str).filter(Boolean),
|
|
176
|
+
options: list(facts['options']).slice(0, AI_PRODUCT_OPTIONS_MAX).map((option)=>{
|
|
163
177
|
const record = option != null ? option : {};
|
|
164
178
|
return {
|
|
165
179
|
name: str(record['name']),
|
|
166
|
-
values: (
|
|
180
|
+
values: list(record['values']).map(str).filter(Boolean).slice(0, AI_PRODUCT_OPTION_VALUES_MAX)
|
|
167
181
|
};
|
|
168
182
|
}).filter((option)=>option.name),
|
|
169
|
-
imageUrl: str(
|
|
170
|
-
seoTitle: str(
|
|
171
|
-
seoDescription: str(
|
|
183
|
+
imageUrl: str(facts['imageUrl']) || null,
|
|
184
|
+
seoTitle: str(facts['seoTitle']),
|
|
185
|
+
seoDescription: str(facts['seoDescription'])
|
|
172
186
|
};
|
|
173
187
|
}
|
|
174
188
|
function modelOf(context) {
|
|
@@ -188,7 +202,7 @@ function modelOf(context) {
|
|
|
188
202
|
/** Ask for one product's copy, and build its proposal from the answer. */ async function writeProductCopy(context, model, site, product, deps) {
|
|
189
203
|
var _ref;
|
|
190
204
|
const { job, firestore, signal } = context;
|
|
191
|
-
const categories = await readCategories(
|
|
205
|
+
const categories = await readCategories(site.hostId, deps.categoryIndex);
|
|
192
206
|
let photo = 'none';
|
|
193
207
|
let image = null;
|
|
194
208
|
if (product.imageUrl) {
|
|
@@ -284,7 +298,7 @@ function modelOf(context) {
|
|
|
284
298
|
const productId = inputs.productIds[done];
|
|
285
299
|
if (!productId) return aiUnspentOutcome(model);
|
|
286
300
|
const more = done + 1 < inputs.productIds.length;
|
|
287
|
-
const product = await readProductFacts(
|
|
301
|
+
const product = await readProductFacts(site.hostId, productId);
|
|
288
302
|
if (!product) {
|
|
289
303
|
const skipped = {
|
|
290
304
|
kind: 'copy',
|
|
@@ -350,7 +364,7 @@ function modelOf(context) {
|
|
|
350
364
|
]
|
|
351
365
|
});
|
|
352
366
|
}
|
|
353
|
-
const existing = await readCategories(
|
|
367
|
+
const existing = await readCategories(site.hostId, deps.categoryIndex);
|
|
354
368
|
const generation = await generateAiCategories(_extends({
|
|
355
369
|
store: site.name,
|
|
356
370
|
brief: job.brief,
|
|
@@ -387,6 +401,7 @@ export const runAiJobProductsStep = createAiJobProductsStep();
|
|
|
387
401
|
* refusal spends nothing.
|
|
388
402
|
*/ export const aiProductsJobAdmission = async (context)=>{
|
|
389
403
|
var _data;
|
|
404
|
+
var _pluginRecordIndex;
|
|
390
405
|
const inputs = parseAiProductsJobInputs(context.inputs);
|
|
391
406
|
if (typeof inputs === 'string') return {
|
|
392
407
|
status: 400,
|
|
@@ -406,14 +421,21 @@ export const runAiJobProductsStep = createAiJobProductsStep();
|
|
|
406
421
|
status: 403,
|
|
407
422
|
error: AI_PRODUCTS_NOT_ENTITLED_COPY
|
|
408
423
|
};
|
|
424
|
+
// The plugin that keeps products is whichever publishes their index
|
|
425
|
+
// (AGL-3080); none is the same refusal as one switched off.
|
|
426
|
+
const keeper = (_pluginRecordIndex = pluginRecordIndex('product')) == null ? void 0 : _pluginRecordIndex.pluginId;
|
|
427
|
+
if (!keeper) return {
|
|
428
|
+
status: 403,
|
|
429
|
+
error: AI_PRODUCTS_COMMERCE_OFF_COPY
|
|
430
|
+
};
|
|
409
431
|
const host = (_data = (await context.firestore.collection('hosts').doc(context.hostId).get()).data()) != null ? _data : null;
|
|
410
432
|
const released = await filterEnabledPluginsByReleaseFlags([
|
|
411
|
-
|
|
433
|
+
keeper
|
|
412
434
|
], {
|
|
413
435
|
orgId: context.orgId,
|
|
414
436
|
authorization: null
|
|
415
437
|
});
|
|
416
|
-
if (!released.includes(
|
|
438
|
+
if (!released.includes(keeper) || !isHostPluginEnabled(org, host, keeper)) {
|
|
417
439
|
return {
|
|
418
440
|
status: 403,
|
|
419
441
|
error: AI_PRODUCTS_COMMERCE_OFF_COPY
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-products-step.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 { checkEntitlement } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport type { AglynOrgBilling } from '@aglyn/aglyn/foundation/definitions/org-billing.types'\nimport { isHostPluginEnabled } from '@aglyn/aglyn/plugin-manager/enabled-plugins'\nimport { resolveOrgIdForHost } from '@aglyn/tenant-data-admin/server/organizations'\nimport { filterEnabledPluginsByReleaseFlags } from '@aglyn/tenant-data-admin/server/release-flags'\nimport type { AiJob, AiJobOutput } from '../model/ai-jobs.types'\nimport {\n AI_PRODUCT_CATEGORY_LIST_MAX,\n AI_PRODUCT_OPTIONS_MAX,\n AI_PRODUCT_OPTION_VALUES_MAX,\n AI_PRODUCT_TEXT_MAX_CHARS,\n AI_PRODUCT_TYPES,\n AI_PRODUCTS_BULK_MAX,\n AI_PRODUCTS_OUTPUT_IDS,\n aiProductGaps,\n aiProductsProposalOf,\n parseAiProductsJobInputs,\n type AiCatalogProposal,\n type AiCategoriesProposal,\n type AiProductCategory,\n type AiProductCopy,\n type AiProductCopyProposal,\n type AiProductFacts,\n type AiProductPhotoRead,\n type AiProductType,\n} from '../model/ai-products'\nimport { AI_STEP_TIERS } from '../providers/catalog'\nimport { aiModelForStep } from '../providers/routing'\nimport type { AiValidatedGeneration } from '../runtime/ai-doctrine'\nimport {\n AI_CATALOG_MAX_TOKENS,\n AI_PRODUCT_CATEGORIES_MAX_TOKENS,\n AI_PRODUCT_COPY_MAX_TOKENS,\n AI_PRODUCTS_STEP,\n generateAiCatalog,\n generateAiCategories,\n generateAiProductCopy,\n} from '../runtime/ai-products-generation'\nimport { aiRouteReadsImages } from '../runtime/ai-runtime'\nimport { registerAiJobAdmission, type AiJobAdmission } from './ai-job-admission'\nimport { aiJobStepBudget } from './ai-job-budget'\nimport { aiGenerationSpent, aiUnspentOutcome } from './ai-job-generation'\nimport { AI_PRODUCT_IMAGE_READ_MS, readAiProductImage, type AiProductImageSeams } from './ai-product-image'\nimport type { AiJobStepContext, AiJobStepOutcome, AiJobStepRunner } from './ai-job-text-step'\nimport { registerAiJobStep, registerAiJobStepPasses } from './ai-jobs'\n\n/**\n * The `products` step (AGL-2916): commerce by AI, as proposals.\n *\n * `inputs.target` names the work (`model/ai-products.ts`): one product's copy\n * from what its editor holds, the copy of many saved products one a pass, a\n * store's first products from a brief, or its categories and a first set of\n * discounts from a brief.\n *\n * ## It writes nothing\n *\n * Like every runner, this reads, asks and returns. Every output is a\n * `product` proposal, and the commerce plugin's own surfaces write what a\n * person accepts: the product editor's Save, the products card's apply, and\n * the create paths the catalog and discount cards already use — each with the\n * allowance, role and seed checks those paths carry. A proposed product is\n * created as a draft with no price, and a proposed discount is created\n * switched off.\n *\n * ## A bulk job, a product at a time\n *\n * A bulk job's pass writes one product's copy and asks the machine to\n * continue while products remain, so each product is one reservation and one\n * provider exchange, and a job that runs out of credits resumes at the\n * product where it stopped. Which product is next is read from the job's own\n * outputs, one a product, so nothing is kept anywhere else. A product that is\n * gone, or whose copy cannot be held to the rules, is reported and passed\n * over rather than failing the products after it.\n *\n * ## What the model is shown\n *\n * The product's name, type, text, tags, options and current search listing,\n * the names of the site's categories, the store's name, and — for a model that\n * reads pictures — the product's first photo, read from the site's own media\n * library as `ai-product-image.ts` describes. A catalog or categories request\n * carries the brief and the store's name, and the second its existing category\n * names. No price, stock, order, customer or other product is read for any of\n * them.\n */\n\nexport const AI_PRODUCTS_NO_SITE_COPY = 'Open the store this job is for before starting it.'\nexport const AI_PRODUCTS_UNKNOWN_SITE_COPY = 'This AI job names a site this workspace does not have.'\nexport const AI_PRODUCTS_NOT_ENTITLED_COPY = 'Your plan does not include selling products. See Billing.'\nexport const AI_PRODUCTS_COMMERCE_OFF_COPY = 'Turn on Commerce for this site before starting the job.'\nexport const AI_PRODUCTS_GONE_COPY = 'This product is no longer in the catalog.'\nexport const AI_PRODUCTS_DECLINED_COPY = 'The model declined to write copy for this product.'\n\n/** The notes a proposal carries about its product's photo. */\nexport const AI_PRODUCT_PHOTO_NOTES: Readonly<Partial<Record<AiProductPhotoRead, string>>> = {\n 'not-in-library':\n 'The photo was not read: it is not in this site’s media library. The copy is written from the product’s words.',\n unreadable: 'The photo could not be read, so the copy is written from the product’s words.',\n model: 'The photo was not read: the model this job ran on does not read pictures.',\n}\n\n/** The commerce plugin, as `plugins.config.json` names it. */\nconst COMMERCE_PLUGIN_ID = 'commerce'\n\n/**\n * ASSUMED: how long a catalog or categories pass reads the site before it\n * asks: the host document and, for categories, the category names.\n */\nexport const AI_PRODUCTS_SITE_READ_MS = 1_000\n\nconst TIER = AI_STEP_TIERS[AI_PRODUCTS_STEP]\n\n/**\n * One product's copy (AGL-3035): its generation and re-ask at the copy's own\n * ceiling on the tier `job.products` is served from, with the photo read. The\n * least any pass of the step needs, and so what the step registers.\n */\nexport const AI_JOB_PRODUCT_COPY_BUDGET = aiJobStepBudget({\n tier: TIER,\n maxTokens: AI_PRODUCT_COPY_MAX_TOKENS,\n lookups: 0,\n ownReadsMs: AI_PRODUCT_IMAGE_READ_MS,\n})\n\n/** A catalog: the largest answer the step asks for, at the routing table's ceiling. */\nexport const AI_JOB_CATALOG_BUDGET = aiJobStepBudget({\n tier: TIER,\n maxTokens: AI_CATALOG_MAX_TOKENS,\n lookups: 0,\n ownReadsMs: AI_PRODUCTS_SITE_READ_MS,\n})\n\n/** Categories and discounts. */\nexport const AI_JOB_CATEGORIES_BUDGET = aiJobStepBudget({\n tier: TIER,\n maxTokens: AI_PRODUCT_CATEGORIES_MAX_TOKENS,\n lookups: 0,\n ownReadsMs: AI_PRODUCTS_SITE_READ_MS,\n})\n\n/** What the `products` step registers: the least any of its passes needs. */\nexport const AI_JOB_PRODUCTS_STEP_BUDGET = AI_JOB_PRODUCT_COPY_BUDGET\n\n/** The least time the next pass of a job needs, by what the job asks for. */\nexport function aiProductsRunMinimumMs(job: Pick<AiJob, 'inputs'>): number {\n const inputs = parseAiProductsJobInputs(job.inputs)\n if (typeof inputs === 'string') return AI_JOB_PRODUCT_COPY_BUDGET.minimumMs\n if (inputs.target === 'catalog') return AI_JOB_CATALOG_BUDGET.minimumMs\n if (inputs.target === 'categories') return AI_JOB_CATEGORIES_BUDGET.minimumMs\n return AI_JOB_PRODUCT_COPY_BUDGET.minimumMs\n}\n\ntype Firestore = FirebaseFirestore.Firestore\n\ninterface StoreSite {\n hostId: string\n subdomain: string | null\n /** The store's name, as its visitors read it. */\n name: string\n}\n\nconst str = (value: unknown): string => (typeof value === 'string' ? value.trim() : '')\n\n/** Through JSON, so no `undefined` reaches the job document's write. */\nconst plain = <T>(value: T): Record<string, unknown> =>\n JSON.parse(JSON.stringify(value)) as Record<string, unknown>\n\n/** The site a job names, read and checked against the job's org. */\nasync function loadStore(firestore: Firestore, job: AiJob): Promise<StoreSite | { failure: string }> {\n if (!job.hostId) return { failure: AI_PRODUCTS_NO_SITE_COPY }\n const snapshot = await firestore.collection('hosts').doc(job.hostId).get()\n const host = snapshot.exists ? (snapshot.data() as Record<string, unknown>) : null\n // The door checks the caller's membership of the org the job is metered\n // against; a site of another org is refused here, before anything of it\n // reaches a prompt.\n if (!host || host['orgId'] !== job.orgId) return { failure: AI_PRODUCTS_UNKNOWN_SITE_COPY }\n const seo = (host['seo'] ?? {}) as Record<string, unknown>\n const subdomain = str(host['subdomain']) || null\n return {\n hostId: job.hostId,\n subdomain,\n name: str(seo['title']) || str(host['displayName']) || subdomain || '',\n }\n}\n\n/** The site's categories by name, as many as a request lists. */\nasync function readCategories(firestore: Firestore, hostId: string): Promise<AiProductCategory[]> {\n const snapshot = await firestore\n .collection('hosts')\n .doc(hostId)\n .collection('productCategories')\n .select('name')\n .limit(AI_PRODUCT_CATEGORY_LIST_MAX)\n .get()\n return snapshot.docs\n .map((doc) => ({ id: doc.id, name: str(doc.get('name')) }))\n .filter((category) => category.name)\n .sort((a, b) => a.name.localeCompare(b.name))\n}\n\n/** A saved product's facts, as a bulk pass reads them; `null` when it is gone. */\nasync function readProductFacts(firestore: Firestore, hostId: string, productId: string): Promise<AiProductFacts | null> {\n const snapshot = await firestore.collection('hosts').doc(hostId).collection('products').doc(productId).get()\n const data = snapshot.exists ? (snapshot.data() as Record<string, unknown>) : null\n if (!data || data['deletedAt']) return null\n const name = str(data['name'])\n if (!name) return null\n const type = data['type']\n const seo = (data['seo'] ?? {}) as Record<string, unknown>\n const mediaUrls = Array.isArray(data['mediaUrls']) ? data['mediaUrls'] : []\n return {\n id: productId,\n name,\n type: (AI_PRODUCT_TYPES as readonly unknown[]).includes(type) ? (type as AiProductType) : 'physical',\n text: str(data['description']).slice(0, AI_PRODUCT_TEXT_MAX_CHARS),\n tags: (Array.isArray(data['tags']) ? data['tags'] : []).map(str).filter(Boolean),\n categoryIds: (Array.isArray(data['categoryIds']) ? data['categoryIds'] : []).map(str).filter(Boolean),\n options: (Array.isArray(data['options']) ? data['options'] : [])\n .slice(0, AI_PRODUCT_OPTIONS_MAX)\n .map((option) => {\n const record = (option ?? {}) as Record<string, unknown>\n return {\n name: str(record['name']),\n values: (Array.isArray(record['values']) ? record['values'] : [])\n .map(str)\n .filter(Boolean)\n .slice(0, AI_PRODUCT_OPTION_VALUES_MAX),\n }\n })\n .filter((option) => option.name),\n imageUrl: str(mediaUrls[0]) || str(data['imageUrl']) || null,\n seoTitle: str(seo['title']),\n seoDescription: str(seo['description']),\n }\n}\n\nfunction modelOf(context: AiJobStepContext): string {\n return context.modelFor?.(AI_PRODUCTS_STEP) ?? aiModelForStep(AI_PRODUCTS_STEP)\n}\n\n/** An output addressed to the site, so its link opens the site's products. */\nfunction output(site: StoreSite, id: string, label: string, proposal: object): AiJobOutput {\n return {\n resource: 'product',\n id,\n hostId: site.hostId,\n hostSubdomain: site.subdomain,\n label,\n proposal: plain(proposal),\n }\n}\n\nexport interface AiJobProductsStepDeps {\n image?: AiProductImageSeams\n /** Whether a model reads pictures; the provider's own descriptor otherwise. */\n readsImages?: (model: string) => boolean\n}\n\ninterface CopyRun {\n generation: AiValidatedGeneration<AiProductCopy>\n proposal: AiProductCopyProposal\n}\n\n/** Ask for one product's copy, and build its proposal from the answer. */\nasync function writeProductCopy(\n context: AiJobStepContext & { firestore: Firestore },\n model: string,\n site: StoreSite,\n product: AiProductFacts,\n deps: AiJobProductsStepDeps,\n): Promise<CopyRun> {\n const { job, firestore, signal } = context\n const categories = await readCategories(firestore, site.hostId)\n let photo: AiProductPhotoRead = 'none'\n let image = null\n if (product.imageUrl) {\n if ((deps.readsImages ?? aiRouteReadsImages)(model)) {\n const read = await readAiProductImage(\n firestore,\n { value: product.imageUrl, hostId: site.hostId, orgId: job.orgId },\n deps.image,\n )\n photo = read.status\n image = read.status === 'read' ? read.image : null\n } else {\n photo = 'model'\n }\n }\n const generation = await generateAiProductCopy({\n store: site.name,\n product,\n categories,\n photo: Boolean(image),\n image,\n model,\n maxTokens: AI_JOB_PRODUCT_COPY_BUDGET.maxTokens(model),\n ...(signal ? { signal } : {}),\n })\n const values = generation.status === 'ok' ? generation.value : null\n const names = new Map(categories.map((category) => [category.id, category.name]))\n const note = AI_PRODUCT_PHOTO_NOTES[photo]\n return {\n generation,\n proposal: {\n kind: 'copy',\n product: { id: product.id, name: product.name },\n values,\n categories: (values?.categoryIds ?? []).map((id) => ({ id, name: names.get(id) ?? id })),\n optionNamesBefore: product.options.map((option) => option.name),\n gaps: values ? aiProductGaps(values.description) : [],\n photo,\n skipped: null,\n notes: note ? [note] : [],\n },\n }\n}\n\n/** The step, with its reads replaceable where a spec needs them. */\nexport function createAiJobProductsStep(deps: AiJobProductsStepDeps = {}): AiJobStepRunner {\n return async (context) => {\n const { job, firestore, signal } = context\n const model = modelOf(context)\n const inputs = parseAiProductsJobInputs(job.inputs)\n if (typeof inputs === 'string') return { ...aiUnspentOutcome(model), failure: inputs }\n const site = await loadStore(firestore, job)\n if ('failure' in site) return { ...aiUnspentOutcome(model), failure: site.failure }\n\n if (inputs.target === 'product') {\n const { generation, proposal } = await writeProductCopy(context, model, site, inputs.product, deps)\n const spent = aiGenerationSpent(generation)\n if (generation.status === 'refused') return { ...spent, refused: true }\n if (generation.status === 'needs_input') return { ...spent, failure: generation.message }\n return {\n ...spent,\n outputs: [output(site, AI_PRODUCTS_OUTPUT_IDS.copy(inputs.product.id), `Product copy · ${inputs.product.name}`, proposal)],\n }\n }\n\n if (inputs.target === 'bulk') {\n const done = (job.outputs ?? []).filter((entry) => aiProductsProposalOf(entry)?.kind === 'copy').length\n const productId = inputs.productIds[done]\n if (!productId) return aiUnspentOutcome(model)\n const more = done + 1 < inputs.productIds.length\n const product = await readProductFacts(firestore, site.hostId, productId)\n if (!product) {\n const skipped: AiProductCopyProposal = {\n kind: 'copy',\n product: { id: productId, name: productId },\n values: null,\n categories: [],\n optionNamesBefore: [],\n gaps: [],\n photo: 'none',\n skipped: AI_PRODUCTS_GONE_COPY,\n notes: [],\n }\n return {\n ...aiUnspentOutcome(model, {\n outputs: [output(site, AI_PRODUCTS_OUTPUT_IDS.copy(productId), `Product copy · ${productId}`, skipped)],\n }),\n continue: more,\n }\n }\n const { generation, proposal } = await writeProductCopy(context, model, site, product, deps)\n const spent = aiGenerationSpent(generation)\n // One product the rules could not hold, or the model declined, is\n // reported and passed over: its tokens are metered with the pass, and\n // the products after it still get their copy.\n const reported: AiProductCopyProposal =\n generation.status === 'ok'\n ? proposal\n : {\n ...proposal,\n skipped: generation.status === 'needs_input' ? generation.message : AI_PRODUCTS_DECLINED_COPY,\n }\n return {\n ...spent,\n outputs: [output(site, AI_PRODUCTS_OUTPUT_IDS.copy(productId), `Product copy · ${product.name}`, reported)],\n continue: more,\n }\n }\n\n if (inputs.target === 'catalog') {\n const generation = await generateAiCatalog({\n store: site.name,\n brief: job.brief,\n model,\n maxTokens: AI_JOB_CATALOG_BUDGET.maxTokens(model),\n ...(signal ? { signal } : {}),\n })\n const spent = aiGenerationSpent(generation)\n if (generation.status === 'refused') return { ...spent, refused: true }\n if (generation.status === 'needs_input') return { ...spent, failure: generation.message }\n const proposal: AiCatalogProposal = { kind: 'catalog', products: generation.value, notes: [] }\n return {\n ...spent,\n outputs: [\n output(\n site,\n AI_PRODUCTS_OUTPUT_IDS.catalog,\n `Proposed products · ${generation.value.length}`,\n proposal,\n ),\n ],\n }\n }\n\n const existing = await readCategories(firestore, site.hostId)\n const generation = await generateAiCategories({\n store: site.name,\n brief: job.brief,\n existingCategoryNames: existing.map((category) => category.name),\n model,\n maxTokens: AI_JOB_CATEGORIES_BUDGET.maxTokens(model),\n ...(signal ? { signal } : {}),\n })\n const spent = aiGenerationSpent(generation)\n if (generation.status === 'refused') return { ...spent, refused: true }\n if (generation.status === 'needs_input') return { ...spent, failure: generation.message }\n const proposal: AiCategoriesProposal = { kind: 'categories', ...generation.value, notes: [] }\n return {\n ...spent,\n outputs: [output(site, AI_PRODUCTS_OUTPUT_IDS.categories, 'Proposed categories and discounts', proposal)],\n }\n }\n}\n\nexport const runAiJobProductsStep = createAiJobProductsStep()\n\n/**\n * Whether a `products` job may start: inputs it can run, a site of the job's\n * own org, a plan that sells products, and Commerce on for the site and past\n * its release flag. Asked by the create door before the job exists, so a\n * refusal spends nothing.\n */\nexport const aiProductsJobAdmission: AiJobAdmission = async (context) => {\n const inputs = parseAiProductsJobInputs(context.inputs)\n if (typeof inputs === 'string') return { status: 400, error: inputs }\n if (!context.hostId) return { status: 400, error: AI_PRODUCTS_NO_SITE_COPY }\n const owner = await resolveOrgIdForHost(context.hostId)\n if (!owner || owner !== context.orgId) return { status: 404, error: AI_PRODUCTS_UNKNOWN_SITE_COPY }\n const org = context.org as (Partial<AglynOrgBilling> & { enabledPlugins?: string[] }) | null\n if (!checkEntitlement(org, 'commerce')) return { status: 403, error: AI_PRODUCTS_NOT_ENTITLED_COPY }\n const host = (await context.firestore.collection('hosts').doc(context.hostId).get()).data() ?? null\n const released = await filterEnabledPluginsByReleaseFlags([COMMERCE_PLUGIN_ID], {\n orgId: context.orgId,\n authorization: null,\n })\n if (!released.includes(COMMERCE_PLUGIN_ID) || !isHostPluginEnabled(org, host, COMMERCE_PLUGIN_ID)) {\n return { status: 403, error: AI_PRODUCTS_COMMERCE_OFF_COPY }\n }\n return null\n}\n\n/**\n * Registers the `products` step, the least time each of its passes needs, its\n * pass bound and its admission.\n */\nexport function registerAiProductsJob(): void {\n registerAiJobStep('products', runAiJobProductsStep, {\n minimumMs: AI_JOB_PRODUCTS_STEP_BUDGET.minimumMs,\n minimumMsFor: aiProductsRunMinimumMs,\n })\n registerAiJobStepPasses('products', AI_PRODUCTS_BULK_MAX)\n registerAiJobAdmission('products', aiProductsJobAdmission)\n}\n"],"names":["checkEntitlement","isHostPluginEnabled","resolveOrgIdForHost","filterEnabledPluginsByReleaseFlags","AI_PRODUCT_CATEGORY_LIST_MAX","AI_PRODUCT_OPTIONS_MAX","AI_PRODUCT_OPTION_VALUES_MAX","AI_PRODUCT_TEXT_MAX_CHARS","AI_PRODUCT_TYPES","AI_PRODUCTS_BULK_MAX","AI_PRODUCTS_OUTPUT_IDS","aiProductGaps","aiProductsProposalOf","parseAiProductsJobInputs","AI_STEP_TIERS","aiModelForStep","AI_CATALOG_MAX_TOKENS","AI_PRODUCT_CATEGORIES_MAX_TOKENS","AI_PRODUCT_COPY_MAX_TOKENS","AI_PRODUCTS_STEP","generateAiCatalog","generateAiCategories","generateAiProductCopy","aiRouteReadsImages","registerAiJobAdmission","aiJobStepBudget","aiGenerationSpent","aiUnspentOutcome","AI_PRODUCT_IMAGE_READ_MS","readAiProductImage","registerAiJobStep","registerAiJobStepPasses","AI_PRODUCTS_NO_SITE_COPY","AI_PRODUCTS_UNKNOWN_SITE_COPY","AI_PRODUCTS_NOT_ENTITLED_COPY","AI_PRODUCTS_COMMERCE_OFF_COPY","AI_PRODUCTS_GONE_COPY","AI_PRODUCTS_DECLINED_COPY","AI_PRODUCT_PHOTO_NOTES","unreadable","model","COMMERCE_PLUGIN_ID","AI_PRODUCTS_SITE_READ_MS","TIER","AI_JOB_PRODUCT_COPY_BUDGET","tier","maxTokens","lookups","ownReadsMs","AI_JOB_CATALOG_BUDGET","AI_JOB_CATEGORIES_BUDGET","AI_JOB_PRODUCTS_STEP_BUDGET","aiProductsRunMinimumMs","job","inputs","minimumMs","target","str","value","trim","plain","JSON","parse","stringify","loadStore","firestore","host","hostId","failure","snapshot","collection","doc","get","exists","data","orgId","seo","subdomain","name","readCategories","select","limit","docs","map","id","filter","category","sort","a","b","localeCompare","readProductFacts","productId","type","mediaUrls","Array","isArray","includes","text","slice","tags","Boolean","categoryIds","options","option","record","values","imageUrl","seoTitle","seoDescription","modelOf","context","modelFor","output","site","label","proposal","resource","hostSubdomain","writeProductCopy","product","deps","signal","categories","photo","image","readsImages","read","status","generation","store","names","Map","note","kind","optionNamesBefore","gaps","description","skipped","notes","createAiJobProductsStep","spent","refused","message","outputs","copy","done","entry","length","productIds","more","continue","reported","brief","products","catalog","existing","existingCategoryNames","runAiJobProductsStep","aiProductsJobAdmission","error","owner","org","released","authorization","registerAiProductsJob","minimumMsFor"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-products-step.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 { checkEntitlement } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport type { AglynOrgBilling } from '@aglyn/aglyn/foundation/definitions/org-billing.types'\nimport {\n pluginRecordIndex,\n type PluginRecordIndex,\n} from '@aglyn/aglyn/plugin-manager/plugin-record-index'\nimport { isHostPluginEnabled } from '@aglyn/aglyn/plugin-manager/enabled-plugins'\nimport { resolveOrgIdForHost } from '@aglyn/tenant-data-admin/server/organizations'\nimport { filterEnabledPluginsByReleaseFlags } from '@aglyn/tenant-data-admin/server/release-flags'\nimport type { AiJob, AiJobOutput } from '../model/ai-jobs.types'\nimport {\n AI_PRODUCT_CATEGORY_LIST_MAX,\n AI_PRODUCT_OPTIONS_MAX,\n AI_PRODUCT_OPTION_VALUES_MAX,\n AI_PRODUCT_TEXT_MAX_CHARS,\n AI_PRODUCT_TYPES,\n AI_PRODUCTS_BULK_MAX,\n AI_PRODUCTS_OUTPUT_IDS,\n aiProductGaps,\n aiProductsProposalOf,\n parseAiProductsJobInputs,\n type AiCatalogProposal,\n type AiCategoriesProposal,\n type AiProductCategory,\n type AiProductCopy,\n type AiProductCopyProposal,\n type AiProductFacts,\n type AiProductPhotoRead,\n type AiProductType,\n} from '../model/ai-products'\nimport { AI_STEP_TIERS } from '../providers/catalog'\nimport { aiModelForStep } from '../providers/routing'\nimport type { AiValidatedGeneration } from '../runtime/ai-doctrine'\nimport {\n AI_CATALOG_MAX_TOKENS,\n AI_PRODUCT_CATEGORIES_MAX_TOKENS,\n AI_PRODUCT_COPY_MAX_TOKENS,\n AI_PRODUCTS_STEP,\n generateAiCatalog,\n generateAiCategories,\n generateAiProductCopy,\n} from '../runtime/ai-products-generation'\nimport { aiRouteReadsImages } from '../runtime/ai-runtime'\nimport { registerAiJobAdmission, type AiJobAdmission } from './ai-job-admission'\nimport { aiJobStepBudget } from './ai-job-budget'\nimport { aiGenerationSpent, aiUnspentOutcome } from './ai-job-generation'\nimport { AI_PRODUCT_IMAGE_READ_MS, readAiProductImage, type AiProductImageSeams } from './ai-product-image'\nimport type { AiJobStepContext, AiJobStepOutcome, AiJobStepRunner } from './ai-job-text-step'\nimport { registerAiJobStep, registerAiJobStepPasses } from './ai-jobs'\n\n/**\n * The `products` step (AGL-2916): commerce by AI, as proposals.\n *\n * `inputs.target` names the work (`model/ai-products.ts`): one product's copy\n * from what its editor holds, the copy of many saved products one a pass, a\n * store's first products from a brief, or its categories and a first set of\n * discounts from a brief.\n *\n * ## It writes nothing\n *\n * Like every runner, this reads, asks and returns. Every output is a\n * `product` proposal, and the commerce plugin's own surfaces write what a\n * person accepts: the product editor's Save, the products card's apply, and\n * the create paths the catalog and discount cards already use — each with the\n * allowance, role and seed checks those paths carry. A proposed product is\n * created as a draft with no price, and a proposed discount is created\n * switched off.\n *\n * ## A bulk job, a product at a time\n *\n * A bulk job's pass writes one product's copy and asks the machine to\n * continue while products remain, so each product is one reservation and one\n * provider exchange, and a job that runs out of credits resumes at the\n * product where it stopped. Which product is next is read from the job's own\n * outputs, one a product, so nothing is kept anywhere else. A product that is\n * gone, or whose copy cannot be held to the rules, is reported and passed\n * over rather than failing the products after it.\n *\n * ## What the model is shown\n *\n * The product's name, type, text, tags, options and current search listing,\n * the names of the site's categories, the store's name, and — for a model that\n * reads pictures — the product's first photo, read from the site's own media\n * library as `ai-product-image.ts` describes. A catalog or categories request\n * carries the brief and the store's name, and the second its existing category\n * names. No price, stock, order, customer or other product is read for any of\n * them.\n */\n\nexport const AI_PRODUCTS_NO_SITE_COPY = 'Open the store this job is for before starting it.'\nexport const AI_PRODUCTS_UNKNOWN_SITE_COPY = 'This AI job names a site this workspace does not have.'\nexport const AI_PRODUCTS_NOT_ENTITLED_COPY = 'Your plan does not include selling products. See Billing.'\nexport const AI_PRODUCTS_COMMERCE_OFF_COPY = 'Turn on Commerce for this site before starting the job.'\nexport const AI_PRODUCTS_GONE_COPY = 'This product is no longer in the catalog.'\nexport const AI_PRODUCTS_DECLINED_COPY = 'The model declined to write copy for this product.'\n\n/** The notes a proposal carries about its product's photo. */\nexport const AI_PRODUCT_PHOTO_NOTES: Readonly<Partial<Record<AiProductPhotoRead, string>>> = {\n 'not-in-library':\n 'The photo was not read: it is not in this site’s media library. The copy is written from the product’s words.',\n unreadable: 'The photo could not be read, so the copy is written from the product’s words.',\n model: 'The photo was not read: the model this job ran on does not read pictures.',\n}\n\n/**\n * ASSUMED: how long a catalog or categories pass reads the site before it\n * asks: the host document and, for categories, the category names.\n */\nexport const AI_PRODUCTS_SITE_READ_MS = 1_000\n\nconst TIER = AI_STEP_TIERS[AI_PRODUCTS_STEP]\n\n/**\n * One product's copy (AGL-3035): its generation and re-ask at the copy's own\n * ceiling on the tier `job.products` is served from, with the photo read. The\n * least any pass of the step needs, and so what the step registers.\n */\nexport const AI_JOB_PRODUCT_COPY_BUDGET = aiJobStepBudget({\n tier: TIER,\n maxTokens: AI_PRODUCT_COPY_MAX_TOKENS,\n lookups: 0,\n ownReadsMs: AI_PRODUCT_IMAGE_READ_MS,\n})\n\n/** A catalog: the largest answer the step asks for, at the routing table's ceiling. */\nexport const AI_JOB_CATALOG_BUDGET = aiJobStepBudget({\n tier: TIER,\n maxTokens: AI_CATALOG_MAX_TOKENS,\n lookups: 0,\n ownReadsMs: AI_PRODUCTS_SITE_READ_MS,\n})\n\n/** Categories and discounts. */\nexport const AI_JOB_CATEGORIES_BUDGET = aiJobStepBudget({\n tier: TIER,\n maxTokens: AI_PRODUCT_CATEGORIES_MAX_TOKENS,\n lookups: 0,\n ownReadsMs: AI_PRODUCTS_SITE_READ_MS,\n})\n\n/** What the `products` step registers: the least any of its passes needs. */\nexport const AI_JOB_PRODUCTS_STEP_BUDGET = AI_JOB_PRODUCT_COPY_BUDGET\n\n/** The least time the next pass of a job needs, by what the job asks for. */\nexport function aiProductsRunMinimumMs(job: Pick<AiJob, 'inputs'>): number {\n const inputs = parseAiProductsJobInputs(job.inputs)\n if (typeof inputs === 'string') return AI_JOB_PRODUCT_COPY_BUDGET.minimumMs\n if (inputs.target === 'catalog') return AI_JOB_CATALOG_BUDGET.minimumMs\n if (inputs.target === 'categories') return AI_JOB_CATEGORIES_BUDGET.minimumMs\n return AI_JOB_PRODUCT_COPY_BUDGET.minimumMs\n}\n\ntype Firestore = FirebaseFirestore.Firestore\n\ninterface StoreSite {\n hostId: string\n subdomain: string | null\n /** The store's name, as its visitors read it. */\n name: string\n}\n\nconst str = (value: unknown): string => (typeof value === 'string' ? value.trim() : '')\n\n/** Through JSON, so no `undefined` reaches the job document's write. */\nconst plain = <T>(value: T): Record<string, unknown> =>\n JSON.parse(JSON.stringify(value)) as Record<string, unknown>\n\n/** The site a job names, read and checked against the job's org. */\nasync function loadStore(firestore: Firestore, job: AiJob): Promise<StoreSite | { failure: string }> {\n if (!job.hostId) return { failure: AI_PRODUCTS_NO_SITE_COPY }\n const snapshot = await firestore.collection('hosts').doc(job.hostId).get()\n const host = snapshot.exists ? (snapshot.data() as Record<string, unknown>) : null\n // The door checks the caller's membership of the org the job is metered\n // against; a site of another org is refused here, before anything of it\n // reaches a prompt.\n if (!host || host['orgId'] !== job.orgId) return { failure: AI_PRODUCTS_UNKNOWN_SITE_COPY }\n const seo = (host['seo'] ?? {}) as Record<string, unknown>\n const subdomain = str(host['subdomain']) || null\n return {\n hostId: job.hostId,\n subdomain,\n name: str(seo['title']) || str(host['displayName']) || subdomain || '',\n }\n}\n\n/**\n * The site's categories by name, as many as a request lists — from the\n * `productCategory` index the plugin that keeps them publishes (AGL-3080).\n * None when no plugin keeps categories here.\n */\nasync function readCategories(\n hostId: string,\n given?: PluginRecordIndex | null,\n): Promise<AiProductCategory[]> {\n const index = given ?? pluginRecordIndex('productCategory')?.index\n if (!index) return []\n const { records } = await index.list({ hostId, limit: AI_PRODUCT_CATEGORY_LIST_MAX })\n return records\n .map((record) => ({ id: record.id, name: record.name }))\n .sort((a, b) => a.name.localeCompare(b.name))\n}\n\n/**\n * A saved product's facts, as a bulk pass reads them; `null` when it is gone\n * — read through the `product` index the plugin that keeps products publishes\n * (AGL-3080), which answers live, named products only.\n */\nasync function readProductFacts(hostId: string, productId: string): Promise<AiProductFacts | null> {\n const record = await pluginRecordIndex('product')?.index.get({ hostId, id: productId })\n if (!record) return null\n const facts = record.facts\n const type = facts['type']\n const list = (value: unknown): unknown[] => (Array.isArray(value) ? value : [])\n return {\n id: record.id,\n name: record.name,\n type: (AI_PRODUCT_TYPES as readonly unknown[]).includes(type) ? (type as AiProductType) : 'physical',\n text: str(facts['description']).slice(0, AI_PRODUCT_TEXT_MAX_CHARS),\n tags: list(facts['tags']).map(str).filter(Boolean),\n categoryIds: list(facts['categoryIds']).map(str).filter(Boolean),\n options: list(facts['options'])\n .slice(0, AI_PRODUCT_OPTIONS_MAX)\n .map((option) => {\n const record = (option ?? {}) as Record<string, unknown>\n return {\n name: str(record['name']),\n values: list(record['values']).map(str).filter(Boolean).slice(0, AI_PRODUCT_OPTION_VALUES_MAX),\n }\n })\n .filter((option) => option.name),\n imageUrl: str(facts['imageUrl']) || null,\n seoTitle: str(facts['seoTitle']),\n seoDescription: str(facts['seoDescription']),\n }\n}\n\nfunction modelOf(context: AiJobStepContext): string {\n return context.modelFor?.(AI_PRODUCTS_STEP) ?? aiModelForStep(AI_PRODUCTS_STEP)\n}\n\n/** An output addressed to the site, so its link opens the site's products. */\nfunction output(site: StoreSite, id: string, label: string, proposal: object): AiJobOutput {\n return {\n resource: 'product',\n id,\n hostId: site.hostId,\n hostSubdomain: site.subdomain,\n label,\n proposal: plain(proposal),\n }\n}\n\nexport interface AiJobProductsStepDeps {\n image?: AiProductImageSeams\n /** Whether a model reads pictures; the provider's own descriptor otherwise. */\n readsImages?: (model: string) => boolean\n /**\n * Where the site's categories are read; the `productCategory` index the\n * plugin that keeps them publishes otherwise. The eval harness hands its\n * own, over the categories a case names.\n */\n categoryIndex?: PluginRecordIndex | null\n}\n\ninterface CopyRun {\n generation: AiValidatedGeneration<AiProductCopy>\n proposal: AiProductCopyProposal\n}\n\n/** Ask for one product's copy, and build its proposal from the answer. */\nasync function writeProductCopy(\n context: AiJobStepContext & { firestore: Firestore },\n model: string,\n site: StoreSite,\n product: AiProductFacts,\n deps: AiJobProductsStepDeps,\n): Promise<CopyRun> {\n const { job, firestore, signal } = context\n const categories = await readCategories(site.hostId, deps.categoryIndex)\n let photo: AiProductPhotoRead = 'none'\n let image = null\n if (product.imageUrl) {\n if ((deps.readsImages ?? aiRouteReadsImages)(model)) {\n const read = await readAiProductImage(\n firestore,\n { value: product.imageUrl, hostId: site.hostId, orgId: job.orgId },\n deps.image,\n )\n photo = read.status\n image = read.status === 'read' ? read.image : null\n } else {\n photo = 'model'\n }\n }\n const generation = await generateAiProductCopy({\n store: site.name,\n product,\n categories,\n photo: Boolean(image),\n image,\n model,\n maxTokens: AI_JOB_PRODUCT_COPY_BUDGET.maxTokens(model),\n ...(signal ? { signal } : {}),\n })\n const values = generation.status === 'ok' ? generation.value : null\n const names = new Map(categories.map((category) => [category.id, category.name]))\n const note = AI_PRODUCT_PHOTO_NOTES[photo]\n return {\n generation,\n proposal: {\n kind: 'copy',\n product: { id: product.id, name: product.name },\n values,\n categories: (values?.categoryIds ?? []).map((id) => ({ id, name: names.get(id) ?? id })),\n optionNamesBefore: product.options.map((option) => option.name),\n gaps: values ? aiProductGaps(values.description) : [],\n photo,\n skipped: null,\n notes: note ? [note] : [],\n },\n }\n}\n\n/** The step, with its reads replaceable where a spec needs them. */\nexport function createAiJobProductsStep(deps: AiJobProductsStepDeps = {}): AiJobStepRunner {\n return async (context) => {\n const { job, firestore, signal } = context\n const model = modelOf(context)\n const inputs = parseAiProductsJobInputs(job.inputs)\n if (typeof inputs === 'string') return { ...aiUnspentOutcome(model), failure: inputs }\n const site = await loadStore(firestore, job)\n if ('failure' in site) return { ...aiUnspentOutcome(model), failure: site.failure }\n\n if (inputs.target === 'product') {\n const { generation, proposal } = await writeProductCopy(context, model, site, inputs.product, deps)\n const spent = aiGenerationSpent(generation)\n if (generation.status === 'refused') return { ...spent, refused: true }\n if (generation.status === 'needs_input') return { ...spent, failure: generation.message }\n return {\n ...spent,\n outputs: [output(site, AI_PRODUCTS_OUTPUT_IDS.copy(inputs.product.id), `Product copy · ${inputs.product.name}`, proposal)],\n }\n }\n\n if (inputs.target === 'bulk') {\n const done = (job.outputs ?? []).filter((entry) => aiProductsProposalOf(entry)?.kind === 'copy').length\n const productId = inputs.productIds[done]\n if (!productId) return aiUnspentOutcome(model)\n const more = done + 1 < inputs.productIds.length\n const product = await readProductFacts(site.hostId, productId)\n if (!product) {\n const skipped: AiProductCopyProposal = {\n kind: 'copy',\n product: { id: productId, name: productId },\n values: null,\n categories: [],\n optionNamesBefore: [],\n gaps: [],\n photo: 'none',\n skipped: AI_PRODUCTS_GONE_COPY,\n notes: [],\n }\n return {\n ...aiUnspentOutcome(model, {\n outputs: [output(site, AI_PRODUCTS_OUTPUT_IDS.copy(productId), `Product copy · ${productId}`, skipped)],\n }),\n continue: more,\n }\n }\n const { generation, proposal } = await writeProductCopy(context, model, site, product, deps)\n const spent = aiGenerationSpent(generation)\n // One product the rules could not hold, or the model declined, is\n // reported and passed over: its tokens are metered with the pass, and\n // the products after it still get their copy.\n const reported: AiProductCopyProposal =\n generation.status === 'ok'\n ? proposal\n : {\n ...proposal,\n skipped: generation.status === 'needs_input' ? generation.message : AI_PRODUCTS_DECLINED_COPY,\n }\n return {\n ...spent,\n outputs: [output(site, AI_PRODUCTS_OUTPUT_IDS.copy(productId), `Product copy · ${product.name}`, reported)],\n continue: more,\n }\n }\n\n if (inputs.target === 'catalog') {\n const generation = await generateAiCatalog({\n store: site.name,\n brief: job.brief,\n model,\n maxTokens: AI_JOB_CATALOG_BUDGET.maxTokens(model),\n ...(signal ? { signal } : {}),\n })\n const spent = aiGenerationSpent(generation)\n if (generation.status === 'refused') return { ...spent, refused: true }\n if (generation.status === 'needs_input') return { ...spent, failure: generation.message }\n const proposal: AiCatalogProposal = { kind: 'catalog', products: generation.value, notes: [] }\n return {\n ...spent,\n outputs: [\n output(\n site,\n AI_PRODUCTS_OUTPUT_IDS.catalog,\n `Proposed products · ${generation.value.length}`,\n proposal,\n ),\n ],\n }\n }\n\n const existing = await readCategories(site.hostId, deps.categoryIndex)\n const generation = await generateAiCategories({\n store: site.name,\n brief: job.brief,\n existingCategoryNames: existing.map((category) => category.name),\n model,\n maxTokens: AI_JOB_CATEGORIES_BUDGET.maxTokens(model),\n ...(signal ? { signal } : {}),\n })\n const spent = aiGenerationSpent(generation)\n if (generation.status === 'refused') return { ...spent, refused: true }\n if (generation.status === 'needs_input') return { ...spent, failure: generation.message }\n const proposal: AiCategoriesProposal = { kind: 'categories', ...generation.value, notes: [] }\n return {\n ...spent,\n outputs: [output(site, AI_PRODUCTS_OUTPUT_IDS.categories, 'Proposed categories and discounts', proposal)],\n }\n }\n}\n\nexport const runAiJobProductsStep = createAiJobProductsStep()\n\n/**\n * Whether a `products` job may start: inputs it can run, a site of the job's\n * own org, a plan that sells products, and Commerce on for the site and past\n * its release flag. Asked by the create door before the job exists, so a\n * refusal spends nothing.\n */\nexport const aiProductsJobAdmission: AiJobAdmission = async (context) => {\n const inputs = parseAiProductsJobInputs(context.inputs)\n if (typeof inputs === 'string') return { status: 400, error: inputs }\n if (!context.hostId) return { status: 400, error: AI_PRODUCTS_NO_SITE_COPY }\n const owner = await resolveOrgIdForHost(context.hostId)\n if (!owner || owner !== context.orgId) return { status: 404, error: AI_PRODUCTS_UNKNOWN_SITE_COPY }\n const org = context.org as (Partial<AglynOrgBilling> & { enabledPlugins?: string[] }) | null\n if (!checkEntitlement(org, 'commerce')) return { status: 403, error: AI_PRODUCTS_NOT_ENTITLED_COPY }\n // The plugin that keeps products is whichever publishes their index\n // (AGL-3080); none is the same refusal as one switched off.\n const keeper = pluginRecordIndex('product')?.pluginId\n if (!keeper) return { status: 403, error: AI_PRODUCTS_COMMERCE_OFF_COPY }\n const host = (await context.firestore.collection('hosts').doc(context.hostId).get()).data() ?? null\n const released = await filterEnabledPluginsByReleaseFlags([keeper], {\n orgId: context.orgId,\n authorization: null,\n })\n if (!released.includes(keeper) || !isHostPluginEnabled(org, host, keeper)) {\n return { status: 403, error: AI_PRODUCTS_COMMERCE_OFF_COPY }\n }\n return null\n}\n\n/**\n * Registers the `products` step, the least time each of its passes needs, its\n * pass bound and its admission.\n */\nexport function registerAiProductsJob(): void {\n registerAiJobStep('products', runAiJobProductsStep, {\n minimumMs: AI_JOB_PRODUCTS_STEP_BUDGET.minimumMs,\n minimumMsFor: aiProductsRunMinimumMs,\n })\n registerAiJobStepPasses('products', AI_PRODUCTS_BULK_MAX)\n registerAiJobAdmission('products', aiProductsJobAdmission)\n}\n"],"names":["checkEntitlement","pluginRecordIndex","isHostPluginEnabled","resolveOrgIdForHost","filterEnabledPluginsByReleaseFlags","AI_PRODUCT_CATEGORY_LIST_MAX","AI_PRODUCT_OPTIONS_MAX","AI_PRODUCT_OPTION_VALUES_MAX","AI_PRODUCT_TEXT_MAX_CHARS","AI_PRODUCT_TYPES","AI_PRODUCTS_BULK_MAX","AI_PRODUCTS_OUTPUT_IDS","aiProductGaps","aiProductsProposalOf","parseAiProductsJobInputs","AI_STEP_TIERS","aiModelForStep","AI_CATALOG_MAX_TOKENS","AI_PRODUCT_CATEGORIES_MAX_TOKENS","AI_PRODUCT_COPY_MAX_TOKENS","AI_PRODUCTS_STEP","generateAiCatalog","generateAiCategories","generateAiProductCopy","aiRouteReadsImages","registerAiJobAdmission","aiJobStepBudget","aiGenerationSpent","aiUnspentOutcome","AI_PRODUCT_IMAGE_READ_MS","readAiProductImage","registerAiJobStep","registerAiJobStepPasses","AI_PRODUCTS_NO_SITE_COPY","AI_PRODUCTS_UNKNOWN_SITE_COPY","AI_PRODUCTS_NOT_ENTITLED_COPY","AI_PRODUCTS_COMMERCE_OFF_COPY","AI_PRODUCTS_GONE_COPY","AI_PRODUCTS_DECLINED_COPY","AI_PRODUCT_PHOTO_NOTES","unreadable","model","AI_PRODUCTS_SITE_READ_MS","TIER","AI_JOB_PRODUCT_COPY_BUDGET","tier","maxTokens","lookups","ownReadsMs","AI_JOB_CATALOG_BUDGET","AI_JOB_CATEGORIES_BUDGET","AI_JOB_PRODUCTS_STEP_BUDGET","aiProductsRunMinimumMs","job","inputs","minimumMs","target","str","value","trim","plain","JSON","parse","stringify","loadStore","firestore","host","hostId","failure","snapshot","collection","doc","get","exists","data","orgId","seo","subdomain","name","readCategories","given","index","records","list","limit","map","record","id","sort","a","b","localeCompare","readProductFacts","productId","facts","type","Array","isArray","includes","text","slice","tags","filter","Boolean","categoryIds","options","option","values","imageUrl","seoTitle","seoDescription","modelOf","context","modelFor","output","site","label","proposal","resource","hostSubdomain","writeProductCopy","product","deps","signal","categories","categoryIndex","photo","image","readsImages","read","status","generation","store","names","Map","category","note","kind","optionNamesBefore","gaps","description","skipped","notes","createAiJobProductsStep","spent","refused","message","outputs","copy","done","entry","length","productIds","more","continue","reported","brief","products","catalog","existing","existingCategoryNames","runAiJobProductsStep","aiProductsJobAdmission","error","owner","org","keeper","pluginId","released","authorization","registerAiProductsJob","minimumMsFor"],"mappings":"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"}
|
|
@@ -19,7 +19,6 @@ import { type SeoListingFieldKey } from '@aglyn/aglyn/app-utils/seo-listing-fiel
|
|
|
19
19
|
import type { AiJobOutput } from '../model/ai-jobs.types';
|
|
20
20
|
import { type AiSeoAuditReport, type AiSeoPageFix, type AiSeoSiteFormField } from '../model/ai-seo';
|
|
21
21
|
import type { AiSystemBlock } from '../runtime/ai-runtime';
|
|
22
|
-
import { type AiSeoAuditPage } from '../runtime/seo-audit';
|
|
23
22
|
import { type AiSeoBatchAnswer, type AiSeoBatchPage } from '../tools/ai-seo-tool';
|
|
24
23
|
import { AI_SEO_FIXES_MAX_TOKENS, AI_SEO_SITE_MAX_TOKENS } from './ai-job-seo-budget';
|
|
25
24
|
import type { AiJobStepRunner } from './ai-job-text-step';
|
|
@@ -48,8 +47,9 @@ import type { AiJobStepRunner } from './ai-job-text-step';
|
|
|
48
47
|
* ## An audit, a unit at a time
|
|
49
48
|
*
|
|
50
49
|
* The first pass reads the site — every published page's version and the
|
|
51
|
-
* shared layouts, for the links between them — scores it without a
|
|
52
|
-
*
|
|
50
|
+
* shared layouts, for the links between them — and scores it without a
|
|
51
|
+
* model, through the platform's SEO check (the same findings the site's SEO
|
|
52
|
+
* section lists to every owner); it records the report, and runs the first unit of generated work. Each later
|
|
53
53
|
* pass runs one more unit: the site-wide proposal, then the page fixes in
|
|
54
54
|
* batches. Every pass is one reservation and one provider exchange, and it
|
|
55
55
|
* asks the machine to continue until nothing is left, so a large site is a
|
|
@@ -59,8 +59,6 @@ export declare const AI_SEO_NO_SITE_COPY = "This SEO job names a site this works
|
|
|
59
59
|
export declare const AI_SEO_NO_PAGE_COPY = "This page is no longer on the site.";
|
|
60
60
|
export declare const AI_SEO_NO_TARGET_COPY = "This SEO job does not say what to write about.";
|
|
61
61
|
export declare const AI_SEO_NO_PRODUCT_COPY = "Give the product a name before writing its listing.";
|
|
62
|
-
/** Shared layouts an audit reads for the links in a site's navigation. */
|
|
63
|
-
export declare const AI_SEO_LAYOUT_SCAN_LIMIT = 20;
|
|
64
62
|
/** How much of each page a batch of fixes carries. */
|
|
65
63
|
export declare const AI_SEO_BATCH_PAGE_TEXT_CHARS = 700;
|
|
66
64
|
/**
|
|
@@ -70,7 +68,6 @@ export declare const AI_SEO_BATCH_PAGE_TEXT_CHARS = 700;
|
|
|
70
68
|
* measured in `ai-job-seo-step.spec.ts`.
|
|
71
69
|
*/
|
|
72
70
|
export { AI_SEO_FIXES_MAX_TOKENS, AI_SEO_SITE_MAX_TOKENS };
|
|
73
|
-
type Firestore = FirebaseFirestore.Firestore;
|
|
74
71
|
/** The host document fields this step reads. */
|
|
75
72
|
export interface SeoHostDocument {
|
|
76
73
|
orgId?: string;
|
|
@@ -103,24 +100,6 @@ export interface SeoHostDocument {
|
|
|
103
100
|
* comma-separated) that the target's editor holds, or all of those.
|
|
104
101
|
*/
|
|
105
102
|
export declare function aiSeoRequestedFields(raw: unknown, allowed: readonly SeoListingFieldKey[]): SeoListingFieldKey[];
|
|
106
|
-
export interface AiSeoScannedPage extends AiSeoAuditPage {
|
|
107
|
-
/** The page's node map as audited. */
|
|
108
|
-
nodes: Record<string, unknown> | null;
|
|
109
|
-
}
|
|
110
|
-
export interface AiSeoSiteScan {
|
|
111
|
-
pages: AiSeoScannedPage[];
|
|
112
|
-
skipped: number;
|
|
113
|
-
notes: string[];
|
|
114
|
-
}
|
|
115
|
-
/**
|
|
116
|
-
* Read the site the way its sitemap lists it: every screen the routing map
|
|
117
|
-
* publishes, less the template screens, the status pages and every screen
|
|
118
|
-
* that is not public — through the same shared predicates the tenant's
|
|
119
|
-
* `sitemap.xml` and `/llms.txt` apply, so the audit covers exactly the pages
|
|
120
|
-
* a crawler is handed. Then each page's published version, and the shared
|
|
121
|
-
* layouts, for the links that decide which pages nothing points to.
|
|
122
|
-
*/
|
|
123
|
-
export declare function scanAiSeoSite(firestore: Firestore, hostId: string, host: SeoHostDocument, rawKeywords: unknown): Promise<AiSeoSiteScan>;
|
|
124
103
|
/** One unit of generated work an audit still owes. */
|
|
125
104
|
export type AiSeoAuditUnit = {
|
|
126
105
|
kind: 'site';
|