@aglyn/plugins-ai 1.0.0-beta.253 → 1.0.0-beta.255

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/package.json +17 -17
  2. package/src/lib/components/ai-site-start-card.component.js +16 -8
  3. package/src/lib/components/ai-site-start-card.component.js.map +1 -1
  4. package/src/lib/components/assist-signals-page.component.js +5 -6
  5. package/src/lib/components/assist-signals-page.component.js.map +1 -1
  6. package/src/lib/components/staff-org-ai-card.component.d.ts +0 -10
  7. package/src/lib/components/staff-org-ai-card.component.js +10 -79
  8. package/src/lib/components/staff-org-ai-card.component.js.map +1 -1
  9. package/src/lib/components/staff-org-ai-conversations.component.js +8 -41
  10. package/src/lib/components/staff-org-ai-conversations.component.js.map +1 -1
  11. package/src/lib/components/staff-user-ai-usage-card.component.js +2 -1
  12. package/src/lib/components/staff-user-ai-usage-card.component.js.map +1 -1
  13. package/src/lib/jobs/ai-job-page-language.d.ts +14 -1
  14. package/src/lib/jobs/ai-job-page-language.js +18 -3
  15. package/src/lib/jobs/ai-job-page-language.js.map +1 -1
  16. package/src/lib/jobs/ai-job-page-step.js +17 -3
  17. package/src/lib/jobs/ai-job-page-step.js.map +1 -1
  18. package/src/lib/jobs/ai-job-plan-step.js +17 -7
  19. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  20. package/src/lib/jobs/ai-job-site-content.d.ts +32 -16
  21. package/src/lib/jobs/ai-job-site-content.js +43 -20
  22. package/src/lib/jobs/ai-job-site-content.js.map +1 -1
  23. package/src/lib/jobs/ai-job-site-step.d.ts +3 -0
  24. package/src/lib/jobs/ai-job-site-step.js +23 -11
  25. package/src/lib/jobs/ai-job-site-step.js.map +1 -1
  26. package/src/lib/jobs/ai-layout-stock-photos.d.ts +30 -0
  27. package/src/lib/jobs/ai-layout-stock-photos.js +76 -8
  28. package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
  29. package/src/lib/jobs/fixtures/ai-dental-site-plan-recording.d.ts +95 -0
  30. package/src/lib/jobs/fixtures/ai-dental-site-plan-recording.js +137 -0
  31. package/src/lib/jobs/fixtures/ai-dental-site-plan-recording.js.map +1 -0
  32. package/src/lib/layout-language/ai-layout-labels.d.ts +2 -0
  33. package/src/lib/layout-language/ai-layout-labels.js +12 -6
  34. package/src/lib/layout-language/ai-layout-labels.js.map +1 -1
  35. package/src/lib/layout-language/ai-layout-pictures.d.ts +12 -3
  36. package/src/lib/layout-language/ai-layout-pictures.js +6 -3
  37. package/src/lib/layout-language/ai-layout-pictures.js.map +1 -1
  38. package/src/lib/model/ai-plan-capabilities.d.ts +5 -0
  39. package/src/lib/model/ai-plan-capabilities.js.map +1 -1
  40. package/src/lib/model/ai-products-bulk.d.ts +48 -0
  41. package/src/lib/model/ai-products-bulk.js +56 -0
  42. package/src/lib/model/ai-products-bulk.js.map +1 -0
  43. package/src/lib/model/ai-products.d.ts +2 -4
  44. package/src/lib/model/ai-products.js +4 -9
  45. package/src/lib/model/ai-products.js.map +1 -1
  46. package/src/lib/model/ai-site-job.d.ts +32 -12
  47. package/src/lib/model/ai-site-job.js +35 -20
  48. package/src/lib/model/ai-site-job.js.map +1 -1
  49. package/src/lib/model/ai-site-kinds.d.ts +5 -5
  50. package/src/lib/model/ai-site-kinds.js +5 -5
  51. package/src/lib/model/ai-site-kinds.js.map +1 -1
  52. package/src/lib/providers/anthropic.js +26 -5
  53. package/src/lib/providers/anthropic.js.map +1 -1
  54. package/src/lib/providers/contract.d.ts +10 -0
  55. package/src/lib/providers/contract.js.map +1 -1
  56. package/src/lib/runtime/ai-doctrine-validators.d.ts +20 -0
  57. package/src/lib/runtime/ai-doctrine-validators.js +53 -1
  58. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  59. package/src/lib/runtime/ai-doctrine.d.ts +11 -0
  60. package/src/lib/runtime/ai-doctrine.js +9 -4
  61. package/src/lib/runtime/ai-doctrine.js.map +1 -1
  62. package/src/lib/runtime/ai-runtime.d.ts +6 -0
  63. package/src/lib/runtime/ai-runtime.js +10 -0
  64. package/src/lib/runtime/ai-runtime.js.map +1 -1
  65. package/src/lib/server/ai-admin-conversations.d.ts +2 -0
  66. package/src/lib/server/ai-admin-conversations.js +40 -7
  67. package/src/lib/server/ai-admin-conversations.js.map +1 -1
  68. package/src/lib/server/ai-admin-org.js +28 -2
  69. package/src/lib/server/ai-admin-org.js.map +1 -1
  70. package/src/lib/server/ai-admin-orgs-spend.js +19 -1
  71. package/src/lib/server/ai-admin-orgs-spend.js.map +1 -1
  72. package/src/lib/server/ai-admin-signals.js +11 -3
  73. package/src/lib/server/ai-admin-signals.js.map +1 -1
  74. package/src/lib/server/ai-admin-user.js +11 -1
  75. package/src/lib/server/ai-admin-user.js.map +1 -1
  76. package/src/lib/usage/ai-usage-wire.d.ts +5 -2
  77. package/src/lib/usage/ai-usage-wire.js +5 -2
  78. package/src/lib/usage/ai-usage-wire.js.map +1 -1
  79. package/src/lib/usage/assist-signals-view.d.ts +123 -0
  80. package/src/lib/usage/assist-signals-view.js +151 -0
  81. package/src/lib/usage/assist-signals-view.js.map +1 -0
  82. package/src/lib/usage/staff-org-ai-conversations-view.d.ts +51 -0
  83. package/src/lib/usage/staff-org-ai-conversations-view.js +127 -0
  84. package/src/lib/usage/staff-org-ai-conversations-view.js.map +1 -0
  85. package/src/lib/usage/staff-org-ai-view.d.ts +181 -0
  86. package/src/lib/usage/staff-org-ai-view.js +263 -0
  87. package/src/lib/usage/staff-org-ai-view.js.map +1 -0
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-plan-capabilities.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n AI_BUILD_PLAN_CREATE_KINDS,\n AI_BUILD_PLAN_CREATION_NOUNS,\n type AiBuildPlan,\n type AiBuildPlanCreateKind,\n} from './ai-build-plan'\n\n/**\n * What a plan may create on one site (AGL-3030): the workspace's plan and the\n * site's counts against its limits, narrowed to what the job itself builds.\n *\n * A plan that names a creation nothing can make is a dead end the member pays\n * for twice: once for the plan, and again for the plan they describe after\n * being told to make the creation by hand — which, on a workspace whose plan\n * does not include it, they cannot. So the plan step is TOLD what may be\n * created before it answers, and the plan rules refuse a creation outside it\n * with the one re-ask every other rule gets.\n *\n * ── Where the doctrine builds inline ─────────────────────────────────────\n *\n * The building doctrine makes a repeat a reusable component and a form a\n * saved form placed by id. A workspace whose plan keeps no reusable\n * components draws a list's repeated items in one section instead\n * (AGL-3071); `reusableComponents` is the one flag every validator reads for\n * that.\n *\n * A form is never drawn inline (AGL-3596): it is a saved form, counted\n * against the site's form allowance like one a member makes. Where the site\n * has none to place and may create none, the page places no form at all.\n *\n * ── Kept out of the cached prefix ────────────────────────────────────────\n *\n * The capabilities are per workspace and per site, so they ride the plan's\n * USER turn, never a system block: the doctrine's cached prefix stays one\n * entry for the platform. The doctrine states the rule once — create only\n * what the request says may be created, and build inline where it says the\n * workspace keeps no reusable components — and the request states the facts.\n *\n * This module imports nothing at runtime beyond the plan's own model, so the\n * validators, the plan step, the doors and their specs read one vocabulary.\n * What a workspace's plan and counts come to is read by\n * `readAiPlanCapabilities` in `jobs/ai-job-drafts.ts`, beside the band\n * arithmetic the create route and the draft writer share.\n */\n\n/** One creation kind, as a job on this site may make it or not. */\nexport interface AiPlanCreation {\n allowed: boolean\n /** How many more the site may hold; `null` when the plan counts none. */\n left: number | null\n /**\n * Why not, as a clause a sentence completes: \"this workspace's plan does\n * not include reusable components\". `null` when allowed.\n */\n reason: string | null\n}\n\nexport interface AiPlanCapabilities {\n /**\n * Whether the workspace keeps reusable components. Where it does not, a\n * list's repeated items are drawn in one section. Forms are not read off\n * it: whether one may be created is `create.form` (AGL-3596).\n */\n reusableComponents: boolean\n /** Every creation kind a plan may name, and whether this job may make one here. */\n create: Readonly<Record<AiBuildPlanCreateKind, AiPlanCreation>>\n /**\n * Whether the workspace spends the Free taste, whose monthly credits are a\n * wall (AGL-2925): a plan is then held to the sections the wall's worst\n * case pays for (AGL-3070). Absent is `false`.\n */\n freeTaste?: boolean\n /**\n * A Free workspace's site scaffold (AGL-3594): the most pages its plan may\n * build, which the plan step sets for a `site` job on the Free taste. The\n * Free wall then holds the plan to that many pages and to the sections a\n * SITE plan's worst case leaves room for. Absent for every other job.\n */\n freeSitePages?: number\n /**\n * Whether this job's pages are written in the compact layout language\n * (AGL-3660), whose compiler draws a section's repeated items itself: as\n * instances of a component the plan places there, or, with none, in their\n * compact form, a title over its text, which is smaller than a block rule 1\n * calls a repeat. A plan is then not asked for a component for a section's\n * repeated items (AGL-3616); a section two pages share still is one. The\n * plan step sets it for a `build` job. Absent is `false`.\n *\n * It is the plan half of one rule. The page half is the tree check's\n * `AiDoctrineTreeContext.repeatsCompiled`, which every page the layout\n * language compiles is checked with: where the site's design draws a\n * section's items as picture cards — larger than the compact form, and so\n * a block rule 1 would count — those cards are the compiler's, not a block\n * typed out twice, and rule 1 asks no component of them (AGL-3660).\n */\n repeatsCompiled?: boolean\n /**\n * The most datasets this job's plan keeps (AGL-3616): a site start builds\n * at most a few. A plan naming more is settled in code before the rules\n * read it (`aiSettlePlanDatasets`), never re-asked. Absent is no cap beyond\n * `create.dataset.left`.\n */\n datasetsMax?: number\n /**\n * The fewest pieces a section that shows the work is planned with\n * (AGL-3754): a site start's `AI_SITE_GALLERY_MIN_ITEMS`. A plan naming\n * fewer is raised to it in code before the rules read it\n * (`aiSettlePlanGalleries`), never re-asked. Absent leaves every count as\n * planned.\n */\n galleryMinItems?: number\n}\n\n/** What a job of one kind builds from its own plan, where it builds only some creations. */\nexport interface AiPlanJobScope {\n /** The job as a refusal names it: \"a page job\". */\n noun: string\n /** The creation kinds the job builds itself; any other is refused before the plan is kept. */\n creates: readonly AiBuildPlanCreateKind[]\n}\n\nconst ALLOWED: AiPlanCreation = { allowed: true, left: null, reason: null }\n\n/**\n * Capabilities that restrict nothing: what a plan is held to when no\n * workspace was read — an org-level job, an eval, a spec about something\n * else. The doctrine applies whole.\n */\nexport function aiUnrestrictedPlanCapabilities(): AiPlanCapabilities {\n return {\n reusableComponents: true,\n create: Object.fromEntries(\n AI_BUILD_PLAN_CREATE_KINDS.map((kind) => [kind, ALLOWED]),\n ) as Record<AiBuildPlanCreateKind, AiPlanCreation>,\n }\n}\n\n/**\n * The workspace's capabilities narrowed to what a job of one kind builds.\n * A creation the workspace itself cannot make keeps the workspace's reason,\n * which is the one a member can act on; one the job does not build says so.\n */\nexport function aiPlanCapabilitiesForJob(\n capabilities: AiPlanCapabilities,\n scope: AiPlanJobScope | null | undefined,\n): AiPlanCapabilities {\n if (!scope) return capabilities\n const create = { ...capabilities.create }\n for (const kind of AI_BUILD_PLAN_CREATE_KINDS) {\n if (!create[kind].allowed || scope.creates.includes(kind)) continue\n create[kind] = { allowed: false, left: null, reason: `${scope.noun} does not build one` }\n }\n return { ...capabilities, create }\n}\n\n/** \"a layout\", \"an email design\": a creation kind with its article. */\nexport function aiCreationNoun(kind: AiBuildPlanCreateKind): string {\n const noun = AI_BUILD_PLAN_CREATION_NOUNS[kind].noun\n return `${/^[aeiou]/.test(noun) ? 'an' : 'a'} ${noun}`\n}\n\n/**\n * The sentence a request states when the workspace keeps no reusable\n * components. It says a list's repeated items go in one section (AGL-3071):\n * \"a repeated item in its own section\" reads just as well as a section for\n * each item, which is how a live About page's four practice areas came to be\n * planned as four sections of one item.\n */\nexport const AI_PLAN_INLINE_SENTENCE =\n \"This workspace keeps no reusable components: draw a list's repeated items in one section.\"\n\n/**\n * The sentence a build's request states (AGL-3616): its pages are written in\n * the layout language, which draws a section's repeated items itself, so the\n * plan counts them in the section's `items` and needs no component for them.\n */\nexport const AI_PLAN_COMPILED_REPEATS_SENTENCE =\n \"A section's repeated items need no component here: count them in its items and the page draws them. A section two pages share is still one component.\"\n\n/**\n * The sentence a request states when the site has no saved form and this job\n * may create none (AGL-3596): the plan places no form, rather than one drawn\n * on the page with no saved form behind it.\n */\nexport const AI_PLAN_NO_FORM_SENTENCE =\n 'This site has no saved form and may create none: plan no form, and no section that collects answers.'\n\n/**\n * What the plan step's user turn says the job may create here: one line a\n * kind, and the inline sentence where the workspace keeps no reusable\n * components. Per workspace and per site, so it is never a system block.\n */\nexport function aiPlanCapabilityLines(capabilities: AiPlanCapabilities): string[] {\n const lines = ['What this job may create on this site:']\n for (const kind of AI_BUILD_PLAN_CREATE_KINDS) {\n const entry = capabilities.create[kind]\n const noun = AI_BUILD_PLAN_CREATION_NOUNS[kind].noun\n if (entry.allowed) {\n lines.push(`- ${noun}: yes${entry.left === null ? '' : `, ${entry.left} more`}`)\n } else {\n lines.push(`- ${noun}: no, because ${entry.reason ?? 'it cannot be made here'}`)\n }\n }\n if (!capabilities.reusableComponents) lines.push(AI_PLAN_INLINE_SENTENCE)\n return lines\n}\n\n/**\n * Whether a plan on this site can place a saved form at all (AGL-3596): the\n * site has one, or the job may create one. Absent capabilities restrict\n * nothing, as everywhere else.\n */\nexport function aiPlanCanPlaceForm(\n inventory: { forms: readonly unknown[] } | null,\n capabilities: Pick<AiPlanCapabilities, 'create'> | null,\n): boolean {\n if (inventory?.forms.length) return true\n return capabilities?.create.form.allowed !== false\n}\n\n/** What to do instead of a creation the job may not make, for the re-ask and the review. */\nfunction insteadOf(kind: AiBuildPlanCreateKind, capabilities: AiPlanCapabilities): string {\n switch (kind) {\n case 'component':\n return capabilities.reusableComponents\n ? 'Place a component the site already has.'\n : \"Draw a list's repeated items in one section instead.\"\n case 'form':\n return 'Place a form the site already has, or leave the form off the page.'\n case 'layout':\n return 'Put the page in a layout the site already has.'\n case 'template':\n return 'Build the page without a template.'\n case 'theme-change':\n return \"Use the colors the theme already has.\"\n case 'dataset':\n return 'Bind a dataset or collection the site already has, or keep the list short enough to write out.'\n case 'email':\n return 'Leave the email out of this job.'\n }\n}\n\n/** One creation a plan names that this job may not make here. */\nexport interface AiPlanUncreatable {\n kind: AiBuildPlanCreateKind\n name: string\n /** The plan path of the creation: `create[2]`. */\n path: string\n /** Why not, as a clause: \"this site has room for 1 more\". */\n reason: string\n /** One customer-safe sentence: what was refused, why, and what to do instead. */\n message: string\n}\n\n/**\n * Why the nth creation of a kind may not be made here, as a clause; `null`\n * when it may. A kind the site may make only so many more of refuses the ones\n * past that count, not the first of them.\n */\nfunction refusedReason(capability: AiPlanCreation, nth: number): string | null {\n const overCount = capability.allowed && capability.left !== null && nth > capability.left\n if (capability.allowed && !overCount) return null\n return overCount\n ? `this site has room for ${capability.left} more`\n : (capability.reason ?? 'it cannot be made here')\n}\n\n/** The creations a plan names that the capabilities refuse, in plan order. */\nexport function aiPlanUncreatable(\n plan: Pick<AiBuildPlan, 'create'>,\n capabilities: AiPlanCapabilities,\n): AiPlanUncreatable[] {\n const refused: AiPlanUncreatable[] = []\n const counted = new Map<AiBuildPlanCreateKind, number>()\n plan.create.forEach((entry, index) => {\n const nth = (counted.get(entry.kind) ?? 0) + 1\n counted.set(entry.kind, nth)\n const reason = refusedReason(capabilities.create[entry.kind], nth)\n if (reason === null) return\n refused.push({\n kind: entry.kind,\n name: entry.name,\n path: `create[${index}]`,\n reason,\n message: `The plan creates ${aiCreationNoun(entry.kind)} named \"${entry.name}\", and ${reason}. ${insteadOf(entry.kind, capabilities)}`,\n })\n })\n return refused\n}\n\n/**\n * Why the plan could not add one more creation of a kind here, and what to\n * do instead; `null` when it could (AGL-3040). Counted past the creations of\n * that kind the plan already names, as `aiPlanUncreatable` counts them, so a\n * plan told to declare a creation is never told to declare one the next\n * answer is refused for.\n */\nexport function aiPlanUncreatableKind(\n plan: Pick<AiBuildPlan, 'create'>,\n kind: AiBuildPlanCreateKind,\n capabilities: AiPlanCapabilities,\n): { reason: string; instead: string } | null {\n const planned = plan.create.filter((entry) => entry.kind === kind).length\n const reason = refusedReason(capabilities.create[kind], planned + 1)\n return reason === null ? null : { reason, instead: insteadOf(kind, capabilities) }\n}\n"],"names":["AI_BUILD_PLAN_CREATE_KINDS","AI_BUILD_PLAN_CREATION_NOUNS","ALLOWED","allowed","left","reason","aiUnrestrictedPlanCapabilities","reusableComponents","create","Object","fromEntries","map","kind","aiPlanCapabilitiesForJob","capabilities","scope","creates","includes","noun","aiCreationNoun","test","AI_PLAN_INLINE_SENTENCE","AI_PLAN_COMPILED_REPEATS_SENTENCE","AI_PLAN_NO_FORM_SENTENCE","aiPlanCapabilityLines","lines","entry","push","aiPlanCanPlaceForm","inventory","forms","length","form","insteadOf","refusedReason","capability","nth","overCount","aiPlanUncreatable","plan","refused","counted","Map","forEach","index","get","set","name","path","message","aiPlanUncreatableKind","planned","filter","instead"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-plan-capabilities.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n AI_BUILD_PLAN_CREATE_KINDS,\n AI_BUILD_PLAN_CREATION_NOUNS,\n type AiBuildPlan,\n type AiBuildPlanCreateKind,\n} from './ai-build-plan'\n\n/**\n * What a plan may create on one site (AGL-3030): the workspace's plan and the\n * site's counts against its limits, narrowed to what the job itself builds.\n *\n * A plan that names a creation nothing can make is a dead end the member pays\n * for twice: once for the plan, and again for the plan they describe after\n * being told to make the creation by hand — which, on a workspace whose plan\n * does not include it, they cannot. So the plan step is TOLD what may be\n * created before it answers, and the plan rules refuse a creation outside it\n * with the one re-ask every other rule gets.\n *\n * ── Where the doctrine builds inline ─────────────────────────────────────\n *\n * The building doctrine makes a repeat a reusable component and a form a\n * saved form placed by id. A workspace whose plan keeps no reusable\n * components draws a list's repeated items in one section instead\n * (AGL-3071); `reusableComponents` is the one flag every validator reads for\n * that.\n *\n * A form is never drawn inline (AGL-3596): it is a saved form, counted\n * against the site's form allowance like one a member makes. Where the site\n * has none to place and may create none, the page places no form at all.\n *\n * ── Kept out of the cached prefix ────────────────────────────────────────\n *\n * The capabilities are per workspace and per site, so they ride the plan's\n * USER turn, never a system block: the doctrine's cached prefix stays one\n * entry for the platform. The doctrine states the rule once — create only\n * what the request says may be created, and build inline where it says the\n * workspace keeps no reusable components — and the request states the facts.\n *\n * This module imports nothing at runtime beyond the plan's own model, so the\n * validators, the plan step, the doors and their specs read one vocabulary.\n * What a workspace's plan and counts come to is read by\n * `readAiPlanCapabilities` in `jobs/ai-job-drafts.ts`, beside the band\n * arithmetic the create route and the draft writer share.\n */\n\n/** One creation kind, as a job on this site may make it or not. */\nexport interface AiPlanCreation {\n allowed: boolean\n /** How many more the site may hold; `null` when the plan counts none. */\n left: number | null\n /**\n * Why not, as a clause a sentence completes: \"this workspace's plan does\n * not include reusable components\". `null` when allowed.\n */\n reason: string | null\n}\n\nexport interface AiPlanCapabilities {\n /**\n * Whether the workspace keeps reusable components. Where it does not, a\n * list's repeated items are drawn in one section. Forms are not read off\n * it: whether one may be created is `create.form` (AGL-3596).\n */\n reusableComponents: boolean\n /** Every creation kind a plan may name, and whether this job may make one here. */\n create: Readonly<Record<AiBuildPlanCreateKind, AiPlanCreation>>\n /**\n * Whether the workspace spends the Free taste, whose monthly credits are a\n * wall (AGL-2925): a plan is then held to the sections the wall's worst\n * case pays for (AGL-3070). Absent is `false`.\n */\n freeTaste?: boolean\n /**\n * A Free workspace's site scaffold (AGL-3594): the most pages its plan may\n * build, which the plan step sets for a `site` job on the Free taste. The\n * Free wall then holds the plan to that many pages and to the sections a\n * SITE plan's worst case leaves room for. Absent for every other job.\n */\n freeSitePages?: number\n /**\n * The first posts a Free blog's site scaffold writes beside its pages\n * (AGL-3798), which the Free wall counts before its sections. Absent is none.\n */\n freeSitePosts?: number\n /**\n * Whether this job's pages are written in the compact layout language\n * (AGL-3660), whose compiler draws a section's repeated items itself: as\n * instances of a component the plan places there, or, with none, in their\n * compact form, a title over its text, which is smaller than a block rule 1\n * calls a repeat. A plan is then not asked for a component for a section's\n * repeated items (AGL-3616); a section two pages share still is one. The\n * plan step sets it for a `build` job. Absent is `false`.\n *\n * It is the plan half of one rule. The page half is the tree check's\n * `AiDoctrineTreeContext.repeatsCompiled`, which every page the layout\n * language compiles is checked with: where the site's design draws a\n * section's items as picture cards — larger than the compact form, and so\n * a block rule 1 would count — those cards are the compiler's, not a block\n * typed out twice, and rule 1 asks no component of them (AGL-3660).\n */\n repeatsCompiled?: boolean\n /**\n * The most datasets this job's plan keeps (AGL-3616): a site start builds\n * at most a few. A plan naming more is settled in code before the rules\n * read it (`aiSettlePlanDatasets`), never re-asked. Absent is no cap beyond\n * `create.dataset.left`.\n */\n datasetsMax?: number\n /**\n * The fewest pieces a section that shows the work is planned with\n * (AGL-3754): a site start's `AI_SITE_GALLERY_MIN_ITEMS`. A plan naming\n * fewer is raised to it in code before the rules read it\n * (`aiSettlePlanGalleries`), never re-asked. Absent leaves every count as\n * planned.\n */\n galleryMinItems?: number\n}\n\n/** What a job of one kind builds from its own plan, where it builds only some creations. */\nexport interface AiPlanJobScope {\n /** The job as a refusal names it: \"a page job\". */\n noun: string\n /** The creation kinds the job builds itself; any other is refused before the plan is kept. */\n creates: readonly AiBuildPlanCreateKind[]\n}\n\nconst ALLOWED: AiPlanCreation = { allowed: true, left: null, reason: null }\n\n/**\n * Capabilities that restrict nothing: what a plan is held to when no\n * workspace was read — an org-level job, an eval, a spec about something\n * else. The doctrine applies whole.\n */\nexport function aiUnrestrictedPlanCapabilities(): AiPlanCapabilities {\n return {\n reusableComponents: true,\n create: Object.fromEntries(\n AI_BUILD_PLAN_CREATE_KINDS.map((kind) => [kind, ALLOWED]),\n ) as Record<AiBuildPlanCreateKind, AiPlanCreation>,\n }\n}\n\n/**\n * The workspace's capabilities narrowed to what a job of one kind builds.\n * A creation the workspace itself cannot make keeps the workspace's reason,\n * which is the one a member can act on; one the job does not build says so.\n */\nexport function aiPlanCapabilitiesForJob(\n capabilities: AiPlanCapabilities,\n scope: AiPlanJobScope | null | undefined,\n): AiPlanCapabilities {\n if (!scope) return capabilities\n const create = { ...capabilities.create }\n for (const kind of AI_BUILD_PLAN_CREATE_KINDS) {\n if (!create[kind].allowed || scope.creates.includes(kind)) continue\n create[kind] = { allowed: false, left: null, reason: `${scope.noun} does not build one` }\n }\n return { ...capabilities, create }\n}\n\n/** \"a layout\", \"an email design\": a creation kind with its article. */\nexport function aiCreationNoun(kind: AiBuildPlanCreateKind): string {\n const noun = AI_BUILD_PLAN_CREATION_NOUNS[kind].noun\n return `${/^[aeiou]/.test(noun) ? 'an' : 'a'} ${noun}`\n}\n\n/**\n * The sentence a request states when the workspace keeps no reusable\n * components. It says a list's repeated items go in one section (AGL-3071):\n * \"a repeated item in its own section\" reads just as well as a section for\n * each item, which is how a live About page's four practice areas came to be\n * planned as four sections of one item.\n */\nexport const AI_PLAN_INLINE_SENTENCE =\n \"This workspace keeps no reusable components: draw a list's repeated items in one section.\"\n\n/**\n * The sentence a build's request states (AGL-3616): its pages are written in\n * the layout language, which draws a section's repeated items itself, so the\n * plan counts them in the section's `items` and needs no component for them.\n */\nexport const AI_PLAN_COMPILED_REPEATS_SENTENCE =\n \"A section's repeated items need no component here: count them in its items and the page draws them. A section two pages share is still one component.\"\n\n/**\n * The sentence a request states when the site has no saved form and this job\n * may create none (AGL-3596): the plan places no form, rather than one drawn\n * on the page with no saved form behind it.\n */\nexport const AI_PLAN_NO_FORM_SENTENCE =\n 'This site has no saved form and may create none: plan no form, and no section that collects answers.'\n\n/**\n * What the plan step's user turn says the job may create here: one line a\n * kind, and the inline sentence where the workspace keeps no reusable\n * components. Per workspace and per site, so it is never a system block.\n */\nexport function aiPlanCapabilityLines(capabilities: AiPlanCapabilities): string[] {\n const lines = ['What this job may create on this site:']\n for (const kind of AI_BUILD_PLAN_CREATE_KINDS) {\n const entry = capabilities.create[kind]\n const noun = AI_BUILD_PLAN_CREATION_NOUNS[kind].noun\n if (entry.allowed) {\n lines.push(`- ${noun}: yes${entry.left === null ? '' : `, ${entry.left} more`}`)\n } else {\n lines.push(`- ${noun}: no, because ${entry.reason ?? 'it cannot be made here'}`)\n }\n }\n if (!capabilities.reusableComponents) lines.push(AI_PLAN_INLINE_SENTENCE)\n return lines\n}\n\n/**\n * Whether a plan on this site can place a saved form at all (AGL-3596): the\n * site has one, or the job may create one. Absent capabilities restrict\n * nothing, as everywhere else.\n */\nexport function aiPlanCanPlaceForm(\n inventory: { forms: readonly unknown[] } | null,\n capabilities: Pick<AiPlanCapabilities, 'create'> | null,\n): boolean {\n if (inventory?.forms.length) return true\n return capabilities?.create.form.allowed !== false\n}\n\n/** What to do instead of a creation the job may not make, for the re-ask and the review. */\nfunction insteadOf(kind: AiBuildPlanCreateKind, capabilities: AiPlanCapabilities): string {\n switch (kind) {\n case 'component':\n return capabilities.reusableComponents\n ? 'Place a component the site already has.'\n : \"Draw a list's repeated items in one section instead.\"\n case 'form':\n return 'Place a form the site already has, or leave the form off the page.'\n case 'layout':\n return 'Put the page in a layout the site already has.'\n case 'template':\n return 'Build the page without a template.'\n case 'theme-change':\n return \"Use the colors the theme already has.\"\n case 'dataset':\n return 'Bind a dataset or collection the site already has, or keep the list short enough to write out.'\n case 'email':\n return 'Leave the email out of this job.'\n }\n}\n\n/** One creation a plan names that this job may not make here. */\nexport interface AiPlanUncreatable {\n kind: AiBuildPlanCreateKind\n name: string\n /** The plan path of the creation: `create[2]`. */\n path: string\n /** Why not, as a clause: \"this site has room for 1 more\". */\n reason: string\n /** One customer-safe sentence: what was refused, why, and what to do instead. */\n message: string\n}\n\n/**\n * Why the nth creation of a kind may not be made here, as a clause; `null`\n * when it may. A kind the site may make only so many more of refuses the ones\n * past that count, not the first of them.\n */\nfunction refusedReason(capability: AiPlanCreation, nth: number): string | null {\n const overCount = capability.allowed && capability.left !== null && nth > capability.left\n if (capability.allowed && !overCount) return null\n return overCount\n ? `this site has room for ${capability.left} more`\n : (capability.reason ?? 'it cannot be made here')\n}\n\n/** The creations a plan names that the capabilities refuse, in plan order. */\nexport function aiPlanUncreatable(\n plan: Pick<AiBuildPlan, 'create'>,\n capabilities: AiPlanCapabilities,\n): AiPlanUncreatable[] {\n const refused: AiPlanUncreatable[] = []\n const counted = new Map<AiBuildPlanCreateKind, number>()\n plan.create.forEach((entry, index) => {\n const nth = (counted.get(entry.kind) ?? 0) + 1\n counted.set(entry.kind, nth)\n const reason = refusedReason(capabilities.create[entry.kind], nth)\n if (reason === null) return\n refused.push({\n kind: entry.kind,\n name: entry.name,\n path: `create[${index}]`,\n reason,\n message: `The plan creates ${aiCreationNoun(entry.kind)} named \"${entry.name}\", and ${reason}. ${insteadOf(entry.kind, capabilities)}`,\n })\n })\n return refused\n}\n\n/**\n * Why the plan could not add one more creation of a kind here, and what to\n * do instead; `null` when it could (AGL-3040). Counted past the creations of\n * that kind the plan already names, as `aiPlanUncreatable` counts them, so a\n * plan told to declare a creation is never told to declare one the next\n * answer is refused for.\n */\nexport function aiPlanUncreatableKind(\n plan: Pick<AiBuildPlan, 'create'>,\n kind: AiBuildPlanCreateKind,\n capabilities: AiPlanCapabilities,\n): { reason: string; instead: string } | null {\n const planned = plan.create.filter((entry) => entry.kind === kind).length\n const reason = refusedReason(capabilities.create[kind], planned + 1)\n return reason === null ? null : { reason, instead: insteadOf(kind, capabilities) }\n}\n"],"names":["AI_BUILD_PLAN_CREATE_KINDS","AI_BUILD_PLAN_CREATION_NOUNS","ALLOWED","allowed","left","reason","aiUnrestrictedPlanCapabilities","reusableComponents","create","Object","fromEntries","map","kind","aiPlanCapabilitiesForJob","capabilities","scope","creates","includes","noun","aiCreationNoun","test","AI_PLAN_INLINE_SENTENCE","AI_PLAN_COMPILED_REPEATS_SENTENCE","AI_PLAN_NO_FORM_SENTENCE","aiPlanCapabilityLines","lines","entry","push","aiPlanCanPlaceForm","inventory","forms","length","form","insteadOf","refusedReason","capability","nth","overCount","aiPlanUncreatable","plan","refused","counted","Map","forEach","index","get","set","name","path","message","aiPlanUncreatableKind","planned","filter","instead"],"mappings":"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"}
@@ -0,0 +1,48 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ /**
18
+ * THE BULK COPY JOB AN IMPORT ASKS FOR (AGL-2916, AGL-3671) — the pure half of
19
+ * "write descriptions with AI as they land", with no import at all, so the
20
+ * native apps replay it (`aiProductsImportJob`) beside the console's products
21
+ * hub card, which starts the very same job.
22
+ *
23
+ * A product import that asked for its copy leaves one `products` job for the
24
+ * products it CREATED: the first `AI_PRODUCTS_BULK_MAX` of them, their ids
25
+ * read from the import job's results (`api/transfer/status`), one brief that
26
+ * names how many, and `inputs` that carry the ids.
27
+ */
28
+ /** The most products one bulk job writes the copy of. */
29
+ export declare const AI_PRODUCTS_BULK_MAX = 50;
30
+ /** The brief a bulk copy job carries, which the jobs list shows and a card recognizes it by. */
31
+ export declare const aiProductsBulkBrief: (count: number) => string;
32
+ /** A bulk job's `inputs`: the product ids that can be read back, once each, at most `AI_PRODUCTS_BULK_MAX`. */
33
+ export declare function aiProductsBulkInputs(productIds: readonly string[]): Record<string, string>;
34
+ /** One row of an import job's results. */
35
+ export interface ImportResultRow {
36
+ outcome?: string;
37
+ recordId?: string;
38
+ }
39
+ /** The products an applied import created, once each and in the order the results list them. */
40
+ export declare function createdProductIdsOf(rows: readonly ImportResultRow[] | null | undefined): string[];
41
+ /**
42
+ * The copy job a landed import starts — the brief and the `inputs` the jobs route takes — for the first
43
+ * `AI_PRODUCTS_BULK_MAX` products it created; `null` when it created none.
44
+ */
45
+ export declare function aiProductsImportJob(rows: readonly ImportResultRow[] | null | undefined): {
46
+ brief: string;
47
+ inputs: Record<string, string>;
48
+ } | null;
@@ -0,0 +1,56 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ /**
17
+ * THE BULK COPY JOB AN IMPORT ASKS FOR (AGL-2916, AGL-3671) — the pure half of
18
+ * "write descriptions with AI as they land", with no import at all, so the
19
+ * native apps replay it (`aiProductsImportJob`) beside the console's products
20
+ * hub card, which starts the very same job.
21
+ *
22
+ * A product import that asked for its copy leaves one `products` job for the
23
+ * products it CREATED: the first `AI_PRODUCTS_BULK_MAX` of them, their ids
24
+ * read from the import job's results (`api/transfer/status`), one brief that
25
+ * names how many, and `inputs` that carry the ids.
26
+ */ /** The most products one bulk job writes the copy of. */ export const AI_PRODUCTS_BULK_MAX = 50;
27
+ const PRODUCT_ID = /^[A-Za-z0-9_-]{1,64}$/;
28
+ /** The brief a bulk copy job carries, which the jobs list shows and a card recognizes it by. */ export const aiProductsBulkBrief = (count)=>`Write storefront copy for ${count} ${count === 1 ? 'product' : 'products'}`;
29
+ /** A bulk job's `inputs`: the product ids that can be read back, once each, at most `AI_PRODUCTS_BULK_MAX`. */ export function aiProductsBulkInputs(productIds) {
30
+ return {
31
+ target: 'bulk',
32
+ productIds: [
33
+ ...new Set(productIds.filter((id)=>PRODUCT_ID.test(id)))
34
+ ].slice(0, AI_PRODUCTS_BULK_MAX).join(',')
35
+ };
36
+ }
37
+ /** The products an applied import created, once each and in the order the results list them. */ export function createdProductIdsOf(rows) {
38
+ const ids = [];
39
+ for (const row of rows != null ? rows : []){
40
+ if ((row == null ? void 0 : row.outcome) === 'created' && row.recordId && !ids.includes(row.recordId)) ids.push(row.recordId);
41
+ }
42
+ return ids;
43
+ }
44
+ /**
45
+ * The copy job a landed import starts — the brief and the `inputs` the jobs route takes — for the first
46
+ * `AI_PRODUCTS_BULK_MAX` products it created; `null` when it created none.
47
+ */ export function aiProductsImportJob(rows) {
48
+ const ids = createdProductIdsOf(rows).slice(0, AI_PRODUCTS_BULK_MAX);
49
+ if (!ids.length) return null;
50
+ return {
51
+ brief: aiProductsBulkBrief(ids.length),
52
+ inputs: aiProductsBulkInputs(ids)
53
+ };
54
+ }
55
+
56
+ //# sourceMappingURL=ai-products-bulk.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-products-bulk.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * THE BULK COPY JOB AN IMPORT ASKS FOR (AGL-2916, AGL-3671) — the pure half of\n * \"write descriptions with AI as they land\", with no import at all, so the\n * native apps replay it (`aiProductsImportJob`) beside the console's products\n * hub card, which starts the very same job.\n *\n * A product import that asked for its copy leaves one `products` job for the\n * products it CREATED: the first `AI_PRODUCTS_BULK_MAX` of them, their ids\n * read from the import job's results (`api/transfer/status`), one brief that\n * names how many, and `inputs` that carry the ids.\n */\n\n/** The most products one bulk job writes the copy of. */\nexport const AI_PRODUCTS_BULK_MAX = 50\n\nconst PRODUCT_ID = /^[A-Za-z0-9_-]{1,64}$/\n\n/** The brief a bulk copy job carries, which the jobs list shows and a card recognizes it by. */\nexport const aiProductsBulkBrief = (count: number): string =>\n `Write storefront copy for ${count} ${count === 1 ? 'product' : 'products'}`\n\n/** A bulk job's `inputs`: the product ids that can be read back, once each, at most `AI_PRODUCTS_BULK_MAX`. */\nexport function aiProductsBulkInputs(productIds: readonly string[]): Record<string, string> {\n return {\n target: 'bulk',\n productIds: [...new Set(productIds.filter((id) => PRODUCT_ID.test(id)))].slice(0, AI_PRODUCTS_BULK_MAX).join(','),\n }\n}\n\n/** One row of an import job's results. */\nexport interface ImportResultRow {\n outcome?: string\n recordId?: string\n}\n\n/** The products an applied import created, once each and in the order the results list them. */\nexport function createdProductIdsOf(rows: readonly ImportResultRow[] | null | undefined): string[] {\n const ids: string[] = []\n for (const row of rows ?? []) {\n if (row?.outcome === 'created' && row.recordId && !ids.includes(row.recordId)) ids.push(row.recordId)\n }\n return ids\n}\n\n/**\n * The copy job a landed import starts — the brief and the `inputs` the jobs route takes — for the first\n * `AI_PRODUCTS_BULK_MAX` products it created; `null` when it created none.\n */\nexport function aiProductsImportJob(\n rows: readonly ImportResultRow[] | null | undefined,\n): { brief: string; inputs: Record<string, string> } | null {\n const ids = createdProductIdsOf(rows).slice(0, AI_PRODUCTS_BULK_MAX)\n if (!ids.length) return null\n return { brief: aiProductsBulkBrief(ids.length), inputs: aiProductsBulkInputs(ids) }\n}\n"],"names":["AI_PRODUCTS_BULK_MAX","PRODUCT_ID","aiProductsBulkBrief","count","aiProductsBulkInputs","productIds","target","Set","filter","id","test","slice","join","createdProductIdsOf","rows","ids","row","outcome","recordId","includes","push","aiProductsImportJob","length","brief","inputs"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;CAUC,GAED,uDAAuD,GACvD,OAAO,MAAMA,uBAAuB,GAAE;AAEtC,MAAMC,aAAa;AAEnB,8FAA8F,GAC9F,OAAO,MAAMC,sBAAsB,CAACC,QAClC,CAAC,0BAA0B,EAAEA,MAAM,CAAC,EAAEA,UAAU,IAAI,YAAY,YAAY,CAAA;AAE9E,6GAA6G,GAC7G,OAAO,SAASC,qBAAqBC,UAA6B;IAChE,OAAO;QACLC,QAAQ;QACRD,YAAY;eAAI,IAAIE,IAAIF,WAAWG,MAAM,CAAC,CAACC,KAAOR,WAAWS,IAAI,CAACD;SAAM,CAACE,KAAK,CAAC,GAAGX,sBAAsBY,IAAI,CAAC;IAC/G;AACF;AAQA,8FAA8F,GAC9F,OAAO,SAASC,oBAAoBC,IAAmD;IACrF,MAAMC,MAAgB,EAAE;IACxB,KAAK,MAAMC,OAAOF,eAAAA,OAAQ,EAAE,CAAE;QAC5B,IAAIE,CAAAA,uBAAAA,IAAKC,OAAO,MAAK,aAAaD,IAAIE,QAAQ,IAAI,CAACH,IAAII,QAAQ,CAACH,IAAIE,QAAQ,GAAGH,IAAIK,IAAI,CAACJ,IAAIE,QAAQ;IACtG;IACA,OAAOH;AACT;AAEA;;;CAGC,GACD,OAAO,SAASM,oBACdP,IAAmD;IAEnD,MAAMC,MAAMF,oBAAoBC,MAAMH,KAAK,CAAC,GAAGX;IAC/C,IAAI,CAACe,IAAIO,MAAM,EAAE,OAAO;IACxB,OAAO;QAAEC,OAAOrB,oBAAoBa,IAAIO,MAAM;QAAGE,QAAQpB,qBAAqBW;IAAK;AACrF"}
@@ -15,6 +15,8 @@
15
15
  * limitations under the License.
16
16
  */
17
17
  import type { AiJobOutput, AiJobSummary } from './ai-jobs.types';
18
+ import { AI_PRODUCTS_BULK_MAX, aiProductsBulkBrief } from './ai-products-bulk';
19
+ export { AI_PRODUCTS_BULK_MAX, aiProductsBulkBrief };
18
20
  /**
19
21
  * COMMERCE BY AI (AGL-2916): what a `products` job is asked, and what it
20
22
  * proposes. Pure, so the step that asks, the console cards that start a job
@@ -57,8 +59,6 @@ export declare const AI_PRODUCT_CATEGORY_LIST_MAX = 100;
57
59
  export declare const AI_PRODUCT_OPTIONS_MAX = 3;
58
60
  export declare const AI_PRODUCT_OPTION_VALUES_MAX = 25;
59
61
  export declare const AI_PRODUCT_OPTION_NAME_MAX_CHARS = 30;
60
- /** The most products one bulk job writes the copy of. */
61
- export declare const AI_PRODUCTS_BULK_MAX = 50;
62
62
  /** How many products a catalog proposal holds. */
63
63
  export declare const AI_CATALOG_PRODUCTS: {
64
64
  readonly min: 1;
@@ -275,7 +275,5 @@ export declare function aiProductsProposalOfKind<K extends AiProductsProposal['k
275
275
  export declare function aiProductGaps(...texts: readonly string[]): string[];
276
276
  /** The option the import zone sets to write the imported products' copy. */
277
277
  export declare const AI_PRODUCTS_IMPORT_WRITE_COPY = "ai.writeCopy";
278
- /** The brief a bulk copy job carries, which the jobs list shows and a card recognizes it by. */
279
- export declare const aiProductsBulkBrief: (count: number) => string;
280
278
  /** How many products a bulk copy job was asked for, read from its brief; `null` for any other job. */
281
279
  export declare function aiProductsBulkCount(job: Pick<AiJobSummary, 'brief'> | null | undefined): number | null;
@@ -13,7 +13,9 @@
13
13
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
- */ /**
16
+ */ import { AI_PRODUCTS_BULK_MAX, aiProductsBulkBrief, aiProductsBulkInputs } from "./ai-products-bulk.js";
17
+ export { AI_PRODUCTS_BULK_MAX, aiProductsBulkBrief };
18
+ /**
17
19
  * COMMERCE BY AI (AGL-2916): what a `products` job is asked, and what it
18
20
  * proposes. Pure, so the step that asks, the console cards that start a job
19
21
  * and show its proposals, and the eval harness all read one shape.
@@ -55,7 +57,6 @@ export const AI_PRODUCT_TAG_MAX_CHARS = 30;
55
57
  /** A product's option axes, as the commerce editor bounds them. */ export const AI_PRODUCT_OPTIONS_MAX = 3;
56
58
  export const AI_PRODUCT_OPTION_VALUES_MAX = 25;
57
59
  export const AI_PRODUCT_OPTION_NAME_MAX_CHARS = 30;
58
- /** The most products one bulk job writes the copy of. */ export const AI_PRODUCTS_BULK_MAX = 50;
59
60
  /** How many products a catalog proposal holds. */ export const AI_CATALOG_PRODUCTS = {
60
61
  min: 1,
61
62
  max: 12
@@ -181,12 +182,7 @@ function jsonOptions(value) {
181
182
  return inputs;
182
183
  }
183
184
  case 'bulk':
184
- return {
185
- target: 'bulk',
186
- productIds: [
187
- ...new Set(input.productIds.filter((id)=>ID.test(id)))
188
- ].slice(0, AI_PRODUCTS_BULK_MAX).join(',')
189
- };
185
+ return aiProductsBulkInputs(input.productIds);
190
186
  default:
191
187
  return {
192
188
  target: input.target
@@ -275,7 +271,6 @@ const isRecord = (value)=>typeof value === 'object' && value !== null && !Array.
275
271
  /* ------------------------------------------------------------------------ *
276
272
  * The console's cards
277
273
  * ------------------------------------------------------------------------ */ /** The option the import zone sets to write the imported products' copy. */ export const AI_PRODUCTS_IMPORT_WRITE_COPY = 'ai.writeCopy';
278
- /** The brief a bulk copy job carries, which the jobs list shows and a card recognizes it by. */ export const aiProductsBulkBrief = (count)=>`Write storefront copy for ${count} ${count === 1 ? 'product' : 'products'}`;
279
274
  const BULK_BRIEF = /^Write storefront copy for (\d+) products?$/;
280
275
  /** How many products a bulk copy job was asked for, read from its brief; `null` for any other job. */ export function aiProductsBulkCount(job) {
281
276
  var _ref;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-products.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { AiJobOutput, AiJobSummary } from './ai-jobs.types'\n\n/**\n * COMMERCE BY AI (AGL-2916): what a `products` job is asked, and what it\n * proposes. Pure, so the step that asks, the console cards that start a job\n * and show its proposals, and the eval harness all read one shape.\n *\n * `inputs.target` names the work:\n *\n * - `product` — one product's copy, from what its editor holds right now:\n * the name, the text, the tags, the categories and options it has, and the\n * first photo. The editor may hold a product nobody has saved yet, so the\n * job never reads a product document for this target.\n * - `bulk` — the copy of up to `AI_PRODUCTS_BULK_MAX` saved products, one\n * product a pass, each read from the catalog as it is reached.\n * - `catalog` — a store's first products from the brief: names, copy, tags\n * and options, what each photo should show, and a suggested price by what\n * the product is (`aiCatalogPrice`, Zach 2026-10-12). No picture.\n * - `categories` — store categories and a first set of discounts from the brief.\n *\n * Every output is a PROPOSAL (`resource: 'product'`, with `proposal`), and the\n * job writes nothing. The commerce plugin's own surfaces write what a person\n * accepts, through the doors those surfaces already use.\n */\n\nexport const AI_PRODUCTS_TARGETS = ['product', 'bulk', 'catalog', 'categories'] as const\nexport type AiProductsTarget = (typeof AI_PRODUCTS_TARGETS)[number]\n\nexport const AI_PRODUCT_TYPES = ['physical', 'digital', 'service'] as const\nexport type AiProductType = (typeof AI_PRODUCT_TYPES)[number]\n\n/** The longest product name the editor saves. */\nexport const AI_PRODUCT_NAME_MAX_CHARS = 120\n\n/** How much of a product's existing description a request carries. */\nexport const AI_PRODUCT_TEXT_MAX_CHARS = 2_000\n\n/** The longest description the job proposes. */\nexport const AI_PRODUCT_DESCRIPTION_MAX_CHARS = 1_200\n\nexport const AI_PRODUCT_TAGS_MAX = 8\nexport const AI_PRODUCT_TAG_MAX_CHARS = 30\n\n/** The most categories one product's proposal names. */\nexport const AI_PRODUCT_CATEGORIES_MAX = 3\n\n/** The most of a site's categories a request lists to choose from. */\nexport const AI_PRODUCT_CATEGORY_LIST_MAX = 100\n\n/** A product's option axes, as the commerce editor bounds them. */\nexport const AI_PRODUCT_OPTIONS_MAX = 3\nexport const AI_PRODUCT_OPTION_VALUES_MAX = 25\nexport const AI_PRODUCT_OPTION_NAME_MAX_CHARS = 30\n\n/** The most products one bulk job writes the copy of. */\nexport const AI_PRODUCTS_BULK_MAX = 50\n\n/** How many products a catalog proposal holds. */\nexport const AI_CATALOG_PRODUCTS = { min: 1, max: 12 } as const\n\n/**\n * A proposed product is shorter than a written one: its description and tags\n * are a start the owner completes, and twelve of them at every bound still fit\n * one answer (`ai-job-products-step.spec.ts` measures it).\n */\nexport const AI_CATALOG_DESCRIPTION_MAX_CHARS = 600\nexport const AI_CATALOG_TAGS_MAX = 6\n\n/** A proposed product's options: few axes and few short values, so its variants stay a short list. */\nexport const AI_CATALOG_OPTIONS_MAX = 2\nexport const AI_CATALOG_OPTION_VALUES_MAX = 6\nexport const AI_CATALOG_OPTION_VALUE_MAX_CHARS = 30\n\n/** What a proposed product's photo should show, in one sentence. */\nexport const AI_CATALOG_PHOTO_MAX_CHARS = 160\n\n/**\n * A proposed product's suggested price, in US dollars — the one currency a\n * catalog prices in (`priceUsd`) — and the range code keeps it to (Zach,\n * 2026-10-12): a store's first products priced by what each one is (a bar\n * of handmade soap about $8–12, a candle $18–32, a gift set $30–45) instead\n * of every one at the commerce default. Never $0 or less, nothing past what\n * a small shop's first product plausibly lists at; a price outside the range,\n * or none, is no price, and the writer gives the product its default price.\n * Below the commerce ceiling (`COMMERCE_MAX_PRICE_USD`, $10,000) by design.\n */\nexport const AI_CATALOG_PRICE_USD = { min: 1, max: 5_000 } as const\n\n/**\n * A proposed price as code keeps it: a finite number of dollars (or a plain\n * \"$12.50\" a model wrote as text) inside `AI_CATALOG_PRICE_USD`, rounded to\n * whole cents; anything else — missing, zero, negative, absurd, not a number\n * — is `null`, so the product takes the store's default price for the owner\n * to change. The model is never re-asked for a price: a bad one costs the\n * product only its suggestion.\n */\nexport function aiCatalogPrice(raw: unknown): number | null {\n let value: number | null = null\n if (typeof raw === 'number') value = raw\n else if (typeof raw === 'string') {\n const match = /^\\s*(?:US\\s*)?\\$?\\s*(\\d{1,6}(?:\\.\\d{1,2})?)\\s*(?:USD)?\\s*$/i.exec(raw)\n value = match ? Number(match[1]) : null\n }\n if (value === null || !Number.isFinite(value)) return null\n const cents = Math.round(value * 100)\n if (cents < AI_CATALOG_PRICE_USD.min * 100 || cents > AI_CATALOG_PRICE_USD.max * 100) return null\n return cents / 100\n}\n\nexport const AI_CATEGORIES_MAX = 12\nexport const AI_DISCOUNTS_MAX = 5\nexport const AI_CATEGORY_NAME_MAX_CHARS = 60\nexport const AI_PROPOSAL_WHY_MAX_CHARS = 200\n\n/** The highest percentage a proposed discount takes off, and the most a fixed one takes, in dollars. */\nexport const AI_DISCOUNT_MAX_PERCENT = 90\nexport const AI_DISCOUNT_MAX_FIXED_USD = 1_000\n\n/** A discount code: letters, digits, dashes and underscores, stored upper case. */\nexport const AI_DISCOUNT_CODE = /^[A-Z0-9_-]{3,20}$/\n\n/** An option axis. */\nexport interface AiProductOption {\n name: string\n values: string[]\n}\n\n/** One product as its editor hands it to a `product` job. */\nexport interface AiProductFacts {\n /** `null` for a product nobody has saved yet. */\n id: string | null\n name: string\n type: AiProductType\n /** The product's current description, cut to `AI_PRODUCT_TEXT_MAX_CHARS`. */\n text: string\n tags: string[]\n categoryIds: string[]\n options: AiProductOption[]\n /** The first photo's stored media value, or `null`. */\n imageUrl: string | null\n seoTitle: string\n seoDescription: string\n}\n\n/** A site's product category, as a request lists it to choose from. */\nexport interface AiProductCategory {\n id: string\n name: string\n}\n\n/** The merchant's own words about a product, which a claim in its copy may repeat. */\nexport function aiProductMerchantWords(product: Pick<AiProductFacts, 'name' | 'text' | 'tags'>): string {\n return [product.name, product.text, ...product.tags].join('\\n')\n}\n\nexport type AiProductsJobInputs =\n | { target: 'product'; product: AiProductFacts }\n | { target: 'bulk'; productIds: string[] }\n | { target: 'catalog' }\n | { target: 'categories' }\n\nconst ID = /^[A-Za-z0-9_-]{1,64}$/\n\nconst text = (value: unknown, max: number): string =>\n typeof value === 'string' ? value.replace(/\\s+/g, ' ').trim().slice(0, max) : ''\n\n/** A list stored as JSON, read defensively: strings only, trimmed, bounded. */\nfunction jsonStrings(value: unknown, max: number, maxChars: number): string[] {\n if (typeof value !== 'string' || !value.trim()) return []\n try {\n const parsed: unknown = JSON.parse(value)\n if (!Array.isArray(parsed)) return []\n return parsed\n .map((entry) => text(entry, maxChars))\n .filter(Boolean)\n .slice(0, max)\n } catch {\n return []\n }\n}\n\nfunction jsonOptions(value: unknown): AiProductOption[] {\n if (typeof value !== 'string' || !value.trim()) return []\n try {\n const parsed: unknown = JSON.parse(value)\n if (!Array.isArray(parsed)) return []\n return parsed\n .map((entry) => {\n const record = (entry ?? {}) as Record<string, unknown>\n return {\n name: text(record['name'], AI_PRODUCT_OPTION_NAME_MAX_CHARS),\n values: Array.isArray(record['values'])\n ? record['values'].map((value) => text(value, 40)).filter(Boolean).slice(0, AI_PRODUCT_OPTION_VALUES_MAX)\n : [],\n }\n })\n .slice(0, AI_PRODUCT_OPTIONS_MAX)\n } catch {\n return []\n }\n}\n\n/**\n * How large a job's inputs may be once written as JSON: under the 8,000\n * characters the create door admits, with room for the door's own keys.\n */\nexport const AI_PRODUCTS_INPUTS_MAX_JSON_CHARS = 7_500\n\n/**\n * A job's inputs for a target, as the console sends them: every value a\n * scalar, lists as JSON or as comma-separated ids, each bounded, and the\n * product's text shortened last until the whole fits what the create door\n * admits.\n */\nexport function aiProductsJobInputs(input: AiProductsJobInputs): Record<string, string> {\n switch (input.target) {\n case 'product': {\n const product = input.product\n const inputs: Record<string, string> = {\n target: 'product',\n productId: product.id ?? '',\n name: text(product.name, AI_PRODUCT_NAME_MAX_CHARS),\n type: product.type,\n text: product.text.slice(0, AI_PRODUCT_TEXT_MAX_CHARS),\n tags: JSON.stringify(product.tags.slice(0, 10).map((tag) => tag.slice(0, 40))),\n categoryIds: product.categoryIds.filter((id) => ID.test(id)).slice(0, 10).join(','),\n options: JSON.stringify(\n product.options.slice(0, AI_PRODUCT_OPTIONS_MAX).map((option) => ({\n name: option.name.slice(0, AI_PRODUCT_OPTION_NAME_MAX_CHARS),\n values: option.values.slice(0, AI_PRODUCT_OPTION_VALUES_MAX).map((value) => value.slice(0, 40)),\n })),\n ),\n imageUrl: (product.imageUrl ?? '').slice(0, 300),\n seoTitle: product.seoTitle.slice(0, 100),\n seoDescription: product.seoDescription.slice(0, 200),\n }\n while (JSON.stringify(inputs).length > AI_PRODUCTS_INPUTS_MAX_JSON_CHARS && inputs['text']) {\n inputs['text'] = inputs['text'].slice(0, Math.max(0, inputs['text'].length - 250))\n }\n return inputs\n }\n case 'bulk':\n return {\n target: 'bulk',\n productIds: [...new Set(input.productIds.filter((id) => ID.test(id)))]\n .slice(0, AI_PRODUCTS_BULK_MAX)\n .join(','),\n }\n default:\n return { target: input.target }\n }\n}\n\n/** Why a `products` job's inputs cannot run, in words a person can act on. */\nexport const AI_PRODUCTS_NO_TARGET_COPY = 'This AI job does not say what to write for your products.'\nexport const AI_PRODUCTS_NO_NAME_COPY = 'Give the product a name before writing its copy.'\nexport const AI_PRODUCTS_NO_SELECTION_COPY = 'Pick the products to write copy for.'\nexport const AI_PRODUCTS_TOO_MANY_COPY = `Pick at most ${AI_PRODUCTS_BULK_MAX} products at a time.`\n\n/** A job's inputs read back, or the sentence that says why they cannot run. */\nexport function parseAiProductsJobInputs(raw: Readonly<Record<string, unknown>> | undefined): AiProductsJobInputs | string {\n const inputs = raw ?? {}\n const target = inputs['target']\n if (!(AI_PRODUCTS_TARGETS as readonly unknown[]).includes(target)) return AI_PRODUCTS_NO_TARGET_COPY\n if (target === 'product') {\n const name = text(inputs['name'], AI_PRODUCT_NAME_MAX_CHARS)\n if (!name) return AI_PRODUCTS_NO_NAME_COPY\n const type = inputs['type']\n const id = typeof inputs['productId'] === 'string' && ID.test(inputs['productId']) ? inputs['productId'] : null\n return {\n target,\n product: {\n id,\n name,\n type: (AI_PRODUCT_TYPES as readonly unknown[]).includes(type) ? (type as AiProductType) : 'physical',\n text: typeof inputs['text'] === 'string' ? inputs['text'].trim().slice(0, AI_PRODUCT_TEXT_MAX_CHARS) : '',\n tags: jsonStrings(inputs['tags'], 10, 40),\n categoryIds: String(inputs['categoryIds'] ?? '')\n .split(',')\n .map((part) => part.trim())\n .filter((part) => ID.test(part))\n .slice(0, 10),\n options: jsonOptions(inputs['options']),\n imageUrl: text(inputs['imageUrl'], 300) || null,\n seoTitle: text(inputs['seoTitle'], 100),\n seoDescription: text(inputs['seoDescription'], 200),\n },\n }\n }\n if (target === 'bulk') {\n const ids = String(inputs['productIds'] ?? '')\n .split(',')\n .map((part) => part.trim())\n .filter((part) => ID.test(part))\n const unique = [...new Set(ids)]\n if (!unique.length) return AI_PRODUCTS_NO_SELECTION_COPY\n if (unique.length > AI_PRODUCTS_BULK_MAX) return AI_PRODUCTS_TOO_MANY_COPY\n return { target, productIds: unique }\n }\n return { target: target as 'catalog' | 'categories' }\n}\n\n/* ------------------------------------------------------------------------ *\n * Proposals\n * ------------------------------------------------------------------------ */\n\n/** A product's copy, as proposed. */\nexport interface AiProductCopy {\n description: string\n seoTitle: string\n seoDescription: string\n tags: string[]\n categoryIds: string[]\n /** One name per option the product had, in order. */\n optionNames: string[]\n}\n\n/** What became of the product's photo. */\nexport type AiProductPhotoRead =\n /** The photo was read. */\n | 'read'\n /** The product has no photo. */\n | 'none'\n /** The photo is not an asset of this site's media library, or its org's. */\n | 'not-in-library'\n /** The photo could not be read. */\n | 'unreadable'\n /** The model this job ran on does not read pictures. */\n | 'model'\n\nexport interface AiProductCopyProposal {\n kind: 'copy'\n product: { id: string | null; name: string }\n /** `null` when there was nothing to propose: see `skipped`. */\n values: AiProductCopy | null\n /** The categories `values` names, with the names they had when the job read them. */\n categories: Array<{ id: string; name: string }>\n /** The option names the product had, beside the ones proposed. */\n optionNamesBefore: string[]\n /** The facts the copy marks as missing, in square brackets, for a person to fill in. */\n gaps: string[]\n photo: AiProductPhotoRead\n /** Why a bulk pass proposed nothing for this product, in customer-safe words. */\n skipped: string | null\n notes: string[]\n}\n\nexport interface AiCatalogProduct {\n name: string\n type: AiProductType\n description: string\n tags: string[]\n options: AiProductOption[]\n seoTitle: string\n seoDescription: string\n /** What the product's photo should show; nothing is uploaded or linked. */\n photo: string\n /**\n * The suggested price for every variant, in US dollars, as `aiCatalogPrice`\n * kept it; `null` (or absent, on a proposal made before 2026-10-12) when\n * the model gave none in range, and the store's default price stands in.\n */\n price?: number | null\n}\n\nexport interface AiCatalogProposal {\n kind: 'catalog'\n products: AiCatalogProduct[]\n notes: string[]\n}\n\nexport type AiDiscountKind = 'percent' | 'fixed' | 'free_shipping'\n\nexport interface AiProposedDiscount {\n name: string\n /** The code a shopper types, upper case; `null` for an automatic discount. */\n code: string | null\n kind: AiDiscountKind\n /** A `percent` discount's percentage. */\n valuePct: number | null\n /** A `fixed` discount's amount off, in cents. */\n valueCents: number | null\n /** The smallest order it applies to, in cents; `null` for any order. */\n minSubtotalCents: number | null\n why: string\n}\n\nexport interface AiCategoriesProposal {\n kind: 'categories'\n categories: Array<{ name: string; why: string }>\n discounts: AiProposedDiscount[]\n notes: string[]\n}\n\nexport type AiProductsProposal = AiProductCopyProposal | AiCatalogProposal | AiCategoriesProposal\n\n/** Output ids, so a pass that runs again finds what it already proposed. */\nexport const AI_PRODUCTS_OUTPUT_IDS = {\n copy: (productId: string | null) => `copy:${productId ?? 'new'}`,\n catalog: 'catalog',\n categories: 'categories',\n} as const\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value)\n\n/** An output's proposal, or `null` for an output that is not a products proposal. */\nexport function aiProductsProposalOf(output: Pick<AiJobOutput, 'resource' | 'proposal'> | null | undefined): AiProductsProposal | null {\n if (!output || output.resource !== 'product' || !isRecord(output.proposal)) return null\n const kind = output.proposal['kind']\n return kind === 'copy' || kind === 'catalog' || kind === 'categories'\n ? (output.proposal as unknown as AiProductsProposal)\n : null\n}\n\n/** A job's copy proposals, in the order the job made them. */\nexport function aiProductCopyProposals(job: Pick<AiJobSummary, 'outputs'> | null | undefined): AiProductCopyProposal[] {\n return (job?.outputs ?? [])\n .map((output) => aiProductsProposalOf(output))\n .filter((proposal): proposal is AiProductCopyProposal => proposal?.kind === 'copy')\n}\n\n/** A job's proposal of one kind, or `null`. */\nexport function aiProductsProposalOfKind<K extends AiProductsProposal['kind']>(\n job: Pick<AiJobSummary, 'outputs'> | null | undefined,\n kind: K,\n): Extract<AiProductsProposal, { kind: K }> | null {\n for (const output of job?.outputs ?? []) {\n const proposal = aiProductsProposalOf(output)\n if (proposal?.kind === kind) return proposal as Extract<AiProductsProposal, { kind: K }>\n }\n return null\n}\n\n/** The gaps a text marks for a person in square brackets, as `[material]`, each once. */\nexport function aiProductGaps(...texts: readonly string[]): string[] {\n const found = new Set<string>()\n for (const value of texts) {\n for (const match of value.matchAll(/\\[([^\\]\\n]{1,60})\\]/g)) found.add(match[1].trim())\n }\n return [...found].filter(Boolean)\n}\n\n/* ------------------------------------------------------------------------ *\n * The console's cards\n * ------------------------------------------------------------------------ */\n\n/** The option the import zone sets to write the imported products' copy. */\nexport const AI_PRODUCTS_IMPORT_WRITE_COPY = 'ai.writeCopy'\n\n/** The brief a bulk copy job carries, which the jobs list shows and a card recognizes it by. */\nexport const aiProductsBulkBrief = (count: number): string =>\n `Write storefront copy for ${count} ${count === 1 ? 'product' : 'products'}`\n\nconst BULK_BRIEF = /^Write storefront copy for (\\d+) products?$/\n\n/** How many products a bulk copy job was asked for, read from its brief; `null` for any other job. */\nexport function aiProductsBulkCount(job: Pick<AiJobSummary, 'brief'> | null | undefined): number | null {\n const match = BULK_BRIEF.exec(job?.brief ?? '')\n return match ? Number(match[1]) : null\n}\n"],"names":["AI_PRODUCTS_TARGETS","AI_PRODUCT_TYPES","AI_PRODUCT_NAME_MAX_CHARS","AI_PRODUCT_TEXT_MAX_CHARS","AI_PRODUCT_DESCRIPTION_MAX_CHARS","AI_PRODUCT_TAGS_MAX","AI_PRODUCT_TAG_MAX_CHARS","AI_PRODUCT_CATEGORIES_MAX","AI_PRODUCT_CATEGORY_LIST_MAX","AI_PRODUCT_OPTIONS_MAX","AI_PRODUCT_OPTION_VALUES_MAX","AI_PRODUCT_OPTION_NAME_MAX_CHARS","AI_PRODUCTS_BULK_MAX","AI_CATALOG_PRODUCTS","min","max","AI_CATALOG_DESCRIPTION_MAX_CHARS","AI_CATALOG_TAGS_MAX","AI_CATALOG_OPTIONS_MAX","AI_CATALOG_OPTION_VALUES_MAX","AI_CATALOG_OPTION_VALUE_MAX_CHARS","AI_CATALOG_PHOTO_MAX_CHARS","AI_CATALOG_PRICE_USD","aiCatalogPrice","raw","value","match","exec","Number","isFinite","cents","Math","round","AI_CATEGORIES_MAX","AI_DISCOUNTS_MAX","AI_CATEGORY_NAME_MAX_CHARS","AI_PROPOSAL_WHY_MAX_CHARS","AI_DISCOUNT_MAX_PERCENT","AI_DISCOUNT_MAX_FIXED_USD","AI_DISCOUNT_CODE","aiProductMerchantWords","product","name","text","tags","join","ID","replace","trim","slice","jsonStrings","maxChars","parsed","JSON","parse","Array","isArray","map","entry","filter","Boolean","jsonOptions","record","values","AI_PRODUCTS_INPUTS_MAX_JSON_CHARS","aiProductsJobInputs","input","target","inputs","productId","id","type","stringify","tag","categoryIds","test","options","option","imageUrl","seoTitle","seoDescription","length","productIds","Set","AI_PRODUCTS_NO_TARGET_COPY","AI_PRODUCTS_NO_NAME_COPY","AI_PRODUCTS_NO_SELECTION_COPY","AI_PRODUCTS_TOO_MANY_COPY","parseAiProductsJobInputs","includes","String","split","part","ids","unique","AI_PRODUCTS_OUTPUT_IDS","copy","catalog","categories","isRecord","aiProductsProposalOf","output","resource","proposal","kind","aiProductCopyProposals","job","outputs","aiProductsProposalOfKind","aiProductGaps","texts","found","matchAll","add","AI_PRODUCTS_IMPORT_WRITE_COPY","aiProductsBulkBrief","count","BULK_BRIEF","aiProductsBulkCount","brief"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-products.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { AiJobOutput, AiJobSummary } from './ai-jobs.types'\nimport { AI_PRODUCTS_BULK_MAX, aiProductsBulkBrief, aiProductsBulkInputs } from './ai-products-bulk'\n\nexport { AI_PRODUCTS_BULK_MAX, aiProductsBulkBrief }\n\n/**\n * COMMERCE BY AI (AGL-2916): what a `products` job is asked, and what it\n * proposes. Pure, so the step that asks, the console cards that start a job\n * and show its proposals, and the eval harness all read one shape.\n *\n * `inputs.target` names the work:\n *\n * - `product` — one product's copy, from what its editor holds right now:\n * the name, the text, the tags, the categories and options it has, and the\n * first photo. The editor may hold a product nobody has saved yet, so the\n * job never reads a product document for this target.\n * - `bulk` — the copy of up to `AI_PRODUCTS_BULK_MAX` saved products, one\n * product a pass, each read from the catalog as it is reached.\n * - `catalog` — a store's first products from the brief: names, copy, tags\n * and options, what each photo should show, and a suggested price by what\n * the product is (`aiCatalogPrice`, Zach 2026-10-12). No picture.\n * - `categories` — store categories and a first set of discounts from the brief.\n *\n * Every output is a PROPOSAL (`resource: 'product'`, with `proposal`), and the\n * job writes nothing. The commerce plugin's own surfaces write what a person\n * accepts, through the doors those surfaces already use.\n */\n\nexport const AI_PRODUCTS_TARGETS = ['product', 'bulk', 'catalog', 'categories'] as const\nexport type AiProductsTarget = (typeof AI_PRODUCTS_TARGETS)[number]\n\nexport const AI_PRODUCT_TYPES = ['physical', 'digital', 'service'] as const\nexport type AiProductType = (typeof AI_PRODUCT_TYPES)[number]\n\n/** The longest product name the editor saves. */\nexport const AI_PRODUCT_NAME_MAX_CHARS = 120\n\n/** How much of a product's existing description a request carries. */\nexport const AI_PRODUCT_TEXT_MAX_CHARS = 2_000\n\n/** The longest description the job proposes. */\nexport const AI_PRODUCT_DESCRIPTION_MAX_CHARS = 1_200\n\nexport const AI_PRODUCT_TAGS_MAX = 8\nexport const AI_PRODUCT_TAG_MAX_CHARS = 30\n\n/** The most categories one product's proposal names. */\nexport const AI_PRODUCT_CATEGORIES_MAX = 3\n\n/** The most of a site's categories a request lists to choose from. */\nexport const AI_PRODUCT_CATEGORY_LIST_MAX = 100\n\n/** A product's option axes, as the commerce editor bounds them. */\nexport const AI_PRODUCT_OPTIONS_MAX = 3\nexport const AI_PRODUCT_OPTION_VALUES_MAX = 25\nexport const AI_PRODUCT_OPTION_NAME_MAX_CHARS = 30\n\n/** How many products a catalog proposal holds. */\nexport const AI_CATALOG_PRODUCTS = { min: 1, max: 12 } as const\n\n/**\n * A proposed product is shorter than a written one: its description and tags\n * are a start the owner completes, and twelve of them at every bound still fit\n * one answer (`ai-job-products-step.spec.ts` measures it).\n */\nexport const AI_CATALOG_DESCRIPTION_MAX_CHARS = 600\nexport const AI_CATALOG_TAGS_MAX = 6\n\n/** A proposed product's options: few axes and few short values, so its variants stay a short list. */\nexport const AI_CATALOG_OPTIONS_MAX = 2\nexport const AI_CATALOG_OPTION_VALUES_MAX = 6\nexport const AI_CATALOG_OPTION_VALUE_MAX_CHARS = 30\n\n/** What a proposed product's photo should show, in one sentence. */\nexport const AI_CATALOG_PHOTO_MAX_CHARS = 160\n\n/**\n * A proposed product's suggested price, in US dollars — the one currency a\n * catalog prices in (`priceUsd`) — and the range code keeps it to (Zach,\n * 2026-10-12): a store's first products priced by what each one is (a bar\n * of handmade soap about $8–12, a candle $18–32, a gift set $30–45) instead\n * of every one at the commerce default. Never $0 or less, nothing past what\n * a small shop's first product plausibly lists at; a price outside the range,\n * or none, is no price, and the writer gives the product its default price.\n * Below the commerce ceiling (`COMMERCE_MAX_PRICE_USD`, $10,000) by design.\n */\nexport const AI_CATALOG_PRICE_USD = { min: 1, max: 5_000 } as const\n\n/**\n * A proposed price as code keeps it: a finite number of dollars (or a plain\n * \"$12.50\" a model wrote as text) inside `AI_CATALOG_PRICE_USD`, rounded to\n * whole cents; anything else — missing, zero, negative, absurd, not a number\n * — is `null`, so the product takes the store's default price for the owner\n * to change. The model is never re-asked for a price: a bad one costs the\n * product only its suggestion.\n */\nexport function aiCatalogPrice(raw: unknown): number | null {\n let value: number | null = null\n if (typeof raw === 'number') value = raw\n else if (typeof raw === 'string') {\n const match = /^\\s*(?:US\\s*)?\\$?\\s*(\\d{1,6}(?:\\.\\d{1,2})?)\\s*(?:USD)?\\s*$/i.exec(raw)\n value = match ? Number(match[1]) : null\n }\n if (value === null || !Number.isFinite(value)) return null\n const cents = Math.round(value * 100)\n if (cents < AI_CATALOG_PRICE_USD.min * 100 || cents > AI_CATALOG_PRICE_USD.max * 100) return null\n return cents / 100\n}\n\nexport const AI_CATEGORIES_MAX = 12\nexport const AI_DISCOUNTS_MAX = 5\nexport const AI_CATEGORY_NAME_MAX_CHARS = 60\nexport const AI_PROPOSAL_WHY_MAX_CHARS = 200\n\n/** The highest percentage a proposed discount takes off, and the most a fixed one takes, in dollars. */\nexport const AI_DISCOUNT_MAX_PERCENT = 90\nexport const AI_DISCOUNT_MAX_FIXED_USD = 1_000\n\n/** A discount code: letters, digits, dashes and underscores, stored upper case. */\nexport const AI_DISCOUNT_CODE = /^[A-Z0-9_-]{3,20}$/\n\n/** An option axis. */\nexport interface AiProductOption {\n name: string\n values: string[]\n}\n\n/** One product as its editor hands it to a `product` job. */\nexport interface AiProductFacts {\n /** `null` for a product nobody has saved yet. */\n id: string | null\n name: string\n type: AiProductType\n /** The product's current description, cut to `AI_PRODUCT_TEXT_MAX_CHARS`. */\n text: string\n tags: string[]\n categoryIds: string[]\n options: AiProductOption[]\n /** The first photo's stored media value, or `null`. */\n imageUrl: string | null\n seoTitle: string\n seoDescription: string\n}\n\n/** A site's product category, as a request lists it to choose from. */\nexport interface AiProductCategory {\n id: string\n name: string\n}\n\n/** The merchant's own words about a product, which a claim in its copy may repeat. */\nexport function aiProductMerchantWords(product: Pick<AiProductFacts, 'name' | 'text' | 'tags'>): string {\n return [product.name, product.text, ...product.tags].join('\\n')\n}\n\nexport type AiProductsJobInputs =\n | { target: 'product'; product: AiProductFacts }\n | { target: 'bulk'; productIds: string[] }\n | { target: 'catalog' }\n | { target: 'categories' }\n\nconst ID = /^[A-Za-z0-9_-]{1,64}$/\n\nconst text = (value: unknown, max: number): string =>\n typeof value === 'string' ? value.replace(/\\s+/g, ' ').trim().slice(0, max) : ''\n\n/** A list stored as JSON, read defensively: strings only, trimmed, bounded. */\nfunction jsonStrings(value: unknown, max: number, maxChars: number): string[] {\n if (typeof value !== 'string' || !value.trim()) return []\n try {\n const parsed: unknown = JSON.parse(value)\n if (!Array.isArray(parsed)) return []\n return parsed\n .map((entry) => text(entry, maxChars))\n .filter(Boolean)\n .slice(0, max)\n } catch {\n return []\n }\n}\n\nfunction jsonOptions(value: unknown): AiProductOption[] {\n if (typeof value !== 'string' || !value.trim()) return []\n try {\n const parsed: unknown = JSON.parse(value)\n if (!Array.isArray(parsed)) return []\n return parsed\n .map((entry) => {\n const record = (entry ?? {}) as Record<string, unknown>\n return {\n name: text(record['name'], AI_PRODUCT_OPTION_NAME_MAX_CHARS),\n values: Array.isArray(record['values'])\n ? record['values'].map((value) => text(value, 40)).filter(Boolean).slice(0, AI_PRODUCT_OPTION_VALUES_MAX)\n : [],\n }\n })\n .slice(0, AI_PRODUCT_OPTIONS_MAX)\n } catch {\n return []\n }\n}\n\n/**\n * How large a job's inputs may be once written as JSON: under the 8,000\n * characters the create door admits, with room for the door's own keys.\n */\nexport const AI_PRODUCTS_INPUTS_MAX_JSON_CHARS = 7_500\n\n/**\n * A job's inputs for a target, as the console sends them: every value a\n * scalar, lists as JSON or as comma-separated ids, each bounded, and the\n * product's text shortened last until the whole fits what the create door\n * admits.\n */\nexport function aiProductsJobInputs(input: AiProductsJobInputs): Record<string, string> {\n switch (input.target) {\n case 'product': {\n const product = input.product\n const inputs: Record<string, string> = {\n target: 'product',\n productId: product.id ?? '',\n name: text(product.name, AI_PRODUCT_NAME_MAX_CHARS),\n type: product.type,\n text: product.text.slice(0, AI_PRODUCT_TEXT_MAX_CHARS),\n tags: JSON.stringify(product.tags.slice(0, 10).map((tag) => tag.slice(0, 40))),\n categoryIds: product.categoryIds.filter((id) => ID.test(id)).slice(0, 10).join(','),\n options: JSON.stringify(\n product.options.slice(0, AI_PRODUCT_OPTIONS_MAX).map((option) => ({\n name: option.name.slice(0, AI_PRODUCT_OPTION_NAME_MAX_CHARS),\n values: option.values.slice(0, AI_PRODUCT_OPTION_VALUES_MAX).map((value) => value.slice(0, 40)),\n })),\n ),\n imageUrl: (product.imageUrl ?? '').slice(0, 300),\n seoTitle: product.seoTitle.slice(0, 100),\n seoDescription: product.seoDescription.slice(0, 200),\n }\n while (JSON.stringify(inputs).length > AI_PRODUCTS_INPUTS_MAX_JSON_CHARS && inputs['text']) {\n inputs['text'] = inputs['text'].slice(0, Math.max(0, inputs['text'].length - 250))\n }\n return inputs\n }\n case 'bulk':\n return aiProductsBulkInputs(input.productIds)\n default:\n return { target: input.target }\n }\n}\n\n/** Why a `products` job's inputs cannot run, in words a person can act on. */\nexport const AI_PRODUCTS_NO_TARGET_COPY = 'This AI job does not say what to write for your products.'\nexport const AI_PRODUCTS_NO_NAME_COPY = 'Give the product a name before writing its copy.'\nexport const AI_PRODUCTS_NO_SELECTION_COPY = 'Pick the products to write copy for.'\nexport const AI_PRODUCTS_TOO_MANY_COPY = `Pick at most ${AI_PRODUCTS_BULK_MAX} products at a time.`\n\n/** A job's inputs read back, or the sentence that says why they cannot run. */\nexport function parseAiProductsJobInputs(raw: Readonly<Record<string, unknown>> | undefined): AiProductsJobInputs | string {\n const inputs = raw ?? {}\n const target = inputs['target']\n if (!(AI_PRODUCTS_TARGETS as readonly unknown[]).includes(target)) return AI_PRODUCTS_NO_TARGET_COPY\n if (target === 'product') {\n const name = text(inputs['name'], AI_PRODUCT_NAME_MAX_CHARS)\n if (!name) return AI_PRODUCTS_NO_NAME_COPY\n const type = inputs['type']\n const id = typeof inputs['productId'] === 'string' && ID.test(inputs['productId']) ? inputs['productId'] : null\n return {\n target,\n product: {\n id,\n name,\n type: (AI_PRODUCT_TYPES as readonly unknown[]).includes(type) ? (type as AiProductType) : 'physical',\n text: typeof inputs['text'] === 'string' ? inputs['text'].trim().slice(0, AI_PRODUCT_TEXT_MAX_CHARS) : '',\n tags: jsonStrings(inputs['tags'], 10, 40),\n categoryIds: String(inputs['categoryIds'] ?? '')\n .split(',')\n .map((part) => part.trim())\n .filter((part) => ID.test(part))\n .slice(0, 10),\n options: jsonOptions(inputs['options']),\n imageUrl: text(inputs['imageUrl'], 300) || null,\n seoTitle: text(inputs['seoTitle'], 100),\n seoDescription: text(inputs['seoDescription'], 200),\n },\n }\n }\n if (target === 'bulk') {\n const ids = String(inputs['productIds'] ?? '')\n .split(',')\n .map((part) => part.trim())\n .filter((part) => ID.test(part))\n const unique = [...new Set(ids)]\n if (!unique.length) return AI_PRODUCTS_NO_SELECTION_COPY\n if (unique.length > AI_PRODUCTS_BULK_MAX) return AI_PRODUCTS_TOO_MANY_COPY\n return { target, productIds: unique }\n }\n return { target: target as 'catalog' | 'categories' }\n}\n\n/* ------------------------------------------------------------------------ *\n * Proposals\n * ------------------------------------------------------------------------ */\n\n/** A product's copy, as proposed. */\nexport interface AiProductCopy {\n description: string\n seoTitle: string\n seoDescription: string\n tags: string[]\n categoryIds: string[]\n /** One name per option the product had, in order. */\n optionNames: string[]\n}\n\n/** What became of the product's photo. */\nexport type AiProductPhotoRead =\n /** The photo was read. */\n | 'read'\n /** The product has no photo. */\n | 'none'\n /** The photo is not an asset of this site's media library, or its org's. */\n | 'not-in-library'\n /** The photo could not be read. */\n | 'unreadable'\n /** The model this job ran on does not read pictures. */\n | 'model'\n\nexport interface AiProductCopyProposal {\n kind: 'copy'\n product: { id: string | null; name: string }\n /** `null` when there was nothing to propose: see `skipped`. */\n values: AiProductCopy | null\n /** The categories `values` names, with the names they had when the job read them. */\n categories: Array<{ id: string; name: string }>\n /** The option names the product had, beside the ones proposed. */\n optionNamesBefore: string[]\n /** The facts the copy marks as missing, in square brackets, for a person to fill in. */\n gaps: string[]\n photo: AiProductPhotoRead\n /** Why a bulk pass proposed nothing for this product, in customer-safe words. */\n skipped: string | null\n notes: string[]\n}\n\nexport interface AiCatalogProduct {\n name: string\n type: AiProductType\n description: string\n tags: string[]\n options: AiProductOption[]\n seoTitle: string\n seoDescription: string\n /** What the product's photo should show; nothing is uploaded or linked. */\n photo: string\n /**\n * The suggested price for every variant, in US dollars, as `aiCatalogPrice`\n * kept it; `null` (or absent, on a proposal made before 2026-10-12) when\n * the model gave none in range, and the store's default price stands in.\n */\n price?: number | null\n}\n\nexport interface AiCatalogProposal {\n kind: 'catalog'\n products: AiCatalogProduct[]\n notes: string[]\n}\n\nexport type AiDiscountKind = 'percent' | 'fixed' | 'free_shipping'\n\nexport interface AiProposedDiscount {\n name: string\n /** The code a shopper types, upper case; `null` for an automatic discount. */\n code: string | null\n kind: AiDiscountKind\n /** A `percent` discount's percentage. */\n valuePct: number | null\n /** A `fixed` discount's amount off, in cents. */\n valueCents: number | null\n /** The smallest order it applies to, in cents; `null` for any order. */\n minSubtotalCents: number | null\n why: string\n}\n\nexport interface AiCategoriesProposal {\n kind: 'categories'\n categories: Array<{ name: string; why: string }>\n discounts: AiProposedDiscount[]\n notes: string[]\n}\n\nexport type AiProductsProposal = AiProductCopyProposal | AiCatalogProposal | AiCategoriesProposal\n\n/** Output ids, so a pass that runs again finds what it already proposed. */\nexport const AI_PRODUCTS_OUTPUT_IDS = {\n copy: (productId: string | null) => `copy:${productId ?? 'new'}`,\n catalog: 'catalog',\n categories: 'categories',\n} as const\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value)\n\n/** An output's proposal, or `null` for an output that is not a products proposal. */\nexport function aiProductsProposalOf(output: Pick<AiJobOutput, 'resource' | 'proposal'> | null | undefined): AiProductsProposal | null {\n if (!output || output.resource !== 'product' || !isRecord(output.proposal)) return null\n const kind = output.proposal['kind']\n return kind === 'copy' || kind === 'catalog' || kind === 'categories'\n ? (output.proposal as unknown as AiProductsProposal)\n : null\n}\n\n/** A job's copy proposals, in the order the job made them. */\nexport function aiProductCopyProposals(job: Pick<AiJobSummary, 'outputs'> | null | undefined): AiProductCopyProposal[] {\n return (job?.outputs ?? [])\n .map((output) => aiProductsProposalOf(output))\n .filter((proposal): proposal is AiProductCopyProposal => proposal?.kind === 'copy')\n}\n\n/** A job's proposal of one kind, or `null`. */\nexport function aiProductsProposalOfKind<K extends AiProductsProposal['kind']>(\n job: Pick<AiJobSummary, 'outputs'> | null | undefined,\n kind: K,\n): Extract<AiProductsProposal, { kind: K }> | null {\n for (const output of job?.outputs ?? []) {\n const proposal = aiProductsProposalOf(output)\n if (proposal?.kind === kind) return proposal as Extract<AiProductsProposal, { kind: K }>\n }\n return null\n}\n\n/** The gaps a text marks for a person in square brackets, as `[material]`, each once. */\nexport function aiProductGaps(...texts: readonly string[]): string[] {\n const found = new Set<string>()\n for (const value of texts) {\n for (const match of value.matchAll(/\\[([^\\]\\n]{1,60})\\]/g)) found.add(match[1].trim())\n }\n return [...found].filter(Boolean)\n}\n\n/* ------------------------------------------------------------------------ *\n * The console's cards\n * ------------------------------------------------------------------------ */\n\n/** The option the import zone sets to write the imported products' copy. */\nexport const AI_PRODUCTS_IMPORT_WRITE_COPY = 'ai.writeCopy'\n\nconst BULK_BRIEF = /^Write storefront copy for (\\d+) products?$/\n\n/** How many products a bulk copy job was asked for, read from its brief; `null` for any other job. */\nexport function aiProductsBulkCount(job: Pick<AiJobSummary, 'brief'> | null | undefined): number | null {\n const match = BULK_BRIEF.exec(job?.brief ?? '')\n return match ? Number(match[1]) : null\n}\n"],"names":["AI_PRODUCTS_BULK_MAX","aiProductsBulkBrief","aiProductsBulkInputs","AI_PRODUCTS_TARGETS","AI_PRODUCT_TYPES","AI_PRODUCT_NAME_MAX_CHARS","AI_PRODUCT_TEXT_MAX_CHARS","AI_PRODUCT_DESCRIPTION_MAX_CHARS","AI_PRODUCT_TAGS_MAX","AI_PRODUCT_TAG_MAX_CHARS","AI_PRODUCT_CATEGORIES_MAX","AI_PRODUCT_CATEGORY_LIST_MAX","AI_PRODUCT_OPTIONS_MAX","AI_PRODUCT_OPTION_VALUES_MAX","AI_PRODUCT_OPTION_NAME_MAX_CHARS","AI_CATALOG_PRODUCTS","min","max","AI_CATALOG_DESCRIPTION_MAX_CHARS","AI_CATALOG_TAGS_MAX","AI_CATALOG_OPTIONS_MAX","AI_CATALOG_OPTION_VALUES_MAX","AI_CATALOG_OPTION_VALUE_MAX_CHARS","AI_CATALOG_PHOTO_MAX_CHARS","AI_CATALOG_PRICE_USD","aiCatalogPrice","raw","value","match","exec","Number","isFinite","cents","Math","round","AI_CATEGORIES_MAX","AI_DISCOUNTS_MAX","AI_CATEGORY_NAME_MAX_CHARS","AI_PROPOSAL_WHY_MAX_CHARS","AI_DISCOUNT_MAX_PERCENT","AI_DISCOUNT_MAX_FIXED_USD","AI_DISCOUNT_CODE","aiProductMerchantWords","product","name","text","tags","join","ID","replace","trim","slice","jsonStrings","maxChars","parsed","JSON","parse","Array","isArray","map","entry","filter","Boolean","jsonOptions","record","values","AI_PRODUCTS_INPUTS_MAX_JSON_CHARS","aiProductsJobInputs","input","target","inputs","productId","id","type","stringify","tag","categoryIds","test","options","option","imageUrl","seoTitle","seoDescription","length","productIds","AI_PRODUCTS_NO_TARGET_COPY","AI_PRODUCTS_NO_NAME_COPY","AI_PRODUCTS_NO_SELECTION_COPY","AI_PRODUCTS_TOO_MANY_COPY","parseAiProductsJobInputs","includes","String","split","part","ids","unique","Set","AI_PRODUCTS_OUTPUT_IDS","copy","catalog","categories","isRecord","aiProductsProposalOf","output","resource","proposal","kind","aiProductCopyProposals","job","outputs","aiProductsProposalOfKind","aiProductGaps","texts","found","matchAll","add","AI_PRODUCTS_IMPORT_WRITE_COPY","BULK_BRIEF","aiProductsBulkCount","brief"],"mappings":"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"}
@@ -111,6 +111,12 @@ export interface AiFreeSiteWorstCase {
111
111
  retry: number;
112
112
  /** A page's listing. */
113
113
  listing: number;
114
+ /**
115
+ * A Free blog's one first post (`AI_SITE_FREE_POSTS`): its answer at the
116
+ * `copy.blog` ceiling writing its cached prefix, and its one re-ask reading
117
+ * it, on the fast tier a Free site's post is written on.
118
+ */
119
+ post: number;
114
120
  }
115
121
  /**
116
122
  * The Free site start's wall at its worst (AGL-3594, re-derived for the
@@ -134,7 +140,13 @@ export interface AiFreeSiteCreations {
134
140
  pages: number;
135
141
  /** The saved forms it makes; a guided start makes one where it places a form. */
136
142
  forms: number;
143
+ /** The first posts it writes: a Free blog's one (`AI_SITE_FREE_POSTS`), else none. */
144
+ posts?: number;
137
145
  }
146
+ /** How many first posts a Free blog start writes, on its provider's fast tier. */
147
+ export declare const AI_SITE_FREE_POSTS = 1;
148
+ /** The first posts a Free start of this kind writes: a blog's one, and none for any other kind. */
149
+ export declare function aiFreeSitePosts(siteKind: string | null | undefined): number;
138
150
  /** A Free site start's credits at its worst for this many sections, its room for one retried page included. */
139
151
  export declare function aiFreeSiteWorstCaseCredits(creations: AiFreeSiteCreations, sections: number, credits?: Readonly<AiFreeSiteWorstCase>): number;
140
152
  /**
@@ -149,10 +161,10 @@ export declare function aiFreeSiteSectionsWithin(creations: AiFreeSiteCreations,
149
161
  /**
150
162
  * What a Free site start of this many pages can cost at its worst, in
151
163
  * credits (AGL-3660): `aiFreeSiteWorstCaseCredits` for an empty site — the
152
- * plan, its look, the layout and form a guided start builds, each page's
153
- * answer and listing, the sections its plan is held to
154
- * (`aiFreeSiteSectionsWithin` at the month's whole taste, which is what the
155
- * plan step allows) and the room for one retried page.
164
+ * plan, its look, the layout and form a guided start builds, a blog's one
165
+ * first post, each page's answer and listing, the sections its plan is held
166
+ * to (`aiFreeSiteSectionsWithin` at the month's whole taste, which is what
167
+ * the plan step allows) and the room for one retried page.
156
168
  *
157
169
  * The CEILING a start is quoted with ("up to 283"), no longer the figure it
158
170
  * is admitted on (AGL-3722): that is the measured p90
@@ -160,7 +172,9 @@ export declare function aiFreeSiteSectionsWithin(creations: AiFreeSiteCreations,
160
172
  * confirmed and pauses with Resume when its credits run out, instead of
161
173
  * being held back from starting at all.
162
174
  */
163
- export declare function aiFreeSiteCreditEstimate(pages: number, credits?: Readonly<AiFreeSiteWorstCase>): number;
175
+ export declare function aiFreeSiteCreditEstimate(pages: number, options?: {
176
+ siteKind?: string | null;
177
+ }, credits?: Readonly<AiFreeSiteWorstCase>): number;
164
178
  /**
165
179
  * What a Free workspace has left of its Free AI credits this month: the less
166
180
  * of its own band's balance and its owner's, since the owner's allowance is
@@ -196,10 +210,13 @@ export declare function aiFreeCreditsResetLabel(resetsOn: string): string;
196
210
  * look, the layout and form a guided start builds, and each page at the
197
211
  * nominal section count, at what each MEASURED in production
198
212
  * (`AI_MEASURED_UNIT_CREDITS`); the ceiling is the whole worst case the wall
199
- * is proven with (`aiFreeSiteCreditEstimate`). Two pages: about 137
200
- * (p90 213, up to 283).
213
+ * is proven with (`aiFreeSiteCreditEstimate`). Two pages: about 113
214
+ * (p90 203, up to 243). A blog's one first post has no measurement of its
215
+ * own yet, so it is counted at its worst case in all three.
201
216
  */
202
- export declare function aiFreeSiteCreditRange(pages: number): AiCreditRange;
217
+ export declare function aiFreeSiteCreditRange(pages: number, options?: {
218
+ siteKind?: string | null;
219
+ }): AiCreditRange;
203
220
  /** The smaller first build a site start offers: its home page alone. */
204
221
  export declare const AI_SITE_HOME_FIRST_LABEL = "Build the home page first";
205
222
  /**
@@ -209,7 +226,9 @@ export declare const AI_SITE_HOME_FIRST_LABEL = "Build the home page first";
209
226
  * refusal: the person may build what fits (the job pauses with Resume when
210
227
  * the credits run out), build the home page first, or upgrade.
211
228
  */
212
- export declare function aiFreeSitePrompt(credits: Pick<AiFreeCreditsLeft, 'left' | 'resetsOn'> | null | undefined, pages: number): AiCreditsPrompt | null;
229
+ export declare function aiFreeSitePrompt(credits: Pick<AiFreeCreditsLeft, 'left' | 'resetsOn'> | null | undefined, pages: number, options?: {
230
+ siteKind?: string | null;
231
+ }): AiCreditsPrompt | null;
213
232
  /**
214
233
  * Why a Free start is refused outright (AGL-3722): nothing at all is left,
215
234
  * so there is nothing for "build what fits" to build.
@@ -232,7 +251,7 @@ export declare const AI_SITE_MAX_SECTIONS = 8;
232
251
  */
233
252
  export declare const AI_SITE_HOME_MIN_SECTIONS = 5;
234
253
  /**
235
- * The blog a paid guided start writes its first posts into (AGL-3676), and
254
+ * The blog a guided start writes its first posts into (AGL-3676), and
236
255
  * the addresses it may answer at, in order of preference: the first one no
237
256
  * planned page takes.
238
257
  */
@@ -599,12 +618,13 @@ export declare function aiSiteCreditEstimate(pages: number, options?: {
599
618
  welcomeEmail?: boolean;
600
619
  siteKind?: string | null;
601
620
  }): number;
602
- /** How many first posts a paid blog start writes, one pass each (`AI_SITE_POSTS`, held equal by `ai-site-job.spec.ts`). */
621
+ /** How many first posts a paid blog start writes, one pass each (`AI_SITE_POSTS`, held equal by `ai-site-job.spec.ts`); a Free one writes `AI_SITE_FREE_POSTS`. */
603
622
  export declare const AI_SITE_START_POSTS = 3;
604
623
  /**
605
624
  * The first content a paid start of this kind writes (AGL-3676): a store's
606
625
  * first products, a blog's first posts, or nothing. `aiSiteContentPart`'s
607
- * answer for a paid start, by the kind's id.
626
+ * answer for a paid start, by the kind's id; a Free start writes a blog its
627
+ * one first post (`aiFreeSitePosts`) and a store nothing.
608
628
  */
609
629
  export declare function aiSiteStartContent(siteKind: string | null | undefined): 'posts' | 'products' | null;
610
630
  /**