@aglyn/plugins-ai 1.0.0-beta.226 → 1.0.0-beta.228

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 (203) hide show
  1. package/README.md +3 -2
  2. package/package.json +17 -17
  3. package/src/lib/billing/ai-month-meter.js +5 -3
  4. package/src/lib/billing/ai-month-meter.js.map +1 -1
  5. package/src/lib/billing/ai-overage-ledger.js +3 -1
  6. package/src/lib/billing/ai-overage-ledger.js.map +1 -1
  7. package/src/lib/components/ai-brief-dialog.component.d.ts +6 -2
  8. package/src/lib/components/ai-brief-dialog.component.js +36 -18
  9. package/src/lib/components/ai-brief-dialog.component.js.map +1 -1
  10. package/src/lib/components/ai-describe-automation.component.js +24 -4
  11. package/src/lib/components/ai-describe-automation.component.js.map +1 -1
  12. package/src/lib/components/ai-describe-button.component.js +6 -1
  13. package/src/lib/components/ai-describe-button.component.js.map +1 -1
  14. package/src/lib/components/ai-describe-page.component.d.ts +1 -1
  15. package/src/lib/components/ai-describe-page.component.js +1 -1
  16. package/src/lib/components/ai-describe-page.component.js.map +1 -1
  17. package/src/lib/components/ai-insight-dialog.component.js +24 -2
  18. package/src/lib/components/ai-insight-dialog.component.js.map +1 -1
  19. package/src/lib/components/ai-job-events.d.ts +20 -0
  20. package/src/lib/components/ai-job-events.js +78 -0
  21. package/src/lib/components/ai-job-events.js.map +1 -0
  22. package/src/lib/components/ai-job-follow.component.d.ts +49 -0
  23. package/src/lib/components/ai-job-follow.component.js +210 -0
  24. package/src/lib/components/ai-job-follow.component.js.map +1 -0
  25. package/src/lib/components/ai-job-links.d.ts +42 -0
  26. package/src/lib/components/ai-job-links.js +140 -0
  27. package/src/lib/components/ai-job-links.js.map +1 -0
  28. package/src/lib/components/ai-job-plan.component.d.ts +13 -6
  29. package/src/lib/components/ai-job-plan.component.js +29 -3
  30. package/src/lib/components/ai-job-plan.component.js.map +1 -1
  31. package/src/lib/components/ai-job-requests.d.ts +24 -0
  32. package/src/lib/components/ai-job-requests.js +76 -0
  33. package/src/lib/components/ai-job-requests.js.map +1 -0
  34. package/src/lib/components/ai-job-site.d.ts +22 -0
  35. package/src/lib/components/ai-job-site.js +73 -0
  36. package/src/lib/components/ai-job-site.js.map +1 -0
  37. package/src/lib/components/ai-jobs-indicator.component.d.ts +25 -0
  38. package/src/lib/components/ai-jobs-indicator.component.js +80 -0
  39. package/src/lib/components/ai-jobs-indicator.component.js.map +1 -0
  40. package/src/lib/components/ai-jobs-list-page.component.d.ts +23 -0
  41. package/src/lib/components/ai-jobs-list-page.component.js +303 -0
  42. package/src/lib/components/ai-jobs-list-page.component.js.map +1 -0
  43. package/src/lib/components/ai-jobs-page.component.d.ts +7 -0
  44. package/src/lib/components/ai-jobs-page.component.js +30 -0
  45. package/src/lib/components/ai-jobs-page.component.js.map +1 -0
  46. package/src/lib/components/ai-jobs-store.d.ts +81 -0
  47. package/src/lib/components/ai-jobs-store.js +218 -0
  48. package/src/lib/components/ai-jobs-store.js.map +1 -0
  49. package/src/lib/components/ai-page-brief-dialog.component.d.ts +4 -4
  50. package/src/lib/components/ai-page-brief-dialog.component.js.map +1 -1
  51. package/src/lib/components/ai-site-build-page.component.d.ts +51 -0
  52. package/src/lib/components/ai-site-build-page.component.js +495 -0
  53. package/src/lib/components/ai-site-build-page.component.js.map +1 -0
  54. package/src/lib/components/ai-site-start-card.component.d.ts +9 -1
  55. package/src/lib/components/ai-site-start-card.component.js +533 -132
  56. package/src/lib/components/ai-site-start-card.component.js.map +1 -1
  57. package/src/lib/components/ai-site-starter-fallback.component.d.ts +24 -0
  58. package/src/lib/components/ai-site-starter-fallback.component.js +112 -0
  59. package/src/lib/components/ai-site-starter-fallback.component.js.map +1 -0
  60. package/src/lib/components/assist-jobs-drawer.component.d.ts +18 -20
  61. package/src/lib/components/assist-jobs-drawer.component.js +152 -172
  62. package/src/lib/components/assist-jobs-drawer.component.js.map +1 -1
  63. package/src/lib/components/assist-panel.component.js +69 -8
  64. package/src/lib/components/assist-panel.component.js.map +1 -1
  65. package/src/lib/components/staff-ai-credit-return.component.d.ts +55 -0
  66. package/src/lib/components/staff-ai-credit-return.component.js +346 -0
  67. package/src/lib/components/staff-ai-credit-return.component.js.map +1 -0
  68. package/src/lib/components/staff-org-ai-card.component.js +364 -279
  69. package/src/lib/components/staff-org-ai-card.component.js.map +1 -1
  70. package/src/lib/components/staff-user-ai-usage-card.component.d.ts +4 -0
  71. package/src/lib/components/staff-user-ai-usage-card.component.js +83 -28
  72. package/src/lib/components/staff-user-ai-usage-card.component.js.map +1 -1
  73. package/src/lib/components/use-ai-job-run.js +5 -18
  74. package/src/lib/components/use-ai-job-run.js.map +1 -1
  75. package/src/lib/jobs/ai-job-auto-confirm.d.ts +52 -0
  76. package/src/lib/jobs/ai-job-auto-confirm.js +62 -0
  77. package/src/lib/jobs/ai-job-auto-confirm.js.map +1 -0
  78. package/src/lib/jobs/ai-job-draft-ids.d.ts +6 -0
  79. package/src/lib/jobs/ai-job-draft-ids.js +9 -0
  80. package/src/lib/jobs/ai-job-draft-ids.js.map +1 -1
  81. package/src/lib/jobs/ai-job-drafts.d.ts +7 -0
  82. package/src/lib/jobs/ai-job-drafts.js +16 -7
  83. package/src/lib/jobs/ai-job-drafts.js.map +1 -1
  84. package/src/lib/jobs/ai-job-layout-step.js +2 -1
  85. package/src/lib/jobs/ai-job-layout-step.js.map +1 -1
  86. package/src/lib/jobs/ai-job-page-sections.d.ts +6 -5
  87. package/src/lib/jobs/ai-job-page-sections.js +3 -3
  88. package/src/lib/jobs/ai-job-page-sections.js.map +1 -1
  89. package/src/lib/jobs/ai-job-page-step.d.ts +19 -1
  90. package/src/lib/jobs/ai-job-page-step.js +50 -8
  91. package/src/lib/jobs/ai-job-page-step.js.map +1 -1
  92. package/src/lib/jobs/ai-job-plan-step.d.ts +33 -2
  93. package/src/lib/jobs/ai-job-plan-step.js +167 -18
  94. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  95. package/src/lib/jobs/ai-job-site-step.d.ts +28 -0
  96. package/src/lib/jobs/ai-job-site-step.js +130 -21
  97. package/src/lib/jobs/ai-job-site-step.js.map +1 -1
  98. package/src/lib/jobs/ai-job-text-step.d.ts +14 -1
  99. package/src/lib/jobs/ai-job-text-step.js.map +1 -1
  100. package/src/lib/jobs/ai-jobs-notify.d.ts +37 -0
  101. package/src/lib/jobs/ai-jobs-notify.js +51 -0
  102. package/src/lib/jobs/ai-jobs-notify.js.map +1 -0
  103. package/src/lib/jobs/ai-jobs.d.ts +39 -4
  104. package/src/lib/jobs/ai-jobs.js +205 -34
  105. package/src/lib/jobs/ai-jobs.js.map +1 -1
  106. package/src/lib/jobs/ai-site-publish.d.ts +98 -0
  107. package/src/lib/jobs/ai-site-publish.js +372 -0
  108. package/src/lib/jobs/ai-site-publish.js.map +1 -0
  109. package/src/lib/jobs/fixtures/ai-page-briefs.d.ts +7 -6
  110. package/src/lib/jobs/fixtures/ai-page-briefs.js +23 -56
  111. package/src/lib/jobs/fixtures/ai-page-briefs.js.map +1 -1
  112. package/src/lib/model/ai-job-activity.d.ts +112 -0
  113. package/src/lib/model/ai-job-activity.js +144 -0
  114. package/src/lib/model/ai-job-activity.js.map +1 -0
  115. package/src/lib/model/ai-job-failure-copy.d.ts +69 -0
  116. package/src/lib/model/ai-job-failure-copy.js +78 -0
  117. package/src/lib/model/ai-job-failure-copy.js.map +1 -0
  118. package/src/lib/model/ai-job-notice.d.ts +63 -0
  119. package/src/lib/model/ai-job-notice.js +129 -0
  120. package/src/lib/model/ai-job-notice.js.map +1 -0
  121. package/src/lib/model/ai-jobs.types.d.ts +53 -0
  122. package/src/lib/model/ai-jobs.types.js +1 -0
  123. package/src/lib/model/ai-jobs.types.js.map +1 -1
  124. package/src/lib/model/ai-plan-capabilities.d.ts +32 -10
  125. package/src/lib/model/ai-plan-capabilities.js +15 -2
  126. package/src/lib/model/ai-plan-capabilities.js.map +1 -1
  127. package/src/lib/model/ai-site-build-progress.d.ts +61 -0
  128. package/src/lib/model/ai-site-build-progress.js +136 -0
  129. package/src/lib/model/ai-site-build-progress.js.map +1 -0
  130. package/src/lib/model/ai-site-inventory.d.ts +9 -0
  131. package/src/lib/model/ai-site-inventory.js +7 -2
  132. package/src/lib/model/ai-site-inventory.js.map +1 -1
  133. package/src/lib/model/ai-site-job.d.ts +93 -2
  134. package/src/lib/model/ai-site-job.js +88 -7
  135. package/src/lib/model/ai-site-job.js.map +1 -1
  136. package/src/lib/model/ai-site-start-seo.d.ts +19 -0
  137. package/src/lib/model/ai-site-start-seo.js +91 -3
  138. package/src/lib/model/ai-site-start-seo.js.map +1 -1
  139. package/src/lib/model/ai-site-start.d.ts +3 -1
  140. package/src/lib/model/ai-site-start.js +6 -4
  141. package/src/lib/model/ai-site-start.js.map +1 -1
  142. package/src/lib/plan-entitlements.d.ts +3 -3
  143. package/src/lib/plan-entitlements.js +3 -3
  144. package/src/lib/plan-entitlements.js.map +1 -1
  145. package/src/lib/plugin.js +38 -2
  146. package/src/lib/plugin.js.map +1 -1
  147. package/src/lib/runtime/ai-abuse-guards.d.ts +9 -6
  148. package/src/lib/runtime/ai-abuse-guards.js +10 -7
  149. package/src/lib/runtime/ai-abuse-guards.js.map +1 -1
  150. package/src/lib/runtime/ai-doctrine-validators.d.ts +16 -12
  151. package/src/lib/runtime/ai-doctrine-validators.js +50 -40
  152. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  153. package/src/lib/runtime/ai-doctrine.js +1 -1
  154. package/src/lib/runtime/ai-doctrine.js.map +1 -1
  155. package/src/lib/runtime/ai-eval-live.d.ts +1 -1
  156. package/src/lib/runtime/ai-eval-live.js +1 -1
  157. package/src/lib/runtime/ai-eval-live.js.map +1 -1
  158. package/src/lib/runtime/ai-gate.d.ts +3 -2
  159. package/src/lib/runtime/ai-gate.js +3 -2
  160. package/src/lib/runtime/ai-gate.js.map +1 -1
  161. package/src/lib/runtime/site-inventory.d.ts +16 -1
  162. package/src/lib/runtime/site-inventory.js +43 -3
  163. package/src/lib/runtime/site-inventory.js.map +1 -1
  164. package/src/lib/server/ai-admin-credits.d.ts +21 -0
  165. package/src/lib/server/ai-admin-credits.js +213 -0
  166. package/src/lib/server/ai-admin-credits.js.map +1 -0
  167. package/src/lib/server/ai-admin-org.js +12 -5
  168. package/src/lib/server/ai-admin-org.js.map +1 -1
  169. package/src/lib/server/ai-admin-signals.js +3 -1
  170. package/src/lib/server/ai-admin-signals.js.map +1 -1
  171. package/src/lib/server/ai-admin-user.js +12 -1
  172. package/src/lib/server/ai-admin-user.js.map +1 -1
  173. package/src/lib/server/ai-allotments.js +3 -2
  174. package/src/lib/server/ai-allotments.js.map +1 -1
  175. package/src/lib/server/ai-jobs-live-sites.d.ts +26 -0
  176. package/src/lib/server/ai-jobs-live-sites.js +58 -0
  177. package/src/lib/server/ai-jobs-live-sites.js.map +1 -0
  178. package/src/lib/server/ai-jobs-route.js +42 -5
  179. package/src/lib/server/ai-jobs-route.js.map +1 -1
  180. package/src/lib/server/assist-chat.js +3 -2
  181. package/src/lib/server/assist-chat.js.map +1 -1
  182. package/src/lib/server/billing-credits.js +4 -3
  183. package/src/lib/server/billing-credits.js.map +1 -1
  184. package/src/lib/server/billing-overage.js +3 -2
  185. package/src/lib/server/billing-overage.js.map +1 -1
  186. package/src/lib/server.js +11 -0
  187. package/src/lib/server.js.map +1 -1
  188. package/src/lib/usage/assist-credit-returns-write.d.ts +75 -0
  189. package/src/lib/usage/assist-credit-returns-write.js +150 -0
  190. package/src/lib/usage/assist-credit-returns-write.js.map +1 -0
  191. package/src/lib/usage/assist-credit-returns.d.ts +68 -0
  192. package/src/lib/usage/assist-credit-returns.js +79 -0
  193. package/src/lib/usage/assist-credit-returns.js.map +1 -0
  194. package/src/lib/usage/assist-free-taste.js +6 -3
  195. package/src/lib/usage/assist-free-taste.js.map +1 -1
  196. package/src/lib/usage/assist-job-refund.d.ts +100 -0
  197. package/src/lib/usage/assist-job-refund.js +109 -0
  198. package/src/lib/usage/assist-job-refund.js.map +1 -0
  199. package/src/lib/usage/assist-usage.js +5 -2
  200. package/src/lib/usage/assist-usage.js.map +1 -1
  201. package/src/lib/usage/staff-org-ai.d.ts +31 -1
  202. package/src/lib/usage/staff-org-ai.js +19 -2
  203. package/src/lib/usage/staff-org-ai.js.map +1 -1
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-site-build-progress.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 { aiJobKindNoun } from './ai-job-activity'\nimport type { AiJobSummary } from './ai-jobs.types'\n\n/**\n * What the \"Building your site\" page shows of a site job (AGL-3594): one row\n * a stage — planning the pages, then each page — with its state, the state\n * the whole job is in, and the job's one price.\n *\n * Read off the job summary every surface already follows: the plan step's\n * status, the plan's pages, and the pages the build has reported as outputs.\n * A scaffold builds its units in plan order and reports each page when it is\n * written, so the pages written so far are the first pages of the plan.\n *\n * Pure: no React, no request. The page and its spec read the same rows.\n */\n\nexport type AiSiteBuildRowState = 'done' | 'active' | 'waiting' | 'failed'\n\nexport interface AiSiteBuildRow {\n id: string\n label: string\n state: AiSiteBuildRowState\n}\n\n/** Where the whole job stands, as the page's header and actions read it. */\nexport type AiSiteBuildPhase = 'working' | 'done' | 'stopped' | 'failed' | 'canceled'\n\nexport function aiSiteBuildPhase(job: Pick<AiJobSummary, 'status'>): AiSiteBuildPhase {\n switch (job.status) {\n case 'done':\n return 'done'\n case 'failed':\n return 'failed'\n case 'canceled':\n return 'canceled'\n case 'needs_review':\n case 'needs_input':\n return 'stopped'\n default:\n return 'working'\n }\n}\n\nexport function aiSiteBuildRows(\n job: Pick<AiJobSummary, 'status' | 'steps' | 'plan' | 'outputs' | 'review'>,\n): AiSiteBuildRow[] {\n const phase = aiSiteBuildPhase(job)\n const planStep = job.steps.find((step) => step.name === 'plan')\n const planDone = planStep?.status === 'done' || Boolean(job.plan)\n const stoppedHere = (active: boolean) => (phase === 'failed' || phase === 'stopped') && active\n const planActive = !planDone\n const rows: AiSiteBuildRow[] = [\n {\n id: 'plan',\n label: 'Planning your pages',\n state: planDone ? 'done' : stoppedHere(planActive) ? 'failed' : phase === 'working' ? 'active' : 'waiting',\n },\n ]\n const pages = job.plan?.screens ?? []\n const written = job.outputs.filter((output) => output.resource === 'screen').length\n pages.forEach((page, index) => {\n const done = index < written || phase === 'done'\n const current = !done && index === written && planDone\n rows.push({\n id: `page-${index}`,\n label: `Writing page ${index + 1} of ${pages.length}: ${page.title}`,\n state: done ? 'done' : stoppedHere(current) ? 'failed' : current && phase === 'working' ? 'active' : 'waiting',\n })\n })\n return rows\n}\n\n/**\n * The job's one price (AGL-3594), never a plan's on its own: what it has used\n * so far while it works, what it used once done, and, on a failure we gave\n * back, that nothing was charged.\n */\nexport function aiSiteBuildCreditsLine(\n job: Pick<AiJobSummary, 'status' | 'creditsSpent' | 'refundedCredits'> & Partial<Pick<AiJobSummary, 'kind'>>,\n): string | null {\n const spent = Math.max(0, job.creditsSpent ?? 0)\n const refunded = Math.max(0, job.refundedCredits ?? 0)\n const net = Math.max(0, spent - refunded)\n const phase = aiSiteBuildPhase(job)\n // What the job made, by its kind; a job that names none is a site's.\n const subject = !job.kind || job.kind === 'site' ? 'This site' : aiJobKindNoun(job.kind) === 'AI job' ? 'This AI job' : `This ${aiJobKindNoun(job.kind)}`\n if (refunded > 0 && net === 0) return `Nothing was charged — we refunded ${refunded} credits.`\n if (phase === 'done') return `${subject} used ${net} credits.`\n if (phase === 'working') return `Credits used so far: ${net}`\n if (refunded > 0) return `${subject} used ${net} credits — we refunded ${refunded}.`\n return net > 0 ? `${subject} used ${net} credits.` : null\n}\n\n/** The heading and the sentence under it, for one job's page in its current phase. */\nexport interface AiJobPageCopy {\n heading: string\n lede: string\n}\n\n/**\n * What one job's page says at the top (AGL-3594, AGL-3596). A site job reads\n * as \"Building your site\"; any other kind names what it makes — \"Building\n * your page\" — or, for a kind with no noun, the job itself.\n *\n * A guided site start publishes its pages when it finishes, and says so\n * (`sitePublish`): with a page live the heading is \"Your site is live\". A\n * done site job without that record — an older job, or one that was not a\n * guided start — built drafts, and says that instead.\n */\nexport function aiJobPageCopy(\n job: Pick<AiJobSummary, 'kind' | 'status' | 'review' | 'error'> & Partial<Pick<AiJobSummary, 'sitePublish'>>,\n brand: string,\n): AiJobPageCopy {\n const phase = aiSiteBuildPhase(job)\n const noun = job.kind === 'site' ? 'site' : aiJobKindNoun(job.kind)\n const generic = noun === 'AI job'\n // `products` is the one plural noun.\n const isAre = noun.endsWith('s') ? 'are' : 'is'\n const why = job.review?.message ?? job.error ?? null\n if (phase === 'working') {\n return {\n heading: generic ? 'Your AI job is running' : `Building your ${noun}`,\n lede:\n job.kind === 'site'\n ? `${brand} AI is planning your pages and writing each one. You can leave this page; it keeps going.`\n : `${brand} AI is working on it. You can leave this page; it keeps going.`,\n }\n }\n if (phase === 'done') {\n const published = job.sitePublish?.published.length ?? 0\n if (job.kind === 'site' && published > 0) {\n return {\n heading: 'Your site is live',\n lede: job.sitePublish?.drafts.length\n ? 'Your pages are published, except the ones listed below, which stayed drafts.'\n : 'Your pages are published, and anyone can visit your site now.',\n }\n }\n if (job.kind === 'site' && job.sitePublish) {\n return {\n heading: 'Your site is ready',\n lede: 'None of your new pages could be published, so they are drafts. The reasons are below.',\n }\n }\n return {\n heading: generic ? 'Your AI job finished' : `Your ${noun} ${isAre} ready`,\n lede:\n job.kind === 'site'\n ? 'Your new pages are drafts. Publish them when you’re happy.'\n : 'Everything it built is an unpublished draft until you publish it.',\n }\n }\n if (phase === 'canceled') {\n return { heading: generic ? 'Your AI job was canceled' : `Your ${noun} was not built`, lede: 'The job was canceled.' }\n }\n return {\n heading: generic ? 'Your AI job stopped' : `Your ${noun} ${noun.endsWith('s') ? 'were' : 'was'} not built`,\n lede: why ?? (generic ? 'Something went wrong running this job.' : `Something went wrong building your ${noun}.`),\n }\n}\n"],"names":["aiJobKindNoun","aiSiteBuildPhase","job","status","aiSiteBuildRows","phase","planStep","steps","find","step","name","planDone","Boolean","plan","stoppedHere","active","planActive","rows","id","label","state","pages","screens","written","outputs","filter","output","resource","length","forEach","page","index","done","current","push","title","aiSiteBuildCreditsLine","spent","Math","max","creditsSpent","refunded","refundedCredits","net","subject","kind","aiJobPageCopy","brand","noun","generic","isAre","endsWith","why","review","message","error","heading","lede","published","sitePublish","drafts"],"mappings":"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"}
@@ -113,6 +113,15 @@ export interface AiInventoryScreen {
113
113
  layoutId: string | null;
114
114
  /** A collection's entry template rather than a page of its own. */
115
115
  template: boolean;
116
+ /**
117
+ * The platform's starter home page, untouched since the site was created
118
+ * (AGL-3594): the screen `host.defaultHomeScreenId` names, still answering
119
+ * `/`, with no version but the one it was provisioned with. A plan may put
120
+ * its own home page at its address, and publishing that page retires this
121
+ * one (AGL-3408). Absent on every other screen, and on a starter home the
122
+ * owner has edited, which a plan keeps and plans around.
123
+ */
124
+ replaceable?: true;
116
125
  }
117
126
  /**
118
127
  * The site's one theme, as a planner and the email rule read it: a short
@@ -101,7 +101,7 @@ export const AI_INVENTORY_KINDS = [
101
101
  case 'screens':
102
102
  {
103
103
  const screen = row;
104
- return `${head} \u00b7 ${screen.slug}${screen.template ? ' \u00b7 entry template' : ''}`;
104
+ return `${head} \u00b7 ${screen.slug}${screen.template ? ' \u00b7 entry template' : ''}${screen.replaceable ? ' \u00b7 starter home, a planned home page at / replaces it' : ''}`;
105
105
  }
106
106
  }
107
107
  }
@@ -119,7 +119,12 @@ export const AI_INVENTORY_KINDS = [
119
119
  * with nowhere to go is sent there, which is why the doctrine names them.
120
120
  */ export function aiHomeScreenIds(inventory) {
121
121
  var _ref;
122
- return ((_ref = inventory == null ? void 0 : inventory.screens) != null ? _ref : []).filter((screen)=>!screen.template && (HOME_SLUGS.has(screen.slug.trim().replace(/^\/+|\/+$/g, '').toLowerCase()) || /^home(?:\s?page)?$/i.test(screen.name.trim()))).map((screen)=>screen.id);
122
+ const homes = ((_ref = inventory == null ? void 0 : inventory.screens) != null ? _ref : []).filter((screen)=>!screen.template && (HOME_SLUGS.has(screen.slug.trim().replace(/^\/+|\/+$/g, '').toLowerCase()) || /^home(?:\s?page)?$/i.test(screen.name.trim())));
123
+ // A starter home a planned home page replaces (AGL-3594) is retired when
124
+ // that page is published, so a link is sent to the page that replaces it
125
+ // wherever the site already has one.
126
+ const kept = homes.filter((screen)=>!screen.replaceable);
127
+ return (kept.length ? kept : homes).map((screen)=>screen.id);
123
128
  }
124
129
  /** A site the org does not own; the inventory is never read for it. */ export class AiInventoryScopeError extends Error {
125
130
  constructor(hostId){
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-site-inventory.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 site's inventory as a model sees it (AGL-2935): what the site already\n * has, so a plan reuses before it creates (doctrine rule 7).\n *\n * COMPACT by construction (AGL-2937): ids, names, prop shapes and slugs —\n * never a node tree, never a record's content. A component is its name and\n * the props an instance fills in; a form is its field names; a dataset its\n * field names. That is everything a planner needs to decide \"reuse this\"\n * and a generator needs to place a reference, and it is a few hundred\n * tokens where the trees would be tens of thousands.\n *\n * CAPPED three times, and the three are different numbers on purpose\n * (AGL-2937). The reader holds each kind to `AI_SITE_INVENTORY_MAX_PER_KIND`;\n * the prompt block LISTS only `AI_SITE_INVENTORY_LISTED_PER_KIND` of them and\n * is held to `AI_SITE_INVENTORY_MAX_CHARS` besides; and the rest are answered\n * on request, through the lookup tool the generation loop offers beside every\n * door's own (`tools/ai-inventory-lookup-tool.ts`).\n *\n * The listing cap is what a prompt can afford: the block is volatile, so it\n * is billed at full input rate on every request and every re-ask, and a site\n * with two hundred components would spend most of a plan's prompt on rows it\n * will not use. The read cap is what a LOOKUP can answer from, which is a\n * different question — a search for \"pricing card\" is worth nothing if the\n * card was never read. A kind cut by either is named in `truncated`, so the\n * model is told more exist rather than concluding they do not.\n *\n * The shapes live here, apart from the reader (`runtime/site-inventory.ts`),\n * so the prompt block, the validators and their specs can hold an inventory\n * without loading the Admin SDK the reader needs.\n */\n\n/**\n * Rows per kind the reader holds. Above the listing cap on purpose: the rows\n * past it are what the lookup tool searches, and a row never read cannot be\n * found. One projection of a few fields each, so the read is small even at\n * this cap.\n */\nexport const AI_SITE_INVENTORY_MAX_PER_KIND = 200\n\n/** Rows per kind the prompt block lists; the rest are found by lookup. */\nexport const AI_SITE_INVENTORY_LISTED_PER_KIND = 40\n\n/** The rendered block's ceiling: 2,000 tokens at four characters a token. */\nexport const AI_SITE_INVENTORY_MAX_CHARS = 8_000\n\nexport type AiInventoryKind =\n | 'components'\n | 'layouts'\n | 'templates'\n | 'forms'\n | 'datasets'\n | 'collections'\n | 'screens'\n\nexport const AI_INVENTORY_KINDS: readonly AiInventoryKind[] = [\n 'components',\n 'layouts',\n 'templates',\n 'forms',\n 'datasets',\n 'collections',\n 'screens',\n]\n\n/**\n * A reusable component's declared props as an instance may fill them: prop\n * name → its declared type (`text`, `image`, `href`, `number`, `boolean`, …).\n */\nexport type AiComponentPropTypes = Record<string, string>\n\nexport interface AiInventoryComponent {\n id: string\n name: string\n /** The props an instance fills in: name → declared type. */\n props: AiComponentPropTypes\n /**\n * The facts in square brackets each prop's default shows until a page sets\n * its own, by prop name (AGL-3056): what a page placing the component still\n * asks the member to fill. Read for the member's note, never listed in a\n * prompt; absent when no default holds one.\n */\n bracketedDefaults?: Record<string, string[]>\n}\n\nexport interface AiInventoryLayout {\n id: string\n name: string\n /** The layout this one nests inside, when it nests. */\n parentId: string | null\n}\n\nexport interface AiInventoryTemplate {\n id: string\n name: string\n kind: string\n}\n\nexport interface AiInventoryForm {\n id: string\n name: string\n fields: string[]\n}\n\nexport interface AiInventoryDataset {\n id: string\n name: string\n fields: string[]\n /**\n * The same fields' ids, in the same order: what a record template binds as\n * `{{item.<id>}}` (AGL-3475). Absent where the reader did not see them.\n */\n fieldIds?: string[]\n}\n\nexport interface AiInventoryCollection {\n id: string\n name: string\n slug: string\n}\n\nexport interface AiInventoryScreen {\n id: string\n name: string\n slug: string\n layoutId: string | null\n /** A collection's entry template rather than a page of its own. */\n template: boolean\n}\n\n/**\n * The site's one theme, as a planner and the email rule read it: a short\n * description, the light scheme's colors by palette path, and the families\n * the theme loads.\n */\nexport interface AiInventoryTheme {\n summary: string[]\n colors: Record<string, string>\n fonts: string[]\n}\n\nexport interface AiSiteInventory {\n hostId: string\n components: AiInventoryComponent[]\n layouts: AiInventoryLayout[]\n templates: AiInventoryTemplate[]\n forms: AiInventoryForm[]\n datasets: AiInventoryDataset[]\n collections: AiInventoryCollection[]\n screens: AiInventoryScreen[]\n theme: AiInventoryTheme | null\n /** Kinds with more records than are listed. */\n truncated: AiInventoryKind[]\n}\n\n/** One inventory record, whatever kind it is. */\nexport type AiInventoryRow =\n | AiInventoryComponent\n | AiInventoryLayout\n | AiInventoryTemplate\n | AiInventoryForm\n | AiInventoryDataset\n | AiInventoryCollection\n | AiInventoryScreen\n\n/**\n * The rows of one kind. The inventory's fields are named after their kinds,\n * so this is a lookup rather than a switch, and it is the one place that\n * relies on the correspondence: the prompt block, the lookup tool and their\n * specs all read a kind through it.\n */\nexport function aiInventoryRows(\n inventory: AiSiteInventory,\n kind: AiInventoryKind,\n): readonly AiInventoryRow[] {\n return inventory[kind]\n}\n\n/** The heading one kind's lines are listed under, naming its columns in order. */\nexport const AI_INVENTORY_KIND_HEADINGS: Readonly<Record<AiInventoryKind, string>> = {\n components: 'Reusable components (id \\u00b7 name \\u00b7 props an instance fills)',\n layouts: 'Layouts (id \\u00b7 name)',\n templates: 'Templates (id \\u00b7 name \\u00b7 kind)',\n forms: 'Forms (id \\u00b7 name \\u00b7 fields)',\n datasets: 'Datasets (id \\u00b7 name \\u00b7 fields)',\n collections: 'Content collections (id \\u00b7 name \\u00b7 slug)',\n screens: 'Pages (id \\u00b7 name \\u00b7 slug)',\n}\n\n/**\n * One record as a line, in the order its heading names the columns. The one\n * renderer: the prompt block and the lookup tool's answer read alike, so a\n * row found by lookup is the same shape as a row that was listed.\n */\nexport function aiInventoryLine(kind: AiInventoryKind, row: AiInventoryRow): string {\n const head = `${row.id} \\u00b7 ${row.name}`\n switch (kind) {\n case 'components': {\n const props = Object.entries((row as AiInventoryComponent).props)\n .map(([name, type]) => `${name}:${type}`)\n .join(', ')\n return `${head} \\u00b7 ${props || 'no props'}`\n }\n case 'layouts':\n return head\n case 'templates':\n return `${head} \\u00b7 ${(row as AiInventoryTemplate).kind}`\n case 'forms':\n return `${head} \\u00b7 ${(row as AiInventoryForm).fields.join(', ')}`\n case 'datasets':\n return `${head} \\u00b7 ${(row as AiInventoryDataset).fields.join(', ')}`\n case 'collections':\n return `${head} \\u00b7 ${(row as AiInventoryCollection).slug}`\n case 'screens': {\n const screen = row as AiInventoryScreen\n return `${head} \\u00b7 ${screen.slug}${screen.template ? ' \\u00b7 entry template' : ''}`\n }\n }\n}\n\n/** Every word of a record a lookup searches: its line, which holds them all. */\nexport function aiInventoryHaystack(kind: AiInventoryKind, row: AiInventoryRow): string {\n return aiInventoryLine(kind, row).toLowerCase()\n}\n\n/** The addresses a site's home screen is stored under. */\nconst HOME_SLUGS = new Set(['', 'home', 'index'])\n\n/**\n * The site's home screens, by id (AGL-3056): a screen at the root or at\n * `home`, as a seeded site stores its first screen, or one named Home. A link\n * with nowhere to go is sent there, which is why the doctrine names them.\n */\nexport function aiHomeScreenIds(inventory: AiSiteInventory | null): string[] {\n return (inventory?.screens ?? [])\n .filter(\n (screen) =>\n !screen.template &&\n (HOME_SLUGS.has(screen.slug.trim().replace(/^\\/+|\\/+$/g, '').toLowerCase()) ||\n /^home(?:\\s?page)?$/i.test(screen.name.trim())),\n )\n .map((screen) => screen.id)\n}\n\n/** A site the org does not own; the inventory is never read for it. */\nexport class AiInventoryScopeError extends Error {\n constructor(hostId: string) {\n super(`host ${hostId} is not a site of the workspace that asked`)\n this.name = 'AiInventoryScopeError'\n }\n}\n\nexport function emptyAiSiteInventory(hostId: string): AiSiteInventory {\n return {\n hostId,\n components: [],\n layouts: [],\n templates: [],\n forms: [],\n datasets: [],\n collections: [],\n screens: [],\n theme: null,\n truncated: [],\n }\n}\n"],"names":["AI_SITE_INVENTORY_MAX_PER_KIND","AI_SITE_INVENTORY_LISTED_PER_KIND","AI_SITE_INVENTORY_MAX_CHARS","AI_INVENTORY_KINDS","aiInventoryRows","inventory","kind","AI_INVENTORY_KIND_HEADINGS","components","layouts","templates","forms","datasets","collections","screens","aiInventoryLine","row","head","id","name","props","Object","entries","map","type","join","fields","slug","screen","template","aiInventoryHaystack","toLowerCase","HOME_SLUGS","Set","aiHomeScreenIds","filter","has","trim","replace","test","AiInventoryScopeError","Error","hostId","emptyAiSiteInventory","theme","truncated"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-site-inventory.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 site's inventory as a model sees it (AGL-2935): what the site already\n * has, so a plan reuses before it creates (doctrine rule 7).\n *\n * COMPACT by construction (AGL-2937): ids, names, prop shapes and slugs —\n * never a node tree, never a record's content. A component is its name and\n * the props an instance fills in; a form is its field names; a dataset its\n * field names. That is everything a planner needs to decide \"reuse this\"\n * and a generator needs to place a reference, and it is a few hundred\n * tokens where the trees would be tens of thousands.\n *\n * CAPPED three times, and the three are different numbers on purpose\n * (AGL-2937). The reader holds each kind to `AI_SITE_INVENTORY_MAX_PER_KIND`;\n * the prompt block LISTS only `AI_SITE_INVENTORY_LISTED_PER_KIND` of them and\n * is held to `AI_SITE_INVENTORY_MAX_CHARS` besides; and the rest are answered\n * on request, through the lookup tool the generation loop offers beside every\n * door's own (`tools/ai-inventory-lookup-tool.ts`).\n *\n * The listing cap is what a prompt can afford: the block is volatile, so it\n * is billed at full input rate on every request and every re-ask, and a site\n * with two hundred components would spend most of a plan's prompt on rows it\n * will not use. The read cap is what a LOOKUP can answer from, which is a\n * different question — a search for \"pricing card\" is worth nothing if the\n * card was never read. A kind cut by either is named in `truncated`, so the\n * model is told more exist rather than concluding they do not.\n *\n * The shapes live here, apart from the reader (`runtime/site-inventory.ts`),\n * so the prompt block, the validators and their specs can hold an inventory\n * without loading the Admin SDK the reader needs.\n */\n\n/**\n * Rows per kind the reader holds. Above the listing cap on purpose: the rows\n * past it are what the lookup tool searches, and a row never read cannot be\n * found. One projection of a few fields each, so the read is small even at\n * this cap.\n */\nexport const AI_SITE_INVENTORY_MAX_PER_KIND = 200\n\n/** Rows per kind the prompt block lists; the rest are found by lookup. */\nexport const AI_SITE_INVENTORY_LISTED_PER_KIND = 40\n\n/** The rendered block's ceiling: 2,000 tokens at four characters a token. */\nexport const AI_SITE_INVENTORY_MAX_CHARS = 8_000\n\nexport type AiInventoryKind =\n | 'components'\n | 'layouts'\n | 'templates'\n | 'forms'\n | 'datasets'\n | 'collections'\n | 'screens'\n\nexport const AI_INVENTORY_KINDS: readonly AiInventoryKind[] = [\n 'components',\n 'layouts',\n 'templates',\n 'forms',\n 'datasets',\n 'collections',\n 'screens',\n]\n\n/**\n * A reusable component's declared props as an instance may fill them: prop\n * name → its declared type (`text`, `image`, `href`, `number`, `boolean`, …).\n */\nexport type AiComponentPropTypes = Record<string, string>\n\nexport interface AiInventoryComponent {\n id: string\n name: string\n /** The props an instance fills in: name → declared type. */\n props: AiComponentPropTypes\n /**\n * The facts in square brackets each prop's default shows until a page sets\n * its own, by prop name (AGL-3056): what a page placing the component still\n * asks the member to fill. Read for the member's note, never listed in a\n * prompt; absent when no default holds one.\n */\n bracketedDefaults?: Record<string, string[]>\n}\n\nexport interface AiInventoryLayout {\n id: string\n name: string\n /** The layout this one nests inside, when it nests. */\n parentId: string | null\n}\n\nexport interface AiInventoryTemplate {\n id: string\n name: string\n kind: string\n}\n\nexport interface AiInventoryForm {\n id: string\n name: string\n fields: string[]\n}\n\nexport interface AiInventoryDataset {\n id: string\n name: string\n fields: string[]\n /**\n * The same fields' ids, in the same order: what a record template binds as\n * `{{item.<id>}}` (AGL-3475). Absent where the reader did not see them.\n */\n fieldIds?: string[]\n}\n\nexport interface AiInventoryCollection {\n id: string\n name: string\n slug: string\n}\n\nexport interface AiInventoryScreen {\n id: string\n name: string\n slug: string\n layoutId: string | null\n /** A collection's entry template rather than a page of its own. */\n template: boolean\n /**\n * The platform's starter home page, untouched since the site was created\n * (AGL-3594): the screen `host.defaultHomeScreenId` names, still answering\n * `/`, with no version but the one it was provisioned with. A plan may put\n * its own home page at its address, and publishing that page retires this\n * one (AGL-3408). Absent on every other screen, and on a starter home the\n * owner has edited, which a plan keeps and plans around.\n */\n replaceable?: true\n}\n\n/**\n * The site's one theme, as a planner and the email rule read it: a short\n * description, the light scheme's colors by palette path, and the families\n * the theme loads.\n */\nexport interface AiInventoryTheme {\n summary: string[]\n colors: Record<string, string>\n fonts: string[]\n}\n\nexport interface AiSiteInventory {\n hostId: string\n components: AiInventoryComponent[]\n layouts: AiInventoryLayout[]\n templates: AiInventoryTemplate[]\n forms: AiInventoryForm[]\n datasets: AiInventoryDataset[]\n collections: AiInventoryCollection[]\n screens: AiInventoryScreen[]\n theme: AiInventoryTheme | null\n /** Kinds with more records than are listed. */\n truncated: AiInventoryKind[]\n}\n\n/** One inventory record, whatever kind it is. */\nexport type AiInventoryRow =\n | AiInventoryComponent\n | AiInventoryLayout\n | AiInventoryTemplate\n | AiInventoryForm\n | AiInventoryDataset\n | AiInventoryCollection\n | AiInventoryScreen\n\n/**\n * The rows of one kind. The inventory's fields are named after their kinds,\n * so this is a lookup rather than a switch, and it is the one place that\n * relies on the correspondence: the prompt block, the lookup tool and their\n * specs all read a kind through it.\n */\nexport function aiInventoryRows(\n inventory: AiSiteInventory,\n kind: AiInventoryKind,\n): readonly AiInventoryRow[] {\n return inventory[kind]\n}\n\n/** The heading one kind's lines are listed under, naming its columns in order. */\nexport const AI_INVENTORY_KIND_HEADINGS: Readonly<Record<AiInventoryKind, string>> = {\n components: 'Reusable components (id \\u00b7 name \\u00b7 props an instance fills)',\n layouts: 'Layouts (id \\u00b7 name)',\n templates: 'Templates (id \\u00b7 name \\u00b7 kind)',\n forms: 'Forms (id \\u00b7 name \\u00b7 fields)',\n datasets: 'Datasets (id \\u00b7 name \\u00b7 fields)',\n collections: 'Content collections (id \\u00b7 name \\u00b7 slug)',\n screens: 'Pages (id \\u00b7 name \\u00b7 slug)',\n}\n\n/**\n * One record as a line, in the order its heading names the columns. The one\n * renderer: the prompt block and the lookup tool's answer read alike, so a\n * row found by lookup is the same shape as a row that was listed.\n */\nexport function aiInventoryLine(kind: AiInventoryKind, row: AiInventoryRow): string {\n const head = `${row.id} \\u00b7 ${row.name}`\n switch (kind) {\n case 'components': {\n const props = Object.entries((row as AiInventoryComponent).props)\n .map(([name, type]) => `${name}:${type}`)\n .join(', ')\n return `${head} \\u00b7 ${props || 'no props'}`\n }\n case 'layouts':\n return head\n case 'templates':\n return `${head} \\u00b7 ${(row as AiInventoryTemplate).kind}`\n case 'forms':\n return `${head} \\u00b7 ${(row as AiInventoryForm).fields.join(', ')}`\n case 'datasets':\n return `${head} \\u00b7 ${(row as AiInventoryDataset).fields.join(', ')}`\n case 'collections':\n return `${head} \\u00b7 ${(row as AiInventoryCollection).slug}`\n case 'screens': {\n const screen = row as AiInventoryScreen\n return `${head} \\u00b7 ${screen.slug}${screen.template ? ' \\u00b7 entry template' : ''}${\n screen.replaceable ? ' \\u00b7 starter home, a planned home page at / replaces it' : ''\n }`\n }\n }\n}\n\n/** Every word of a record a lookup searches: its line, which holds them all. */\nexport function aiInventoryHaystack(kind: AiInventoryKind, row: AiInventoryRow): string {\n return aiInventoryLine(kind, row).toLowerCase()\n}\n\n/** The addresses a site's home screen is stored under. */\nconst HOME_SLUGS = new Set(['', 'home', 'index'])\n\n/**\n * The site's home screens, by id (AGL-3056): a screen at the root or at\n * `home`, as a seeded site stores its first screen, or one named Home. A link\n * with nowhere to go is sent there, which is why the doctrine names them.\n */\nexport function aiHomeScreenIds(inventory: AiSiteInventory | null): string[] {\n const homes = (inventory?.screens ?? []).filter(\n (screen) =>\n !screen.template &&\n (HOME_SLUGS.has(screen.slug.trim().replace(/^\\/+|\\/+$/g, '').toLowerCase()) ||\n /^home(?:\\s?page)?$/i.test(screen.name.trim())),\n )\n // A starter home a planned home page replaces (AGL-3594) is retired when\n // that page is published, so a link is sent to the page that replaces it\n // wherever the site already has one.\n const kept = homes.filter((screen) => !screen.replaceable)\n return (kept.length ? kept : homes).map((screen) => screen.id)\n}\n\n/** A site the org does not own; the inventory is never read for it. */\nexport class AiInventoryScopeError extends Error {\n constructor(hostId: string) {\n super(`host ${hostId} is not a site of the workspace that asked`)\n this.name = 'AiInventoryScopeError'\n }\n}\n\nexport function emptyAiSiteInventory(hostId: string): AiSiteInventory {\n return {\n hostId,\n components: [],\n layouts: [],\n templates: [],\n forms: [],\n datasets: [],\n collections: [],\n screens: [],\n theme: null,\n truncated: [],\n }\n}\n"],"names":["AI_SITE_INVENTORY_MAX_PER_KIND","AI_SITE_INVENTORY_LISTED_PER_KIND","AI_SITE_INVENTORY_MAX_CHARS","AI_INVENTORY_KINDS","aiInventoryRows","inventory","kind","AI_INVENTORY_KIND_HEADINGS","components","layouts","templates","forms","datasets","collections","screens","aiInventoryLine","row","head","id","name","props","Object","entries","map","type","join","fields","slug","screen","template","replaceable","aiInventoryHaystack","toLowerCase","HOME_SLUGS","Set","aiHomeScreenIds","homes","filter","has","trim","replace","test","kept","length","AiInventoryScopeError","Error","hostId","emptyAiSiteInventory","theme","truncated"],"mappings":"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"}
@@ -44,6 +44,84 @@ export declare const AI_SITE_PAGES: {
44
44
  readonly min: 4;
45
45
  readonly max: 8;
46
46
  };
47
+ /**
48
+ * How many pages a Free workspace's scaffold builds (AGL-3594): its home page
49
+ * and the one page the brief most needs. A product decision, not a figure the
50
+ * wall derives — the wall's proof (`jobs/ai-job-free-site.spec.ts`) shows a
51
+ * plan and a build of this many pages fit the Free taste with room for one
52
+ * retried section.
53
+ */
54
+ export declare const AI_SITE_FREE_PAGES: {
55
+ readonly min: 1;
56
+ readonly max: 2;
57
+ };
58
+ /** The pages a scaffold builds for a workspace on the Free taste or on a paid plan. */
59
+ export declare function aiSitePagesBand(freeTaste: boolean | null | undefined): {
60
+ readonly min: number;
61
+ readonly max: number;
62
+ };
63
+ /**
64
+ * The job input a guided site start sets so its plan is confirmed for it
65
+ * (AGL-3594); `jobs/ai-job-auto-confirm.ts` says where it is honored.
66
+ */
67
+ export declare const AI_JOB_AUTO_CONFIRM_INPUT = "autoConfirm";
68
+ /** What a Free site start's page selector says under it (AGL-3594). */
69
+ export declare const AI_SITE_FREE_PAGES_NOTE = "Paid plans can generate more pages.";
70
+ /**
71
+ * The plan's answer ceiling for a scaffold (AGL-3594), Free and paid. A plan
72
+ * is an outline — each page's title, address, search listing and sections by
73
+ * name — and the build steps write everything else, so a scaffold plans with
74
+ * no thinking and at a ceiling sized to the outline of its largest band with
75
+ * room to spare: about 300 tokens a page. The 13,491-token plan a Free site
76
+ * spent 227 of its 300 credits on was its thinking, at the routing table's
77
+ * 8,000 a call.
78
+ */
79
+ export declare const AI_SITE_PLAN_MAX_TOKENS: {
80
+ readonly free: 2000;
81
+ readonly paid: 4000;
82
+ };
83
+ /** What each exchange of a Free site job comes to at its worst, in credits. */
84
+ export interface AiFreeSiteWorstCase {
85
+ /** The site plan: its answer and its one re-ask, each at its ceiling. */
86
+ plan: number;
87
+ /** The one layout a Free plan includes, built first on a site with none. */
88
+ layout: number;
89
+ /** A page's first section pass, which writes the cached prefix. */
90
+ firstSection: number;
91
+ /** Each later section pass. */
92
+ laterSection: number;
93
+ /** A page's listing. */
94
+ listing: number;
95
+ }
96
+ /**
97
+ * The Free site start's wall at its worst (AGL-3594): the site plan as
98
+ * `jobs/ai-job-free-site.spec.ts` derives it from the plan request as it
99
+ * stands, on the model a Free site plans with, every answer at its ceiling;
100
+ * the build's exchanges are the Free page's (`AI_FREE_PAGE_WORST_CASE_CREDITS`),
101
+ * which the same spec holds these to. The spec fails when a figure it derives
102
+ * moves and this does not.
103
+ */
104
+ export declare const AI_FREE_SITE_WORST_CASE_CREDITS: Readonly<AiFreeSiteWorstCase>;
105
+ /**
106
+ * The most sections a Free site's plan fits in the Free taste at its worst,
107
+ * across all its pages: the plan, the layouts it builds first, each page's
108
+ * listing and first pass, and ROOM FOR ONE RETRIED SECTION, then as many later
109
+ * passes as the rest pays for; none when that is already past the wall. Every
110
+ * page's first pass is counted as a cache write, which errs dear.
111
+ */
112
+ export declare function aiFreeSiteSectionsWithin(creations: {
113
+ layouts: number;
114
+ pages: number;
115
+ }, taste: number, credits?: Readonly<AiFreeSiteWorstCase>): number;
116
+ /** Sections a Free site's page is assumed to hold before its plan names them. */
117
+ export declare const AI_FREE_SITE_NOMINAL_SECTIONS = 3;
118
+ /**
119
+ * About what a Free site start of this many pages costs at its worst, in
120
+ * credits, for the dialog that asks for one: the site plan, each page's
121
+ * listing and its sections at the nominal count, from the figures the wall is
122
+ * proven with rather than the nominal credits a paid estimate counts.
123
+ */
124
+ export declare function aiFreeSiteCreditEstimate(pages: number, credits?: Readonly<AiFreeSiteWorstCase>): number;
47
125
  /**
48
126
  * The most sections one scaffolded page may hold. The plan model allows more
49
127
  * for a page job, which builds one page; a scaffold builds eight, and each
@@ -162,6 +240,15 @@ export declare const AI_SITE_EMAIL_TYPE = "reply";
162
240
  export declare function aiSiteSubmissions(inputs: Readonly<Record<string, unknown>> | null | undefined): AiSiteSubmissions | null;
163
241
  /** A scaffold's inputs, or the sentence the door refuses them with. */
164
242
  export declare function parseAiSiteJobInputs(inputs: Readonly<Record<string, unknown>> | null | undefined): AiSiteJobInputs | string;
243
+ /**
244
+ * Why a workspace cannot ask for this many pages (AGL-3594), in a sentence a
245
+ * member reads; `null` when it can. A Free workspace builds one or two, and a
246
+ * paid one four to eight, as it always has. Refused rather than clamped: the
247
+ * guided start offers only the band, so a request outside it was not made
248
+ * there, and a job quietly building fewer pages than it was asked for is a
249
+ * worse answer than a sentence saying why.
250
+ */
251
+ export declare function aiSitePagesRefusal(pages: number, freeTaste: boolean | null | undefined): string | null;
165
252
  /**
166
253
  * What the site itself is, as a step that is NOT the scaffold reads it off a
167
254
  * job's inputs (AGL-2918).
@@ -205,12 +292,16 @@ export declare function aiSitePlanPrerequisites(plan: AiBuildPlan): Array<{
205
292
  * with a re-ask (AGL-3030), and the doors hold a confirmed plan to it again
206
293
  * through `aiSitePlanRefusal`.
207
294
  */
208
- export declare function aiSitePlanShapeRefusal(plan: AiBuildPlan): string | null;
295
+ export declare function aiSitePlanShapeRefusal(plan: AiBuildPlan, options?: {
296
+ freeTaste?: boolean;
297
+ }): string | null;
209
298
  /**
210
299
  * Why a scaffold cannot build this plan, in a sentence a member reads when
211
300
  * confirming it; `null` when it can.
212
301
  */
213
- export declare function aiSitePlanRefusal(plan: AiBuildPlan): string | null;
302
+ export declare function aiSitePlanRefusal(plan: AiBuildPlan, options?: {
303
+ freeTaste?: boolean;
304
+ }): string | null;
214
305
  /** What the estimate counts a plan in: whether a welcome email follows, and the creations the job builds. */
215
306
  export interface AiPlanPassOptions {
216
307
  welcomeEmail?: boolean;
@@ -43,6 +43,72 @@ import { AI_PAGE_CREATE_KINDS } from "./ai-page-job.js";
43
43
  min: 4,
44
44
  max: 8
45
45
  };
46
+ /**
47
+ * How many pages a Free workspace's scaffold builds (AGL-3594): its home page
48
+ * and the one page the brief most needs. A product decision, not a figure the
49
+ * wall derives — the wall's proof (`jobs/ai-job-free-site.spec.ts`) shows a
50
+ * plan and a build of this many pages fit the Free taste with room for one
51
+ * retried section.
52
+ */ export const AI_SITE_FREE_PAGES = {
53
+ min: 1,
54
+ max: 2
55
+ };
56
+ /** The pages a scaffold builds for a workspace on the Free taste or on a paid plan. */ export function aiSitePagesBand(freeTaste) {
57
+ return freeTaste ? AI_SITE_FREE_PAGES : AI_SITE_PAGES;
58
+ }
59
+ /**
60
+ * The job input a guided site start sets so its plan is confirmed for it
61
+ * (AGL-3594); `jobs/ai-job-auto-confirm.ts` says where it is honored.
62
+ */ export const AI_JOB_AUTO_CONFIRM_INPUT = 'autoConfirm';
63
+ /** What a Free site start's page selector says under it (AGL-3594). */ export const AI_SITE_FREE_PAGES_NOTE = 'Paid plans can generate more pages.';
64
+ /**
65
+ * The plan's answer ceiling for a scaffold (AGL-3594), Free and paid. A plan
66
+ * is an outline — each page's title, address, search listing and sections by
67
+ * name — and the build steps write everything else, so a scaffold plans with
68
+ * no thinking and at a ceiling sized to the outline of its largest band with
69
+ * room to spare: about 300 tokens a page. The 13,491-token plan a Free site
70
+ * spent 227 of its 300 credits on was its thinking, at the routing table's
71
+ * 8,000 a call.
72
+ */ export const AI_SITE_PLAN_MAX_TOKENS = {
73
+ free: 2000,
74
+ paid: 4000
75
+ };
76
+ /**
77
+ * The Free site start's wall at its worst (AGL-3594): the site plan as
78
+ * `jobs/ai-job-free-site.spec.ts` derives it from the plan request as it
79
+ * stands, on the model a Free site plans with, every answer at its ceiling;
80
+ * the build's exchanges are the Free page's (`AI_FREE_PAGE_WORST_CASE_CREDITS`),
81
+ * which the same spec holds these to. The spec fails when a figure it derives
82
+ * moves and this does not.
83
+ */ export const AI_FREE_SITE_WORST_CASE_CREDITS = {
84
+ plan: 35,
85
+ layout: 64,
86
+ firstSection: 44,
87
+ laterSection: 20,
88
+ listing: 3
89
+ };
90
+ /**
91
+ * The most sections a Free site's plan fits in the Free taste at its worst,
92
+ * across all its pages: the plan, the layouts it builds first, each page's
93
+ * listing and first pass, and ROOM FOR ONE RETRIED SECTION, then as many later
94
+ * passes as the rest pays for; none when that is already past the wall. Every
95
+ * page's first pass is counted as a cache write, which errs dear.
96
+ */ export function aiFreeSiteSectionsWithin(creations, taste, credits = AI_FREE_SITE_WORST_CASE_CREDITS) {
97
+ const pages = Math.max(1, Math.floor(creations.pages));
98
+ const before = credits.plan + creations.layouts * credits.layout + pages * (credits.listing + credits.firstSection) + credits.firstSection;
99
+ if (before > taste) return 0;
100
+ return pages + Math.floor((taste - before) / credits.laterSection);
101
+ }
102
+ /** Sections a Free site's page is assumed to hold before its plan names them. */ export const AI_FREE_SITE_NOMINAL_SECTIONS = 3;
103
+ /**
104
+ * About what a Free site start of this many pages costs at its worst, in
105
+ * credits, for the dialog that asks for one: the site plan, each page's
106
+ * listing and its sections at the nominal count, from the figures the wall is
107
+ * proven with rather than the nominal credits a paid estimate counts.
108
+ */ export function aiFreeSiteCreditEstimate(pages, credits = AI_FREE_SITE_WORST_CASE_CREDITS) {
109
+ const count = Math.max(1, Math.floor(pages));
110
+ return credits.plan + count * (credits.listing + credits.firstSection) + count * (AI_FREE_SITE_NOMINAL_SECTIONS - 1) * credits.laterSection;
111
+ }
46
112
  /**
47
113
  * The most sections one scaffolded page may hold. The plan model allows more
48
114
  * for a page job, which builds one page; a scaffold builds eight, and each
@@ -162,8 +228,10 @@ const ID_CHARS = /^[A-Za-z0-9_-]{1,64}$/;
162
228
  }
163
229
  const rawPages = inputs == null ? void 0 : inputs['pages'];
164
230
  const pages = typeof rawPages === 'number' ? rawPages : Number(rawPages != null ? rawPages : NaN);
165
- if (!Number.isInteger(pages) || pages < AI_SITE_PAGES.min || pages > AI_SITE_PAGES.max) {
166
- return `pages must be a whole number from ${AI_SITE_PAGES.min} to ${AI_SITE_PAGES.max}`;
231
+ // Read here across both bands; the door holds a workspace to its own
232
+ // (`aiSitePagesRefusal`), since only it knows the workspace's plan.
233
+ if (!Number.isInteger(pages) || pages < AI_SITE_FREE_PAGES.min || pages > AI_SITE_PAGES.max) {
234
+ return `pages must be a whole number from ${AI_SITE_FREE_PAGES.min} to ${AI_SITE_PAGES.max}`;
167
235
  }
168
236
  // Where submissions go is admitted only as one of the two the form step
169
237
  // can actually bind; anything else is nobody having said, and the model
@@ -185,6 +253,18 @@ const ID_CHARS = /^[A-Za-z0-9_-]{1,64}$/;
185
253
  batchId
186
254
  };
187
255
  }
256
+ /**
257
+ * Why a workspace cannot ask for this many pages (AGL-3594), in a sentence a
258
+ * member reads; `null` when it can. A Free workspace builds one or two, and a
259
+ * paid one four to eight, as it always has. Refused rather than clamped: the
260
+ * guided start offers only the band, so a request outside it was not made
261
+ * there, and a job quietly building fewer pages than it was asked for is a
262
+ * worse answer than a sentence saying why.
263
+ */ export function aiSitePagesRefusal(pages, freeTaste) {
264
+ const band = aiSitePagesBand(freeTaste);
265
+ if (pages >= band.min && pages <= band.max) return null;
266
+ return freeTaste ? `A Free workspace's AI site start builds ${AI_SITE_FREE_PAGES.min} or ${AI_SITE_FREE_PAGES.max} pages. ${AI_SITE_FREE_PAGES_NOTE}` : `A site is planned with ${band.min} to ${band.max} pages.`;
267
+ }
188
268
  /** Whether either half of {@link AiSiteWords} says anything. */ export function aiSiteWordsSaidAnything(words) {
189
269
  return Boolean(words.about || words.audience);
190
270
  }
@@ -237,10 +317,11 @@ const ID_CHARS = /^[A-Za-z0-9_-]{1,64}$/;
237
317
  * `null` when the shape is one it builds. The plan step holds a plan to it
238
318
  * with a re-ask (AGL-3030), and the doors hold a confirmed plan to it again
239
319
  * through `aiSitePlanRefusal`.
240
- */ export function aiSitePlanShapeRefusal(plan) {
320
+ */ export function aiSitePlanShapeRefusal(plan, options = {}) {
241
321
  const { screens } = plan;
242
- if (screens.length < AI_SITE_PAGES.min || screens.length > AI_SITE_PAGES.max) {
243
- return `This plan builds ${screens.length} ${screens.length === 1 ? 'page' : 'pages'}, and a site scaffold builds ${AI_SITE_PAGES.min} to ${AI_SITE_PAGES.max}. Describe the site again.`;
322
+ const band = aiSitePagesBand(options.freeTaste);
323
+ if (screens.length < band.min || screens.length > band.max) {
324
+ return `This plan builds ${screens.length} ${screens.length === 1 ? 'page' : 'pages'}, and ${options.freeTaste ? "a Free workspace's site scaffold" : 'a site scaffold'} builds ${band.min} to ${band.max}. Describe the site again.`;
244
325
  }
245
326
  const slugs = new Set();
246
327
  for (const screen of screens){
@@ -264,8 +345,8 @@ const ID_CHARS = /^[A-Za-z0-9_-]{1,64}$/;
264
345
  /**
265
346
  * Why a scaffold cannot build this plan, in a sentence a member reads when
266
347
  * confirming it; `null` when it can.
267
- */ export function aiSitePlanRefusal(plan) {
268
- const shape = aiSitePlanShapeRefusal(plan);
348
+ */ export function aiSitePlanRefusal(plan, options = {}) {
349
+ const shape = aiSitePlanShapeRefusal(plan, options);
269
350
  if (shape) return shape;
270
351
  const prerequisites = aiSitePlanPrerequisites(plan);
271
352
  if (!prerequisites.length) return null;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-site-job.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_CREATION_NOUNS,\n aiPlanUndeclaredRefs,\n type AiBuildPlan,\n type AiBuildPlanCreateKind,\n} from './ai-build-plan'\nimport { AI_JOB_CREATE_KINDS } from './ai-job-creations'\nimport { AI_PAGE_CREATE_KINDS } from './ai-page-job'\n\n/**\n * What a site scaffold is (AGL-2911): the inputs a `site` job is admitted\n * with, the plans a scaffold can build, and what one is estimated to cost\n * before it starts.\n *\n * A scaffold is the one job kind that builds a whole site rather than one\n * record: a plan of four to eight screens, the layout they render inside,\n * the contact form they place, a palette suggestion, and — where the email\n * step is loaded — a welcome email. It builds each of those through the job\n * kind that already owns it, so this module describes what a scaffold may\n * ask for and refuses the rest at confirmation, before a credit is spent,\n * naming what to make first.\n *\n * ── The estimate is the guard rail ───────────────────────────────────────\n *\n * A scaffold is tens of model calls, where every other kind is a few, so the\n * member reads what it will cost before they confirm the plan that spends\n * it. The figure is the machine's own nominal credits per step multiplied by\n * the passes the plan implies — an estimate, and said to be one wherever it\n * is shown.\n *\n * This module imports nothing at runtime, so the doors, the step, the batch\n * card and the plan proposal read one vocabulary.\n */\n\n/** How many pages a scaffold builds: enough to be a site, few enough to read. */\nexport const AI_SITE_PAGES = { min: 4, max: 8 } as const\n\n/**\n * The most sections one scaffolded page may hold. The plan model allows more\n * for a page job, which builds one page; a scaffold builds eight, and each\n * section is a pass of its own.\n */\nexport const AI_SITE_MAX_SECTIONS = 8\n\n/**\n * Sections a page is assumed to hold before a plan names them, for the\n * estimate a member reads when they ask for a scaffold. A declared\n * assumption, not a measurement: the plan's own section counts replace it\n * the moment the plan exists.\n */\nexport const AI_SITE_NOMINAL_SECTIONS = 5\n\n/**\n * The credits one pass is held against: the machine's\n * `AI_JOB_STEP_RESERVE_CREDITS`, declared here so the estimate stays a pure\n * number the console computes without the Admin SDK. `ai-site-job.spec.ts`\n * holds the two equal.\n */\nexport const AI_SITE_PASS_CREDITS = 50\n\n/** The longest a scaffold's scalar inputs may run; they are names, not copy. */\nexport const AI_SITE_INPUT_MAX_CHARS = 120\n\n/** The sites one agency batch may generate for in one request. */\nexport const AI_SITE_BATCH_MAX = 25\n\n/**\n * The sites a plan must hold before the org Sites page offers the batch at\n * all: the Agency and Advanced bands, which the plan's `hostLimit` is what\n * distinguishes.\n */\nexport const AI_SITE_BATCH_MIN_HOST_LIMIT = 25\n\nexport interface AiSiteJobInputs {\n /** What the business is, in a few words: the brief's subject. */\n businessType: string\n /**\n * Who the site is for, in a few words (AGL-2918); empty when nobody said.\n * The plan step puts it in front of the model on a line of its own, as it\n * does every other scalar here, so it narrows the pages a plan proposes\n * without a prompt of its own.\n */\n audience: string\n /**\n * The starter site whose shape the person liked (AGL-2918), by its\n * `STARTER_TEMPLATES` id; empty when they picked none.\n *\n * A hint carried as the id rather than the starter, because an id is what\n * the guided start's list, this door and the plan's line all agree on\n * without any of them loading a catalog of node maps. Unrecognized ids are\n * admitted for the same reason an unrecognized business type is: it is a\n * few words in a brief, not a lookup.\n */\n starter: string\n /** How many pages the member asked for. */\n pages: number\n /** The per-site variables an agency batch varies; empty when none was given. */\n businessName: string\n city: string\n brand: string\n /** Whether the scaffold also drafts a welcome email. */\n welcomeEmail: boolean\n /**\n * Where the contact form's submissions go (AGL-2918); `null` where nobody\n * said, which is every job created before the question existed and every\n * door that does not ask it.\n *\n * The one setting a new site owner actually needs made, and the one thing\n * about a contact form a model cannot know: whether a submission is a note\n * to read or a lead to chase is a decision about how the business runs. So\n * an answer BINDS the form step rather than hinting to it, and `null`\n * leaves the model's own proposal standing, exactly as it stood before.\n */\n submissions: AiSiteSubmissions | null\n /** The batch this job was created under (AGL-2911); `null` for a single site. */\n batchId: string | null\n}\n\n/**\n * Where a site's contact form sends what a visitor writes.\n *\n * Two, not three. The form step's own vocabulary has a third — a mailing\n * list — which it can only answer with a note, because the stored routing has\n * no place for a list; offering it as a question would be asking somebody to\n * pick an outcome the platform then explains it cannot store.\n */\nexport const AI_SITE_SUBMISSIONS = ['inbox', 'lead'] as const\n\nexport type AiSiteSubmissions = (typeof AI_SITE_SUBMISSIONS)[number]\n\n/**\n * What each answer to \"where do submissions go\" is called, and what it means.\n *\n * Here rather than beside the guided start's own questions, because it is not\n * only the guided start that asks: the agency batch asks the same thing of a\n * whole run (AGL-2918), and two surfaces wording one decision two ways is two\n * decisions. `ai-site-job.spec.ts` holds the list to {@link AI_SITE_SUBMISSIONS}.\n */\nexport const AI_SITE_SUBMISSION_CHOICES: ReadonlyArray<{\n id: AiSiteSubmissions\n label: string\n blurb: string\n}> = [\n {\n id: 'inbox',\n label: 'The Inbox',\n blurb: 'Every message is one to read and reply to.',\n },\n {\n id: 'lead',\n label: 'The Inbox, and CRM as a lead',\n blurb: 'Every message with an email address is also somebody to follow up.',\n },\n]\n\n/**\n * Which kind of email a scaffold's email unit is (AGL-2918), carried on the\n * derived job as `inputs.emailType`. The email step names the kind on the one\n * line it puts above the brief, and a scaffold was naming none.\n *\n * A key of the email step's own `AI_EMAIL_TYPES`, held here because neither\n * step may import the other: a scaffold delegates by job kind, and a\n * deployment that scaffolds is not necessarily one that has loaded the email\n * step. `ai-job-email-step.spec.ts` holds this key to that catalog.\n *\n * `reply` and not `welcome`. The five kinds a member picks are campaigns\n * somebody sends to an audience, and this is an answer to one person who\n * wrote in through the site; the nearest, `welcome`, is a label about signing\n * up, and a kind that misdescribes the email is worse than an absent one\n * because it is the first thing the model is told.\n */\nexport const AI_SITE_EMAIL_TYPE = 'reply'\n\n/** Where a job's inputs say submissions go, or `null` where they do not say. */\nexport function aiSiteSubmissions(\n inputs: Readonly<Record<string, unknown>> | null | undefined,\n): AiSiteSubmissions | null {\n const value = inputs?.['submissions']\n return (AI_SITE_SUBMISSIONS as readonly unknown[]).includes(value)\n ? (value as AiSiteSubmissions)\n : null\n}\n\nconst ID_CHARS = /^[A-Za-z0-9_-]{1,64}$/\n\n/** A scaffold's inputs, or the sentence the door refuses them with. */\nexport function parseAiSiteJobInputs(\n inputs: Readonly<Record<string, unknown>> | null | undefined,\n): AiSiteJobInputs | string {\n const text = (key: string): string | null => {\n const value = inputs?.[key]\n if (value === undefined || value === null || value === '') return ''\n if (typeof value !== 'string') return null\n const trimmed = value.trim()\n return trimmed.length > AI_SITE_INPUT_MAX_CHARS ? null : trimmed\n }\n const businessType = text('businessType')\n if (businessType === null) {\n return `businessType must be text under ${AI_SITE_INPUT_MAX_CHARS} characters`\n }\n if (!businessType) return 'Say what kind of business the site is for'\n const businessName = text('businessName')\n const city = text('city')\n const brand = text('brand')\n const audience = text('audience')\n const starter = text('starter')\n for (const [key, value] of [\n ['businessName', businessName],\n ['city', city],\n ['brand', brand],\n ['audience', audience],\n ['starter', starter],\n ] as const) {\n if (value === null)\n return `${key} must be text under ${AI_SITE_INPUT_MAX_CHARS} characters`\n }\n const rawPages = inputs?.['pages']\n const pages =\n typeof rawPages === 'number' ? rawPages : Number(rawPages ?? NaN)\n if (\n !Number.isInteger(pages) ||\n pages < AI_SITE_PAGES.min ||\n pages > AI_SITE_PAGES.max\n ) {\n return `pages must be a whole number from ${AI_SITE_PAGES.min} to ${AI_SITE_PAGES.max}`\n }\n // Where submissions go is admitted only as one of the two the form step\n // can actually bind; anything else is nobody having said, and the model\n // proposes as it always did rather than the door refusing the whole job.\n const submissions = aiSiteSubmissions(inputs)\n const rawBatch = inputs?.['batchId']\n const batchId =\n rawBatch === undefined || rawBatch === null || rawBatch === ''\n ? null\n : typeof rawBatch === 'string' && ID_CHARS.test(rawBatch)\n ? rawBatch\n : undefined\n if (batchId === undefined) return 'batchId is not a batch id'\n return {\n businessType,\n audience: audience as string,\n starter: starter as string,\n pages,\n businessName: businessName as string,\n city: city as string,\n brand: brand as string,\n welcomeEmail: inputs?.['welcomeEmail'] !== false,\n submissions,\n batchId,\n }\n}\n\n/**\n * What the site itself is, as a step that is NOT the scaffold reads it off a\n * job's inputs (AGL-2918).\n *\n * A scaffold delegates unit by unit under a job derived from its own — a page\n * job, a form job — and the derived job carries the scaffold's inputs. So the\n * two answers that describe the site rather than one record of it are\n * readable by every delegated step, and a step that wants them does not have\n * to know it was delegated to.\n *\n * Lenient where {@link parseAiSiteJobInputs} is strict, because it reads the\n * inputs of jobs that are not scaffolds: a page job started from the Screens\n * page has no `businessType` and no `pages`, and that is not an error here —\n * it is a site nobody described, and the answer is two empty strings.\n */\nexport interface AiSiteWords {\n /** What kind of site it is; empty when the job's inputs do not say. */\n about: string\n /** Who it is for; empty when the job's inputs do not say. */\n audience: string\n}\n\n/** Whether either half of {@link AiSiteWords} says anything. */\nexport function aiSiteWordsSaidAnything(words: AiSiteWords): boolean {\n return Boolean(words.about || words.audience)\n}\n\n/** What a job's inputs say the site is, read defensively and trimmed to the input ceiling. */\nexport function aiSiteWords(\n inputs: Readonly<Record<string, unknown>> | null | undefined,\n): AiSiteWords {\n const read = (key: string): string => {\n const value = inputs?.[key]\n if (typeof value !== 'string') return ''\n return value.replace(/\\s+/g, ' ').trim().slice(0, AI_SITE_INPUT_MAX_CHARS).trim()\n }\n return { about: read('businessType'), audience: read('audience') }\n}\n\n/**\n * What a scaffold builds for itself: the layout its pages render inside, the\n * form they place, and the palette suggestion a member applies in the Theme\n * section. Anything else a plan asks to create is a job of its own.\n */\nexport const AI_SITE_CREATE_KINDS: readonly AiBuildPlanCreateKind[] = [\n 'layout',\n 'form',\n 'theme-change',\n]\n\n/** The creations a scaffold cannot build, in the order the plan lists them, each named once. */\nexport function aiSitePlanPrerequisites(\n plan: AiBuildPlan,\n): Array<{ kind: AiBuildPlanCreateKind; name: string }> {\n const seen = new Set<string>()\n const prerequisites: Array<{ kind: AiBuildPlanCreateKind; name: string }> = []\n const add = (kind: AiBuildPlanCreateKind, name: string) => {\n const key = `${kind}:${name.toLowerCase()}`\n if (seen.has(key)) return\n seen.add(key)\n prerequisites.push({ kind, name })\n }\n for (const entry of plan.create) {\n if (!AI_SITE_CREATE_KINDS.includes(entry.kind)) add(entry.kind, entry.name)\n }\n // A reference the create list does not carry is refused by the plan rules\n // before a plan is kept: rule 2 for a screen's layout, rule 7 for anything\n // else (`plan-creation-undeclared`, AGL-3040). A plan confirmed before that\n // rule existed can still carry one. It is named as a component: the\n // reference carries no kind, and the refusal must say where one is made.\n for (const { name } of aiPlanUndeclaredRefs(plan)) add('component', name)\n return prerequisites\n}\n\n/**\n * Why a scaffold cannot build a plan of this SHAPE — the page band, a page's\n * sections, its addresses, its navigation — in a sentence a member reads;\n * `null` when the shape is one it builds. The plan step holds a plan to it\n * with a re-ask (AGL-3030), and the doors hold a confirmed plan to it again\n * through `aiSitePlanRefusal`.\n */\nexport function aiSitePlanShapeRefusal(plan: AiBuildPlan): string | null {\n const { screens } = plan\n if (\n screens.length < AI_SITE_PAGES.min ||\n screens.length > AI_SITE_PAGES.max\n ) {\n return `This plan builds ${screens.length} ${screens.length === 1 ? 'page' : 'pages'}, and a site scaffold builds ${AI_SITE_PAGES.min} to ${AI_SITE_PAGES.max}. Describe the site again.`\n }\n const slugs = new Set<string>()\n for (const screen of screens) {\n if (!screen.sections.length) {\n return `The page “${screen.title}” has no sections to build. Describe the site again.`\n }\n if (screen.sections.length > AI_SITE_MAX_SECTIONS) {\n return `The page “${screen.title}” has ${screen.sections.length} sections, and a scaffolded page holds ${AI_SITE_MAX_SECTIONS}. Describe the site again.`\n }\n const slug = screen.slug.trim().toLowerCase()\n if (slugs.has(slug)) {\n return `Two pages in this plan share the address ${screen.slug}. Describe the site again.`\n }\n slugs.add(slug)\n }\n if (!screens.some((screen) => screen.nav)) {\n return 'No page in this plan is in the site navigation. Describe the site again.'\n }\n return null\n}\n\n/**\n * Why a scaffold cannot build this plan, in a sentence a member reads when\n * confirming it; `null` when it can.\n */\nexport function aiSitePlanRefusal(plan: AiBuildPlan): string | null {\n const shape = aiSitePlanShapeRefusal(plan)\n if (shape) return shape\n const prerequisites = aiSitePlanPrerequisites(plan)\n if (!prerequisites.length) return null\n const parts = prerequisites.map(\n ({ kind, name }) =>\n `the ${AI_BUILD_PLAN_CREATION_NOUNS[kind].noun} “${name}” ${AI_BUILD_PLAN_CREATION_NOUNS[kind].where}`,\n )\n const listed =\n parts.length === 1\n ? parts[0]\n : `${parts.slice(0, -1).join(', ')} and ${parts[parts.length - 1]}`\n return `This site needs what the workspace does not have yet. Create ${listed}, then describe the site again.`\n}\n\n/** What the estimate counts a plan in: whether a welcome email follows, and the creations the job builds. */\nexport interface AiPlanPassOptions {\n welcomeEmail?: boolean\n /** The creation kinds the job builds itself; a scaffold's when absent. */\n creates?: readonly AiBuildPlanCreateKind[]\n}\n\n/**\n * The passes a plan implies: one per section of every screen, one more per\n * screen for its search listing and its draft, and one for each thing the\n * plan creates that the job builds. What the estimate is counted in.\n */\nexport function aiPlanPasses(plan: AiBuildPlan, options: AiPlanPassOptions = {}): number {\n const screens = plan.screens.reduce(\n (total, screen) => total + screen.sections.length + 1,\n 0,\n )\n const creates = options.creates ?? AI_SITE_CREATE_KINDS\n const creations = plan.create.filter((entry) => creates.includes(entry.kind)).length\n return screens + creations + (options.welcomeEmail ? 1 : 0)\n}\n\n/**\n * About what a plan costs to build, in credits. An ESTIMATE: the nominal\n * credits the machine holds per step, times the passes the plan implies.\n * What each pass actually costs is its model's tokens, recorded on the job\n * as it runs.\n */\nexport function aiPlanCreditEstimate(plan: AiBuildPlan, options: AiPlanPassOptions = {}): number {\n return aiPlanPasses(plan, options) * AI_SITE_PASS_CREDITS\n}\n\n/**\n * About what a job of this kind costs to build from its plan (AGL-3031): a\n * page job counts the layout, forms and components it builds before its\n * page; a template, layout, component, form or email job counts its own\n * record and every creation it builds before it (AGL-3143 §15); and a\n * scaffold counts what it builds. The figure the proposal shows beside\n * Confirm, and the hold a confirmation takes.\n */\nexport function aiJobPlanCreditEstimate(kind: string, plan: AiBuildPlan): number {\n const creates =\n kind === 'page' ? AI_PAGE_CREATE_KINDS : AI_JOB_CREATE_KINDS[kind as keyof typeof AI_JOB_CREATE_KINDS]\n return aiPlanCreditEstimate(plan, creates ? { creates } : {})\n}\n\n/**\n * About what a scaffold of this many pages costs, before a plan names its\n * sections: the same arithmetic on the nominal section count, plus the plan\n * step that proposes it. Shown where a member asks for a scaffold.\n */\nexport function aiSiteCreditEstimate(\n pages: number,\n options: { welcomeEmail?: boolean } = {},\n): number {\n const screens = Math.max(0, Math.floor(pages))\n const passes =\n 1 +\n screens * (AI_SITE_NOMINAL_SECTIONS + 1) +\n 2 +\n (options.welcomeEmail ? 1 : 0)\n return passes * AI_SITE_PASS_CREDITS\n}\n"],"names":["AI_BUILD_PLAN_CREATION_NOUNS","aiPlanUndeclaredRefs","AI_JOB_CREATE_KINDS","AI_PAGE_CREATE_KINDS","AI_SITE_PAGES","min","max","AI_SITE_MAX_SECTIONS","AI_SITE_NOMINAL_SECTIONS","AI_SITE_PASS_CREDITS","AI_SITE_INPUT_MAX_CHARS","AI_SITE_BATCH_MAX","AI_SITE_BATCH_MIN_HOST_LIMIT","AI_SITE_SUBMISSIONS","AI_SITE_SUBMISSION_CHOICES","id","label","blurb","AI_SITE_EMAIL_TYPE","aiSiteSubmissions","inputs","value","includes","ID_CHARS","parseAiSiteJobInputs","text","key","undefined","trimmed","trim","length","businessType","businessName","city","brand","audience","starter","rawPages","pages","Number","NaN","isInteger","submissions","rawBatch","batchId","test","welcomeEmail","aiSiteWordsSaidAnything","words","Boolean","about","aiSiteWords","read","replace","slice","AI_SITE_CREATE_KINDS","aiSitePlanPrerequisites","plan","seen","Set","prerequisites","add","kind","name","toLowerCase","has","push","entry","create","aiSitePlanShapeRefusal","screens","slugs","screen","sections","title","slug","some","nav","aiSitePlanRefusal","shape","parts","map","noun","where","listed","join","aiPlanPasses","options","reduce","total","creates","creations","filter","aiPlanCreditEstimate","aiJobPlanCreditEstimate","aiSiteCreditEstimate","Math","floor","passes"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-site-job.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_CREATION_NOUNS,\n aiPlanUndeclaredRefs,\n type AiBuildPlan,\n type AiBuildPlanCreateKind,\n} from './ai-build-plan'\nimport { AI_JOB_CREATE_KINDS } from './ai-job-creations'\nimport { AI_PAGE_CREATE_KINDS } from './ai-page-job'\n\n/**\n * What a site scaffold is (AGL-2911): the inputs a `site` job is admitted\n * with, the plans a scaffold can build, and what one is estimated to cost\n * before it starts.\n *\n * A scaffold is the one job kind that builds a whole site rather than one\n * record: a plan of four to eight screens, the layout they render inside,\n * the contact form they place, a palette suggestion, and — where the email\n * step is loaded — a welcome email. It builds each of those through the job\n * kind that already owns it, so this module describes what a scaffold may\n * ask for and refuses the rest at confirmation, before a credit is spent,\n * naming what to make first.\n *\n * ── The estimate is the guard rail ───────────────────────────────────────\n *\n * A scaffold is tens of model calls, where every other kind is a few, so the\n * member reads what it will cost before they confirm the plan that spends\n * it. The figure is the machine's own nominal credits per step multiplied by\n * the passes the plan implies — an estimate, and said to be one wherever it\n * is shown.\n *\n * This module imports nothing at runtime, so the doors, the step, the batch\n * card and the plan proposal read one vocabulary.\n */\n\n/** How many pages a scaffold builds: enough to be a site, few enough to read. */\nexport const AI_SITE_PAGES = { min: 4, max: 8 } as const\n\n/**\n * How many pages a Free workspace's scaffold builds (AGL-3594): its home page\n * and the one page the brief most needs. A product decision, not a figure the\n * wall derives — the wall's proof (`jobs/ai-job-free-site.spec.ts`) shows a\n * plan and a build of this many pages fit the Free taste with room for one\n * retried section.\n */\nexport const AI_SITE_FREE_PAGES = { min: 1, max: 2 } as const\n\n/** The pages a scaffold builds for a workspace on the Free taste or on a paid plan. */\nexport function aiSitePagesBand(\n freeTaste: boolean | null | undefined,\n): { readonly min: number; readonly max: number } {\n return freeTaste ? AI_SITE_FREE_PAGES : AI_SITE_PAGES\n}\n\n/**\n * The job input a guided site start sets so its plan is confirmed for it\n * (AGL-3594); `jobs/ai-job-auto-confirm.ts` says where it is honored.\n */\nexport const AI_JOB_AUTO_CONFIRM_INPUT = 'autoConfirm'\n\n/** What a Free site start's page selector says under it (AGL-3594). */\nexport const AI_SITE_FREE_PAGES_NOTE = 'Paid plans can generate more pages.'\n\n/**\n * The plan's answer ceiling for a scaffold (AGL-3594), Free and paid. A plan\n * is an outline — each page's title, address, search listing and sections by\n * name — and the build steps write everything else, so a scaffold plans with\n * no thinking and at a ceiling sized to the outline of its largest band with\n * room to spare: about 300 tokens a page. The 13,491-token plan a Free site\n * spent 227 of its 300 credits on was its thinking, at the routing table's\n * 8,000 a call.\n */\nexport const AI_SITE_PLAN_MAX_TOKENS = { free: 2_000, paid: 4_000 } as const\n\n/** What each exchange of a Free site job comes to at its worst, in credits. */\nexport interface AiFreeSiteWorstCase {\n /** The site plan: its answer and its one re-ask, each at its ceiling. */\n plan: number\n /** The one layout a Free plan includes, built first on a site with none. */\n layout: number\n /** A page's first section pass, which writes the cached prefix. */\n firstSection: number\n /** Each later section pass. */\n laterSection: number\n /** A page's listing. */\n listing: number\n}\n\n/**\n * The Free site start's wall at its worst (AGL-3594): the site plan as\n * `jobs/ai-job-free-site.spec.ts` derives it from the plan request as it\n * stands, on the model a Free site plans with, every answer at its ceiling;\n * the build's exchanges are the Free page's (`AI_FREE_PAGE_WORST_CASE_CREDITS`),\n * which the same spec holds these to. The spec fails when a figure it derives\n * moves and this does not.\n */\nexport const AI_FREE_SITE_WORST_CASE_CREDITS: Readonly<AiFreeSiteWorstCase> = {\n plan: 35,\n layout: 64,\n firstSection: 44,\n laterSection: 20,\n listing: 3,\n}\n\n/**\n * The most sections a Free site's plan fits in the Free taste at its worst,\n * across all its pages: the plan, the layouts it builds first, each page's\n * listing and first pass, and ROOM FOR ONE RETRIED SECTION, then as many later\n * passes as the rest pays for; none when that is already past the wall. Every\n * page's first pass is counted as a cache write, which errs dear.\n */\nexport function aiFreeSiteSectionsWithin(\n creations: { layouts: number; pages: number },\n taste: number,\n credits: Readonly<AiFreeSiteWorstCase> = AI_FREE_SITE_WORST_CASE_CREDITS,\n): number {\n const pages = Math.max(1, Math.floor(creations.pages))\n const before =\n credits.plan +\n creations.layouts * credits.layout +\n pages * (credits.listing + credits.firstSection) +\n credits.firstSection\n if (before > taste) return 0\n return pages + Math.floor((taste - before) / credits.laterSection)\n}\n\n/** Sections a Free site's page is assumed to hold before its plan names them. */\nexport const AI_FREE_SITE_NOMINAL_SECTIONS = 3\n\n/**\n * About what a Free site start of this many pages costs at its worst, in\n * credits, for the dialog that asks for one: the site plan, each page's\n * listing and its sections at the nominal count, from the figures the wall is\n * proven with rather than the nominal credits a paid estimate counts.\n */\nexport function aiFreeSiteCreditEstimate(\n pages: number,\n credits: Readonly<AiFreeSiteWorstCase> = AI_FREE_SITE_WORST_CASE_CREDITS,\n): number {\n const count = Math.max(1, Math.floor(pages))\n return (\n credits.plan +\n count * (credits.listing + credits.firstSection) +\n count * (AI_FREE_SITE_NOMINAL_SECTIONS - 1) * credits.laterSection\n )\n}\n\n/**\n * The most sections one scaffolded page may hold. The plan model allows more\n * for a page job, which builds one page; a scaffold builds eight, and each\n * section is a pass of its own.\n */\nexport const AI_SITE_MAX_SECTIONS = 8\n\n/**\n * Sections a page is assumed to hold before a plan names them, for the\n * estimate a member reads when they ask for a scaffold. A declared\n * assumption, not a measurement: the plan's own section counts replace it\n * the moment the plan exists.\n */\nexport const AI_SITE_NOMINAL_SECTIONS = 5\n\n/**\n * The credits one pass is held against: the machine's\n * `AI_JOB_STEP_RESERVE_CREDITS`, declared here so the estimate stays a pure\n * number the console computes without the Admin SDK. `ai-site-job.spec.ts`\n * holds the two equal.\n */\nexport const AI_SITE_PASS_CREDITS = 50\n\n/** The longest a scaffold's scalar inputs may run; they are names, not copy. */\nexport const AI_SITE_INPUT_MAX_CHARS = 120\n\n/** The sites one agency batch may generate for in one request. */\nexport const AI_SITE_BATCH_MAX = 25\n\n/**\n * The sites a plan must hold before the org Sites page offers the batch at\n * all: the Agency and Advanced bands, which the plan's `hostLimit` is what\n * distinguishes.\n */\nexport const AI_SITE_BATCH_MIN_HOST_LIMIT = 25\n\nexport interface AiSiteJobInputs {\n /** What the business is, in a few words: the brief's subject. */\n businessType: string\n /**\n * Who the site is for, in a few words (AGL-2918); empty when nobody said.\n * The plan step puts it in front of the model on a line of its own, as it\n * does every other scalar here, so it narrows the pages a plan proposes\n * without a prompt of its own.\n */\n audience: string\n /**\n * The starter site whose shape the person liked (AGL-2918), by its\n * `STARTER_TEMPLATES` id; empty when they picked none.\n *\n * A hint carried as the id rather than the starter, because an id is what\n * the guided start's list, this door and the plan's line all agree on\n * without any of them loading a catalog of node maps. Unrecognized ids are\n * admitted for the same reason an unrecognized business type is: it is a\n * few words in a brief, not a lookup.\n */\n starter: string\n /** How many pages the member asked for. */\n pages: number\n /** The per-site variables an agency batch varies; empty when none was given. */\n businessName: string\n city: string\n brand: string\n /** Whether the scaffold also drafts a welcome email. */\n welcomeEmail: boolean\n /**\n * Where the contact form's submissions go (AGL-2918); `null` where nobody\n * said, which is every job created before the question existed and every\n * door that does not ask it.\n *\n * The one setting a new site owner actually needs made, and the one thing\n * about a contact form a model cannot know: whether a submission is a note\n * to read or a lead to chase is a decision about how the business runs. So\n * an answer BINDS the form step rather than hinting to it, and `null`\n * leaves the model's own proposal standing, exactly as it stood before.\n */\n submissions: AiSiteSubmissions | null\n /** The batch this job was created under (AGL-2911); `null` for a single site. */\n batchId: string | null\n}\n\n/**\n * Where a site's contact form sends what a visitor writes.\n *\n * Two, not three. The form step's own vocabulary has a third — a mailing\n * list — which it can only answer with a note, because the stored routing has\n * no place for a list; offering it as a question would be asking somebody to\n * pick an outcome the platform then explains it cannot store.\n */\nexport const AI_SITE_SUBMISSIONS = ['inbox', 'lead'] as const\n\nexport type AiSiteSubmissions = (typeof AI_SITE_SUBMISSIONS)[number]\n\n/**\n * What each answer to \"where do submissions go\" is called, and what it means.\n *\n * Here rather than beside the guided start's own questions, because it is not\n * only the guided start that asks: the agency batch asks the same thing of a\n * whole run (AGL-2918), and two surfaces wording one decision two ways is two\n * decisions. `ai-site-job.spec.ts` holds the list to {@link AI_SITE_SUBMISSIONS}.\n */\nexport const AI_SITE_SUBMISSION_CHOICES: ReadonlyArray<{\n id: AiSiteSubmissions\n label: string\n blurb: string\n}> = [\n {\n id: 'inbox',\n label: 'The Inbox',\n blurb: 'Every message is one to read and reply to.',\n },\n {\n id: 'lead',\n label: 'The Inbox, and CRM as a lead',\n blurb: 'Every message with an email address is also somebody to follow up.',\n },\n]\n\n/**\n * Which kind of email a scaffold's email unit is (AGL-2918), carried on the\n * derived job as `inputs.emailType`. The email step names the kind on the one\n * line it puts above the brief, and a scaffold was naming none.\n *\n * A key of the email step's own `AI_EMAIL_TYPES`, held here because neither\n * step may import the other: a scaffold delegates by job kind, and a\n * deployment that scaffolds is not necessarily one that has loaded the email\n * step. `ai-job-email-step.spec.ts` holds this key to that catalog.\n *\n * `reply` and not `welcome`. The five kinds a member picks are campaigns\n * somebody sends to an audience, and this is an answer to one person who\n * wrote in through the site; the nearest, `welcome`, is a label about signing\n * up, and a kind that misdescribes the email is worse than an absent one\n * because it is the first thing the model is told.\n */\nexport const AI_SITE_EMAIL_TYPE = 'reply'\n\n/** Where a job's inputs say submissions go, or `null` where they do not say. */\nexport function aiSiteSubmissions(\n inputs: Readonly<Record<string, unknown>> | null | undefined,\n): AiSiteSubmissions | null {\n const value = inputs?.['submissions']\n return (AI_SITE_SUBMISSIONS as readonly unknown[]).includes(value)\n ? (value as AiSiteSubmissions)\n : null\n}\n\nconst ID_CHARS = /^[A-Za-z0-9_-]{1,64}$/\n\n/** A scaffold's inputs, or the sentence the door refuses them with. */\nexport function parseAiSiteJobInputs(\n inputs: Readonly<Record<string, unknown>> | null | undefined,\n): AiSiteJobInputs | string {\n const text = (key: string): string | null => {\n const value = inputs?.[key]\n if (value === undefined || value === null || value === '') return ''\n if (typeof value !== 'string') return null\n const trimmed = value.trim()\n return trimmed.length > AI_SITE_INPUT_MAX_CHARS ? null : trimmed\n }\n const businessType = text('businessType')\n if (businessType === null) {\n return `businessType must be text under ${AI_SITE_INPUT_MAX_CHARS} characters`\n }\n if (!businessType) return 'Say what kind of business the site is for'\n const businessName = text('businessName')\n const city = text('city')\n const brand = text('brand')\n const audience = text('audience')\n const starter = text('starter')\n for (const [key, value] of [\n ['businessName', businessName],\n ['city', city],\n ['brand', brand],\n ['audience', audience],\n ['starter', starter],\n ] as const) {\n if (value === null)\n return `${key} must be text under ${AI_SITE_INPUT_MAX_CHARS} characters`\n }\n const rawPages = inputs?.['pages']\n const pages =\n typeof rawPages === 'number' ? rawPages : Number(rawPages ?? NaN)\n // Read here across both bands; the door holds a workspace to its own\n // (`aiSitePagesRefusal`), since only it knows the workspace's plan.\n if (\n !Number.isInteger(pages) ||\n pages < AI_SITE_FREE_PAGES.min ||\n pages > AI_SITE_PAGES.max\n ) {\n return `pages must be a whole number from ${AI_SITE_FREE_PAGES.min} to ${AI_SITE_PAGES.max}`\n }\n // Where submissions go is admitted only as one of the two the form step\n // can actually bind; anything else is nobody having said, and the model\n // proposes as it always did rather than the door refusing the whole job.\n const submissions = aiSiteSubmissions(inputs)\n const rawBatch = inputs?.['batchId']\n const batchId =\n rawBatch === undefined || rawBatch === null || rawBatch === ''\n ? null\n : typeof rawBatch === 'string' && ID_CHARS.test(rawBatch)\n ? rawBatch\n : undefined\n if (batchId === undefined) return 'batchId is not a batch id'\n return {\n businessType,\n audience: audience as string,\n starter: starter as string,\n pages,\n businessName: businessName as string,\n city: city as string,\n brand: brand as string,\n welcomeEmail: inputs?.['welcomeEmail'] !== false,\n submissions,\n batchId,\n }\n}\n\n/**\n * Why a workspace cannot ask for this many pages (AGL-3594), in a sentence a\n * member reads; `null` when it can. A Free workspace builds one or two, and a\n * paid one four to eight, as it always has. Refused rather than clamped: the\n * guided start offers only the band, so a request outside it was not made\n * there, and a job quietly building fewer pages than it was asked for is a\n * worse answer than a sentence saying why.\n */\nexport function aiSitePagesRefusal(pages: number, freeTaste: boolean | null | undefined): string | null {\n const band = aiSitePagesBand(freeTaste)\n if (pages >= band.min && pages <= band.max) return null\n return freeTaste\n ? `A Free workspace's AI site start builds ${AI_SITE_FREE_PAGES.min} or ${AI_SITE_FREE_PAGES.max} pages. ${AI_SITE_FREE_PAGES_NOTE}`\n : `A site is planned with ${band.min} to ${band.max} pages.`\n}\n\n/**\n * What the site itself is, as a step that is NOT the scaffold reads it off a\n * job's inputs (AGL-2918).\n *\n * A scaffold delegates unit by unit under a job derived from its own — a page\n * job, a form job — and the derived job carries the scaffold's inputs. So the\n * two answers that describe the site rather than one record of it are\n * readable by every delegated step, and a step that wants them does not have\n * to know it was delegated to.\n *\n * Lenient where {@link parseAiSiteJobInputs} is strict, because it reads the\n * inputs of jobs that are not scaffolds: a page job started from the Screens\n * page has no `businessType` and no `pages`, and that is not an error here —\n * it is a site nobody described, and the answer is two empty strings.\n */\nexport interface AiSiteWords {\n /** What kind of site it is; empty when the job's inputs do not say. */\n about: string\n /** Who it is for; empty when the job's inputs do not say. */\n audience: string\n}\n\n/** Whether either half of {@link AiSiteWords} says anything. */\nexport function aiSiteWordsSaidAnything(words: AiSiteWords): boolean {\n return Boolean(words.about || words.audience)\n}\n\n/** What a job's inputs say the site is, read defensively and trimmed to the input ceiling. */\nexport function aiSiteWords(\n inputs: Readonly<Record<string, unknown>> | null | undefined,\n): AiSiteWords {\n const read = (key: string): string => {\n const value = inputs?.[key]\n if (typeof value !== 'string') return ''\n return value.replace(/\\s+/g, ' ').trim().slice(0, AI_SITE_INPUT_MAX_CHARS).trim()\n }\n return { about: read('businessType'), audience: read('audience') }\n}\n\n/**\n * What a scaffold builds for itself: the layout its pages render inside, the\n * form they place, and the palette suggestion a member applies in the Theme\n * section. Anything else a plan asks to create is a job of its own.\n */\nexport const AI_SITE_CREATE_KINDS: readonly AiBuildPlanCreateKind[] = [\n 'layout',\n 'form',\n 'theme-change',\n]\n\n/** The creations a scaffold cannot build, in the order the plan lists them, each named once. */\nexport function aiSitePlanPrerequisites(\n plan: AiBuildPlan,\n): Array<{ kind: AiBuildPlanCreateKind; name: string }> {\n const seen = new Set<string>()\n const prerequisites: Array<{ kind: AiBuildPlanCreateKind; name: string }> = []\n const add = (kind: AiBuildPlanCreateKind, name: string) => {\n const key = `${kind}:${name.toLowerCase()}`\n if (seen.has(key)) return\n seen.add(key)\n prerequisites.push({ kind, name })\n }\n for (const entry of plan.create) {\n if (!AI_SITE_CREATE_KINDS.includes(entry.kind)) add(entry.kind, entry.name)\n }\n // A reference the create list does not carry is refused by the plan rules\n // before a plan is kept: rule 2 for a screen's layout, rule 7 for anything\n // else (`plan-creation-undeclared`, AGL-3040). A plan confirmed before that\n // rule existed can still carry one. It is named as a component: the\n // reference carries no kind, and the refusal must say where one is made.\n for (const { name } of aiPlanUndeclaredRefs(plan)) add('component', name)\n return prerequisites\n}\n\n/**\n * Why a scaffold cannot build a plan of this SHAPE — the page band, a page's\n * sections, its addresses, its navigation — in a sentence a member reads;\n * `null` when the shape is one it builds. The plan step holds a plan to it\n * with a re-ask (AGL-3030), and the doors hold a confirmed plan to it again\n * through `aiSitePlanRefusal`.\n */\nexport function aiSitePlanShapeRefusal(\n plan: AiBuildPlan,\n options: { freeTaste?: boolean } = {},\n): string | null {\n const { screens } = plan\n const band = aiSitePagesBand(options.freeTaste)\n if (screens.length < band.min || screens.length > band.max) {\n return `This plan builds ${screens.length} ${screens.length === 1 ? 'page' : 'pages'}, and ${\n options.freeTaste ? \"a Free workspace's site scaffold\" : 'a site scaffold'\n } builds ${band.min} to ${band.max}. Describe the site again.`\n }\n const slugs = new Set<string>()\n for (const screen of screens) {\n if (!screen.sections.length) {\n return `The page “${screen.title}” has no sections to build. Describe the site again.`\n }\n if (screen.sections.length > AI_SITE_MAX_SECTIONS) {\n return `The page “${screen.title}” has ${screen.sections.length} sections, and a scaffolded page holds ${AI_SITE_MAX_SECTIONS}. Describe the site again.`\n }\n const slug = screen.slug.trim().toLowerCase()\n if (slugs.has(slug)) {\n return `Two pages in this plan share the address ${screen.slug}. Describe the site again.`\n }\n slugs.add(slug)\n }\n if (!screens.some((screen) => screen.nav)) {\n return 'No page in this plan is in the site navigation. Describe the site again.'\n }\n return null\n}\n\n/**\n * Why a scaffold cannot build this plan, in a sentence a member reads when\n * confirming it; `null` when it can.\n */\nexport function aiSitePlanRefusal(\n plan: AiBuildPlan,\n options: { freeTaste?: boolean } = {},\n): string | null {\n const shape = aiSitePlanShapeRefusal(plan, options)\n if (shape) return shape\n const prerequisites = aiSitePlanPrerequisites(plan)\n if (!prerequisites.length) return null\n const parts = prerequisites.map(\n ({ kind, name }) =>\n `the ${AI_BUILD_PLAN_CREATION_NOUNS[kind].noun} “${name}” ${AI_BUILD_PLAN_CREATION_NOUNS[kind].where}`,\n )\n const listed =\n parts.length === 1\n ? parts[0]\n : `${parts.slice(0, -1).join(', ')} and ${parts[parts.length - 1]}`\n return `This site needs what the workspace does not have yet. Create ${listed}, then describe the site again.`\n}\n\n/** What the estimate counts a plan in: whether a welcome email follows, and the creations the job builds. */\nexport interface AiPlanPassOptions {\n welcomeEmail?: boolean\n /** The creation kinds the job builds itself; a scaffold's when absent. */\n creates?: readonly AiBuildPlanCreateKind[]\n}\n\n/**\n * The passes a plan implies: one per section of every screen, one more per\n * screen for its search listing and its draft, and one for each thing the\n * plan creates that the job builds. What the estimate is counted in.\n */\nexport function aiPlanPasses(plan: AiBuildPlan, options: AiPlanPassOptions = {}): number {\n const screens = plan.screens.reduce(\n (total, screen) => total + screen.sections.length + 1,\n 0,\n )\n const creates = options.creates ?? AI_SITE_CREATE_KINDS\n const creations = plan.create.filter((entry) => creates.includes(entry.kind)).length\n return screens + creations + (options.welcomeEmail ? 1 : 0)\n}\n\n/**\n * About what a plan costs to build, in credits. An ESTIMATE: the nominal\n * credits the machine holds per step, times the passes the plan implies.\n * What each pass actually costs is its model's tokens, recorded on the job\n * as it runs.\n */\nexport function aiPlanCreditEstimate(plan: AiBuildPlan, options: AiPlanPassOptions = {}): number {\n return aiPlanPasses(plan, options) * AI_SITE_PASS_CREDITS\n}\n\n/**\n * About what a job of this kind costs to build from its plan (AGL-3031): a\n * page job counts the layout, forms and components it builds before its\n * page; a template, layout, component, form or email job counts its own\n * record and every creation it builds before it (AGL-3143 §15); and a\n * scaffold counts what it builds. The figure the proposal shows beside\n * Confirm, and the hold a confirmation takes.\n */\nexport function aiJobPlanCreditEstimate(kind: string, plan: AiBuildPlan): number {\n const creates =\n kind === 'page' ? AI_PAGE_CREATE_KINDS : AI_JOB_CREATE_KINDS[kind as keyof typeof AI_JOB_CREATE_KINDS]\n return aiPlanCreditEstimate(plan, creates ? { creates } : {})\n}\n\n/**\n * About what a scaffold of this many pages costs, before a plan names its\n * sections: the same arithmetic on the nominal section count, plus the plan\n * step that proposes it. Shown where a member asks for a scaffold.\n */\nexport function aiSiteCreditEstimate(\n pages: number,\n options: { welcomeEmail?: boolean } = {},\n): number {\n const screens = Math.max(0, Math.floor(pages))\n const passes =\n 1 +\n screens * (AI_SITE_NOMINAL_SECTIONS + 1) +\n 2 +\n (options.welcomeEmail ? 1 : 0)\n return passes * AI_SITE_PASS_CREDITS\n}\n"],"names":["AI_BUILD_PLAN_CREATION_NOUNS","aiPlanUndeclaredRefs","AI_JOB_CREATE_KINDS","AI_PAGE_CREATE_KINDS","AI_SITE_PAGES","min","max","AI_SITE_FREE_PAGES","aiSitePagesBand","freeTaste","AI_JOB_AUTO_CONFIRM_INPUT","AI_SITE_FREE_PAGES_NOTE","AI_SITE_PLAN_MAX_TOKENS","free","paid","AI_FREE_SITE_WORST_CASE_CREDITS","plan","layout","firstSection","laterSection","listing","aiFreeSiteSectionsWithin","creations","taste","credits","pages","Math","floor","before","layouts","AI_FREE_SITE_NOMINAL_SECTIONS","aiFreeSiteCreditEstimate","count","AI_SITE_MAX_SECTIONS","AI_SITE_NOMINAL_SECTIONS","AI_SITE_PASS_CREDITS","AI_SITE_INPUT_MAX_CHARS","AI_SITE_BATCH_MAX","AI_SITE_BATCH_MIN_HOST_LIMIT","AI_SITE_SUBMISSIONS","AI_SITE_SUBMISSION_CHOICES","id","label","blurb","AI_SITE_EMAIL_TYPE","aiSiteSubmissions","inputs","value","includes","ID_CHARS","parseAiSiteJobInputs","text","key","undefined","trimmed","trim","length","businessType","businessName","city","brand","audience","starter","rawPages","Number","NaN","isInteger","submissions","rawBatch","batchId","test","welcomeEmail","aiSitePagesRefusal","band","aiSiteWordsSaidAnything","words","Boolean","about","aiSiteWords","read","replace","slice","AI_SITE_CREATE_KINDS","aiSitePlanPrerequisites","seen","Set","prerequisites","add","kind","name","toLowerCase","has","push","entry","create","aiSitePlanShapeRefusal","options","screens","slugs","screen","sections","title","slug","some","nav","aiSitePlanRefusal","shape","parts","map","noun","where","listed","join","aiPlanPasses","reduce","total","creates","filter","aiPlanCreditEstimate","aiJobPlanCreditEstimate","aiSiteCreditEstimate","passes"],"mappings":"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"}
@@ -77,6 +77,23 @@ export interface AiSiteSeoProposal {
77
77
  }
78
78
  /** What the proposal says about itself wherever it is shown. */
79
79
  export declare const AI_SITE_SEO_NOTE = "Written from what you said the site is and who it is for, not from its pages \u2014 read it before you save it.";
80
+ /**
81
+ * A short search title (AGL-3596): the business's name, what it is and where,
82
+ * held to the title cap and never cut mid-phrase. The answer to "what kind of
83
+ * site" is often a whole sentence, and cutting that sentence at sixty
84
+ * characters published "…that takes bookings for" as a site's title.
85
+ *
86
+ * The longest of these that fits wins: the whole answer; its first clause
87
+ * (the subject before "that", "for", a comma…); that clause without its
88
+ * place. The city the person gave is added where the words do not already
89
+ * say it, and the name leads where there is one. Only when even the shortest
90
+ * does not fit is it cut, at a word, never on a dangling one.
91
+ */
92
+ export declare function aiSiteSeoTitle(input: {
93
+ name?: string;
94
+ subject: string;
95
+ city?: string;
96
+ }): string;
80
97
  /**
81
98
  * What the listing is built from: either answer may be missing, because the
82
99
  * guided start requires only the first of them.
@@ -84,6 +101,8 @@ export declare const AI_SITE_SEO_NOTE = "Written from what you said the site is
84
101
  export interface AiSiteSeoInput extends Partial<AiSiteWords> {
85
102
  /** The site's own name where the job carries one; empty for a site named on its pages. */
86
103
  siteName?: string;
104
+ /** The city the person gave, where they gave one. */
105
+ city?: string;
87
106
  }
88
107
  /**
89
108
  * The site's fallback listing from the answers, or `null` when the answers