@aglyn/plugins-ai 1.0.0-beta.249 → 1.0.0-beta.252

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 (110) hide show
  1. package/package.json +17 -17
  2. package/src/lib/components/ai-brief-dialog.component.js +15 -1
  3. package/src/lib/components/ai-brief-dialog.component.js.map +1 -1
  4. package/src/lib/components/ai-job-links.js +1 -1
  5. package/src/lib/components/ai-job-links.js.map +1 -1
  6. package/src/lib/components/ai-job-plan.component.js +20 -2
  7. package/src/lib/components/ai-job-plan.component.js.map +1 -1
  8. package/src/lib/components/ai-jobs-store.d.ts +11 -0
  9. package/src/lib/components/ai-jobs-store.js +22 -0
  10. package/src/lib/components/ai-jobs-store.js.map +1 -1
  11. package/src/lib/jobs/ai-job-build-step.js +3 -2
  12. package/src/lib/jobs/ai-job-build-step.js.map +1 -1
  13. package/src/lib/jobs/ai-job-crm-step.js +7 -4
  14. package/src/lib/jobs/ai-job-crm-step.js.map +1 -1
  15. package/src/lib/jobs/ai-job-drafts.d.ts +24 -0
  16. package/src/lib/jobs/ai-job-drafts.js +46 -0
  17. package/src/lib/jobs/ai-job-drafts.js.map +1 -1
  18. package/src/lib/jobs/ai-job-experiment-step.js +6 -1
  19. package/src/lib/jobs/ai-job-experiment-step.js.map +1 -1
  20. package/src/lib/jobs/ai-job-form-reuse.d.ts +78 -0
  21. package/src/lib/jobs/ai-job-form-reuse.js +136 -0
  22. package/src/lib/jobs/ai-job-form-reuse.js.map +1 -0
  23. package/src/lib/jobs/ai-job-form-step.d.ts +17 -0
  24. package/src/lib/jobs/ai-job-form-step.js +63 -11
  25. package/src/lib/jobs/ai-job-form-step.js.map +1 -1
  26. package/src/lib/jobs/ai-job-insight-step.js +6 -1
  27. package/src/lib/jobs/ai-job-insight-step.js.map +1 -1
  28. package/src/lib/jobs/ai-job-page-language.d.ts +12 -1
  29. package/src/lib/jobs/ai-job-page-language.js +48 -11
  30. package/src/lib/jobs/ai-job-page-language.js.map +1 -1
  31. package/src/lib/jobs/ai-job-page-step.js +8 -3
  32. package/src/lib/jobs/ai-job-page-step.js.map +1 -1
  33. package/src/lib/jobs/ai-job-plan-step.d.ts +19 -0
  34. package/src/lib/jobs/ai-job-plan-step.js +39 -1
  35. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  36. package/src/lib/jobs/ai-job-seo-step.js +5 -3
  37. package/src/lib/jobs/ai-job-seo-step.js.map +1 -1
  38. package/src/lib/jobs/ai-job-site-content.d.ts +7 -5
  39. package/src/lib/jobs/ai-job-site-content.js +7 -5
  40. package/src/lib/jobs/ai-job-site-content.js.map +1 -1
  41. package/src/lib/jobs/ai-job-site-datasets.d.ts +17 -0
  42. package/src/lib/jobs/ai-job-site-datasets.js +24 -0
  43. package/src/lib/jobs/ai-job-site-datasets.js.map +1 -1
  44. package/src/lib/jobs/ai-job-site-step.d.ts +5 -2
  45. package/src/lib/jobs/ai-job-site-step.js +20 -6
  46. package/src/lib/jobs/ai-job-site-step.js.map +1 -1
  47. package/src/lib/jobs/ai-job-text-step.js +6 -3
  48. package/src/lib/jobs/ai-job-text-step.js.map +1 -1
  49. package/src/lib/jobs/ai-layout-stock-photos.d.ts +22 -8
  50. package/src/lib/jobs/ai-layout-stock-photos.js +39 -8
  51. package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
  52. package/src/lib/jobs/ai-stock-business-nouns.d.ts +62 -0
  53. package/src/lib/jobs/ai-stock-business-nouns.js +264 -0
  54. package/src/lib/jobs/ai-stock-business-nouns.js.map +1 -0
  55. package/src/lib/layout-language/ai-layout-gaps.d.ts +15 -0
  56. package/src/lib/layout-language/ai-layout-gaps.js +57 -7
  57. package/src/lib/layout-language/ai-layout-gaps.js.map +1 -1
  58. package/src/lib/model/ai-build-plan.d.ts +7 -3
  59. package/src/lib/model/ai-build-plan.js +11 -1
  60. package/src/lib/model/ai-build-plan.js.map +1 -1
  61. package/src/lib/model/ai-credit-estimate.d.ts +32 -18
  62. package/src/lib/model/ai-credit-estimate.js +36 -18
  63. package/src/lib/model/ai-credit-estimate.js.map +1 -1
  64. package/src/lib/model/ai-job-activity.d.ts +30 -0
  65. package/src/lib/model/ai-job-activity.js +35 -0
  66. package/src/lib/model/ai-job-activity.js.map +1 -1
  67. package/src/lib/model/ai-media-credits.d.ts +5 -3
  68. package/src/lib/model/ai-media-credits.js +7 -5
  69. package/src/lib/model/ai-media-credits.js.map +1 -1
  70. package/src/lib/model/ai-site-job.js +6 -3
  71. package/src/lib/model/ai-site-job.js.map +1 -1
  72. package/src/lib/model/ai-site-store-pages.d.ts +5 -0
  73. package/src/lib/model/ai-site-store-pages.js +2 -0
  74. package/src/lib/model/ai-site-store-pages.js.map +1 -1
  75. package/src/lib/plugin-config.js +2 -2
  76. package/src/lib/plugin-config.js.map +1 -1
  77. package/src/lib/providers/anthropic-crm-wording-superseded.d.ts +29 -0
  78. package/src/lib/providers/anthropic-crm-wording-superseded.js +31 -0
  79. package/src/lib/providers/anthropic-crm-wording-superseded.js.map +1 -0
  80. package/src/lib/providers/catalog.d.ts +24 -17
  81. package/src/lib/providers/catalog.js +63 -30
  82. package/src/lib/providers/catalog.js.map +1 -1
  83. package/src/lib/providers/model-choice.d.ts +1 -1
  84. package/src/lib/providers/model-choice.js +39 -30
  85. package/src/lib/providers/model-choice.js.map +1 -1
  86. package/src/lib/providers/routing.d.ts +20 -1
  87. package/src/lib/providers/routing.js +47 -8
  88. package/src/lib/providers/routing.js.map +1 -1
  89. package/src/lib/runtime/ai-dev-env.d.ts +37 -1
  90. package/src/lib/runtime/ai-dev-env.js +59 -6
  91. package/src/lib/runtime/ai-dev-env.js.map +1 -1
  92. package/src/lib/runtime/ai-doctrine-validators.js +3 -1
  93. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  94. package/src/lib/runtime/ai-doctrine.js +8 -4
  95. package/src/lib/runtime/ai-doctrine.js.map +1 -1
  96. package/src/lib/runtime/ai-eval-live.d.ts +8 -0
  97. package/src/lib/runtime/ai-eval-live.js +34 -8
  98. package/src/lib/runtime/ai-eval-live.js.map +1 -1
  99. package/src/lib/runtime/ai-palette.generated.js +4 -0
  100. package/src/lib/runtime/ai-palette.generated.js.map +1 -1
  101. package/src/lib/runtime/ai-runtime.js +17 -2
  102. package/src/lib/runtime/ai-runtime.js.map +1 -1
  103. package/src/lib/runtime/seo-fields.js +4 -3
  104. package/src/lib/runtime/seo-fields.js.map +1 -1
  105. package/src/lib/server/ai-assist.js +3 -2
  106. package/src/lib/server/ai-assist.js.map +1 -1
  107. package/src/lib/server/ai-media-svg.js +4 -0
  108. package/src/lib/server/ai-media-svg.js.map +1 -1
  109. package/src/lib/usage/ai-allotment-alerts.js +2 -5
  110. package/src/lib/usage/ai-allotment-alerts.js.map +1 -1
@@ -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 { aiSiteKind } from './ai-site-kinds'\nimport {\n AI_BUILD_PLAN_CREATION_NOUNS,\n aiPlanFormDatasetNames,\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'\nimport { FREE_AI_TASTE_CREDITS_PER_MONTH } from '../plan-entitlements'\nimport {\n AI_CREDIT_RANGE_ZERO,\n AI_MEASURED_PAID_SITE_CREDITS,\n AI_MEASURED_PASS_CREDITS,\n AI_MEASURED_STANDALONE_CREATION_CREDITS,\n AI_MEASURED_UNIT_CREDITS,\n aiCreditRangeAdd,\n aiCreditRangeOf,\n aiCreditRangeOrdered,\n aiCreditsPromptFor,\n aiMeasuredPageCredits,\n aiMeasuredStandalonePageCredits,\n type AiCreditRange,\n type AiCreditsPrompt,\n type AiMeasuredCredits,\n} from './ai-credit-estimate'\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 page.\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 site's look (AGL-3660): its one answer at its ceiling, on the fast tier. */\n look: number\n /**\n * The layout a site with none has built first (AGL-3660): its header and\n * footer as ONE answer in the layout language, writing its cached prefix,\n * at the most a live frame answer wrote, and never under what the live\n * guided start's layout step metered.\n */\n layout: number\n /** The saved form a guided start makes (AGL-3596), at what the live guided start's form step metered. */\n form: number\n /**\n * A language page's answer before its sections (AGL-3660): its cached\n * prefix written, its turn, and the whole room its ceiling keeps for\n * thinking. A page is ONE answer, not a pass a section.\n */\n page: number\n /** Each planned section's words in that answer, at the most a section is written in. */\n section: number\n /**\n * Room for one page asked again: the answer once more, reading its cached\n * prefix, with its thinking room and a full page of sections.\n */\n retry: number\n /** A page's listing. */\n listing: number\n}\n\n/**\n * The Free site start's wall at its worst (AGL-3594, re-derived for the\n * layout language in AGL-3660): the plan and the look as\n * `jobs/ai-job-free-site.spec.ts` derives them from their requests at their\n * ceilings; the layout, form and pages from the language requests as they\n * stand, priced as each field says. 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: 36,\n look: 7,\n layout: 23,\n form: 29,\n page: 43,\n section: 4,\n retry: 64,\n listing: 3,\n}\n\n/**\n * The most sections a Free site plans across its pages (AGL-3660), whatever\n * the wall leaves: a full home of five and a page of two or three. A product\n * figure, so the spend a site EXPECTS stays near what it measured; the wall\n * proves it fits.\n */\nexport const AI_FREE_SITE_MAX_SECTIONS = 8\n\n/** What a Free site start builds before its sections: the layouts it builds first, its forms and its pages. */\nexport interface AiFreeSiteCreations {\n layouts: number\n pages: number\n /** The saved forms it makes; a guided start makes one where it places a form. */\n forms: number\n}\n\n/** A Free site start's credits at its worst for this many sections, its room for one retried page included. */\nexport function aiFreeSiteWorstCaseCredits(\n creations: AiFreeSiteCreations,\n sections: number,\n credits: Readonly<AiFreeSiteWorstCase> = AI_FREE_SITE_WORST_CASE_CREDITS,\n): number {\n const pages = Math.max(1, Math.floor(creations.pages))\n return (\n credits.plan +\n credits.look +\n creations.layouts * credits.layout +\n creations.forms * credits.form +\n pages * (credits.page + credits.listing) +\n sections * credits.section +\n credits.retry\n )\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 (AGL-3660): the plan, the look, the layout and form it\n * builds first, each page's answer and listing, ROOM FOR ONE RETRIED PAGE,\n * and as many sections as the rest pays for — at most\n * `AI_FREE_SITE_MAX_SECTIONS`, and at most what the pages hold. None when the\n * rest is already past the wall.\n */\nexport function aiFreeSiteSectionsWithin(\n creations: AiFreeSiteCreations,\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 = aiFreeSiteWorstCaseCredits(creations, 0, credits)\n if (before > taste) return 0\n const paid = credits.section > 0 ? Math.floor((taste - before) / credits.section) : Number.POSITIVE_INFINITY\n return Math.min(AI_FREE_SITE_MAX_SECTIONS, pages * AI_SITE_MAX_SECTIONS, paid)\n}\n\n/**\n * What a Free site start of this many pages can cost at its worst, in\n * credits (AGL-3660): `aiFreeSiteWorstCaseCredits` for an empty site — the\n * plan, its look, the layout and form a guided start builds, each page's\n * answer and listing, the sections its plan is held to\n * (`aiFreeSiteSectionsWithin` at the month's whole taste, which is what the\n * plan step allows) and the room for one retried page.\n *\n * The CEILING a start is quoted with (\"up to 283\"), no longer the figure it\n * is admitted on (AGL-3722): that is the measured p90\n * (`aiFreeSiteCreditRange`), and a start past what is left is asked to be\n * confirmed and pauses with Resume when its credits run out, instead of\n * being held back from starting at all.\n */\nexport function aiFreeSiteCreditEstimate(\n pages: number,\n credits: Readonly<AiFreeSiteWorstCase> = AI_FREE_SITE_WORST_CASE_CREDITS,\n): number {\n const creations: AiFreeSiteCreations = { layouts: 1, forms: 1, pages: Math.max(1, Math.floor(pages)) }\n const sections = aiFreeSiteSectionsWithin(creations, FREE_AI_TASTE_CREDITS_PER_MONTH, credits)\n return aiFreeSiteWorstCaseCredits(creations, sections, credits)\n}\n\n/**\n * What a Free workspace has left of its Free AI credits this month: the less\n * of its own band's balance and its owner's, since the owner's allowance is\n * shared by every Free workspace they hold (AGL-2925) and the reservation\n * refuses at whichever wall is reached first. `resetsOn` is the UTC day the\n * month — and with it both balances — rolls over, `YYYY-MM-DD`.\n */\nexport interface AiFreeCreditsLeft {\n left: number\n total: number\n resetsOn: string\n /**\n * Both meters `left` is the lesser of, in credits net of give-backs, for the\n * usage strip's live read (AGL-3722): the owner's Free allowance across\n * their Free workspaces (`account`, `null` with no owner) and this\n * workspace's band.\n */\n used?: {\n account: number | null\n org: number\n orgBand: number | null\n /** The strip's warn state for these figures, worked out where the strip's own rule is not shipped to the browser. */\n state: 'ok' | 'warn' | 'capped'\n }\n}\n\n/** The first UTC day of the month after `now`, `YYYY-MM-DD`: when the Free credits come back. */\nexport function aiFreeCreditsResetOn(now: Date): string {\n const next = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + 1, 1))\n return next.toISOString().slice(0, 10)\n}\n\n/** A reset day as a person reads it: \"November 1\". */\nexport function aiFreeCreditsResetLabel(resetsOn: string): string {\n const at = new Date(`${resetsOn}T00:00:00.000Z`)\n return Number.isFinite(at.getTime())\n ? at.toLocaleDateString('en-US', { month: 'long', day: 'numeric', timeZone: 'UTC' })\n : 'the first of next month'\n}\n\n/**\n * What a Free site start of this many pages is likely to cost, its p90 and\n * its ceiling (AGL-3722), before a plan names its sections: the plan, the\n * look, the layout and form a guided start builds, and each page at the\n * nominal section count, at what each MEASURED in production\n * (`AI_MEASURED_UNIT_CREDITS`); the ceiling is the whole worst case the wall\n * is proven with (`aiFreeSiteCreditEstimate`). Two pages: about 137\n * (p90 213, up to 283).\n */\nexport function aiFreeSiteCreditRange(pages: number): AiCreditRange {\n const count = Math.max(1, Math.floor(pages))\n return aiCreditRangeOrdered({\n ...aiSiteStartMeasured(count),\n ceiling: aiFreeSiteCreditEstimate(count),\n })\n}\n\n/** A site start's measured likely and p90 before its plan: plan, look, layout, form and its pages at the nominal sections. */\nfunction aiSiteStartMeasured(pages: number): Pick<AiCreditRange, 'likely' | 'p90'> {\n const { plan, theme, layout, form } = AI_MEASURED_UNIT_CREDITS\n const page = aiMeasuredPageCredits(AI_SITE_NOMINAL_SECTIONS)\n return {\n likely: plan.median + theme.median + layout.median + form.median + pages * page.median,\n p90: plan.p90 + theme.p90 + layout.p90 + form.p90 + pages * page.p90,\n }\n}\n\n/** The smaller first build a site start offers: its home page alone. */\nexport const AI_SITE_HOME_FIRST_LABEL = 'Build the home page first'\n\n/**\n * What a Free site start says before it starts when what is left does not\n * cover its p90 (AGL-3722), or `null` when it fits — or when nothing is known\n * about what is left, since the reservation still decides each pass. Not a\n * refusal: the person may build what fits (the job pauses with Resume when\n * the credits run out), build the home page first, or upgrade.\n */\nexport function aiFreeSitePrompt(\n credits: Pick<AiFreeCreditsLeft, 'left' | 'resetsOn'> | null | undefined,\n pages: number,\n): AiCreditsPrompt | null {\n if (!credits || !Number.isFinite(credits.left)) return null\n const count = Math.max(1, Math.floor(pages))\n const home = aiFreeSiteCreditRange(1)\n return aiCreditsPromptFor(\n aiFreeSiteCreditRange(count),\n credits,\n count > 1 ? { label: AI_SITE_HOME_FIRST_LABEL, ...home } : null,\n )\n}\n\n/**\n * Why a Free start is refused outright (AGL-3722): nothing at all is left,\n * so there is nothing for \"build what fits\" to build.\n */\nexport function aiFreeCreditsNoneLeftText(resetsOn: string): string {\n return (\n `You have no free AI credits left this month. They are shared by all your Free workspaces and ` +\n `renew on ${aiFreeCreditsResetLabel(resetsOn)}. Upgrade this workspace to build with AI now.`\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 * The fewest sections a site start's home page is planned with (AGL-3660).\n * A new site is a full website (Zach, 2026-10-04): its home reads as one — a\n * hero, then bands such as the offer, why us, social proof, and a closing\n * call to action. A plan is an outline and nothing asked for more than a\n * ceiling, so a live Free yoga start (2026-10-07) planned its home with two\n * sections. The plan step names this count in the plan's turn and re-asks a\n * first answer whose home is under it (`aiSiteThinHomeViolations`).\n */\nexport const AI_SITE_HOME_MIN_SECTIONS = 5\n\n/**\n * The blog a paid guided start writes its first posts into (AGL-3676), and\n * the addresses it may answer at, in order of preference: the first one no\n * planned page takes.\n */\nexport const AI_SITE_BLOG_NAME = 'Blog'\nexport const AI_SITE_BLOG_SLUGS = ['blog', 'posts', 'journal', 'articles', 'writing', 'stories'] as const\n\n/** A planned page's first path segment, lower-cased: `/journal/2026` → `journal`. */\nconst firstSegment = (slug: string) => slug.trim().replace(/^\\/+/, '').split('/')[0].toLowerCase()\n\n/** The address the blog answers at, beside these planned pages: the first of `AI_SITE_BLOG_SLUGS` none of them takes. */\nexport function aiSiteBlogSlug(screens: ReadonlyArray<{ slug: string }>): string {\n const taken = new Set(screens.map((screen) => firstSegment(screen.slug)).filter(Boolean))\n return AI_SITE_BLOG_SLUGS.find((slug) => !taken.has(slug)) ?? AI_SITE_BLOG_SLUGS[0]\n}\n\n/** The header's link to the blog (AGL-3660): a path, not a page, since the blog is the posts collection. */\nexport const AI_SITE_BLOG_NAV_ID = 'aiSiteBlog'\n\n/** The nav entry a site whose start writes posts links its blog by. */\nexport function aiSiteBlogNavPage(screens: ReadonlyArray<{ slug: string }>): {\n id: string\n label: string\n slug: string\n href: string\n} {\n const slug = aiSiteBlogSlug(screens)\n return { id: AI_SITE_BLOG_NAV_ID, label: AI_SITE_BLOG_NAME, slug: `/${slug}`, href: `/${slug}` }\n}\n\n/**\n * Whether a planned page stands in for the blog a start writes (AGL-3660):\n * its address is one of the blog's, or its name says it lists posts. The\n * home page never does.\n */\nexport function aiSiteIsBlogStandIn(screen: { slug: string; title: string }): boolean {\n if (aiSitePlanIsHome(screen)) return false\n return (AI_SITE_BLOG_SLUGS as readonly string[]).includes(firstSegment(screen.slug)) || BLOG_PAGE_WORDS.test(screen.title)\n}\n\n/**\n * A start that writes posts, with its plan's stand-in pages taken out\n * (AGL-3676): the plan step asks once for a plan without one and keeps a\n * second answer that still has one, so the scaffold merges it into the blog\n * instead — the page is not built, its links go to the blog, and the blog\n * takes the address it left free. The kept screens hold their plan index,\n * which a page unit's slot is named by.\n */\nexport function aiSitePlanWithoutBlogStandIns<T extends { slug: string; title: string }>(\n screens: readonly T[],\n): { screens: T[]; standIns: T[] } {\n return {\n screens: screens.filter((screen) => !aiSiteIsBlogStandIn(screen)),\n standIns: screens.filter((screen) => aiSiteIsBlogStandIn(screen)),\n }\n}\n\n/** The code a planned page that stands in for the written blog is re-asked under. */\nexport const AI_SITE_BLOG_PAGE_CODE = 'plan-blog-page-duplicate'\n\n/** A page's name that says it lists the posts. */\nconst BLOG_PAGE_WORDS = /\\b(blog|posts?|articles?|journal|writing|stories|news)\\b/i\n\n/**\n * A site plan's pages that stand in for the blog its start writes (AGL-3660):\n * the live Slow Roads start (2026-10-08) planned an \"Articles\" page of\n * featured cards beside the posts it wrote at /blog, and the header linked\n * Articles and never the blog. A page whose address is one of the blog's, or\n * whose name says it lists posts, duplicates it; the home page never does.\n */\nexport function aiSiteBlogStandInViolations(\n plan: Pick<AiBuildPlan, 'screens'>,\n): Array<{ rule: null; code: string; message: string; paths: string[] }> {\n const standIns = plan.screens.flatMap((screen, index) => (aiSiteIsBlogStandIn(screen) ? [{ screen, index }] : []))\n if (!standIns.length) return []\n const names = standIns.map(({ screen }) => `\"${screen.title}\" at ${screen.slug}`).join(', ')\n return [\n {\n rule: null,\n code: AI_SITE_BLOG_PAGE_CODE,\n message: `${names} ${standIns.length === 1 ? 'stands' : 'stand'} in for the blog this site already gets: its first posts are written at /blog, and the header links it. Take ${standIns.length === 1 ? 'that page' : 'those pages'} out, and plan another page the brief needs, or feature the posts in a section of the home page.`,\n paths: standIns.map(({ index }) => `screens[${index}]`),\n },\n ]\n}\n\n/** The code a site plan's work section planned without its pieces is re-asked under. */\nexport const AI_SITE_EMPTY_GALLERY_CODE = 'plan-empty-gallery'\n\n/** The pieces a section that shows the work is planned with (AGL-3660). */\nexport const AI_SITE_GALLERY_MIN_ITEMS = 3\nexport const AI_SITE_GALLERY_MAX_ITEMS = 6\n\n/**\n * A planned section that shows the work: a gallery, a portfolio, a\n * collection, selected or featured work. \"Galleries\" — an audience, as in\n * \"Why galleries choose us\" — and \"how we work\" are not.\n */\nconst GALLERY_SECTION =\n /\\b(gallery|portfolio|collections?|lookbook|showcase|works|projects|(?:selected|featured|recent|our|past) (?:work|pieces|projects))\\b/i\n\n/** A section that only opens, introduces or closes a page, whatever page it is on: \"Portfolio Hero\". */\nconst FRAMING_SECTION = /\\b(hero|intro(?:duction)?|banner|header|cta|call to action|contact|inquiry|enquiry|about)\\b/i\n\n/** Whether a planned section's name says it shows the work. */\nexport function aiSiteSectionShowsWork(name: string): boolean {\n return GALLERY_SECTION.test(name) && !FRAMING_SECTION.test(name)\n}\n\n/** The sentence a site plan's turn states about a section that shows the work. */\nexport const AI_SITE_GALLERY_SENTENCE = `A section that shows the work — a gallery, portfolio, collection or selected works — counts its pieces in its items: ${AI_SITE_GALLERY_MIN_ITEMS} to ${AI_SITE_GALLERY_MAX_ITEMS}.`\n\n/**\n * A site plan's sections that show the work with fewer than three pieces\n * (AGL-3660). The live Juniper Clay start (2026-10-08) planned its Portfolio\n * page's \"Works Gallery\" with no items, and the page's works lost their place\n * to an inquiry form: a page about the work showed none.\n */\nexport function aiSiteEmptyGalleryViolations(\n plan: Pick<AiBuildPlan, 'screens'>,\n): Array<{ rule: null; code: string; message: string; paths: string[] }> {\n const thin = plan.screens.flatMap((screen, screenIndex) =>\n screen.sections.flatMap((section, sectionIndex) =>\n aiSiteSectionShowsWork(section.name) && section.items < AI_SITE_GALLERY_MIN_ITEMS\n ? [{ name: `\"${section.name}\" on ${screen.title}`, path: `screens[${screenIndex}].sections[${sectionIndex}].items` }]\n : [],\n ),\n )\n if (!thin.length) return []\n const names = thin.map((entry) => entry.name).join(', ')\n return [\n {\n rule: null,\n code: AI_SITE_EMPTY_GALLERY_CODE,\n message: `${names} ${thin.length === 1 ? 'shows' : 'show'} the work with fewer than ${AI_SITE_GALLERY_MIN_ITEMS} pieces. Plan ${thin.length === 1 ? 'it' : 'each'} with ${AI_SITE_GALLERY_MIN_ITEMS} to ${AI_SITE_GALLERY_MAX_ITEMS} items, one for each piece it shows.`,\n paths: thin.map((entry) => entry.path),\n },\n ]\n}\n\n/** The violation a site plan whose home page is under its fewest sections is re-asked under. */\nexport const AI_SITE_THIN_HOME_CODE = 'plan-thin-home'\n\n/** What a site plan's home is composed of, in the words the plan's turn and its re-ask both use. */\nexport const AI_SITE_HOME_BANDS =\n 'a hero first, then bands such as services or the offer, about or why us, and testimonials or other social proof, and a closing call to action or contact band last'\n\n/**\n * The fewest sections this site start's home is held to (AGL-3660): five, or\n * 0 — no minimum — where the Free wall cannot pay for a full home beside the\n * other pages. `across` is the sections the Free taste fits across `pages`\n * pages, each other page keeping at least one; `null` is a paid start, which\n * no wall shares out.\n *\n * ⛔ NEVER a lowered minimum. A Free guided start creates its site empty and\n * plans its layout, and the wall then fits 4 sections across two pages. Told\n * \"a home of at least 3\" beside that, the live yoga start of 2026-10-08\n * planned 4 + 3 and then 4 + 2, was refused for the wall twice, and stopped:\n * a home asked for in bands the wall cannot pay for is a start that fails.\n */\nexport function aiSiteHomeMinSections(input: { pages: number; across: number | null }): number {\n if (input.across === null) return AI_SITE_HOME_MIN_SECTIONS\n const others = Math.max(0, Math.floor(input.pages) - 1)\n return input.across - others >= AI_SITE_HOME_MIN_SECTIONS ? AI_SITE_HOME_MIN_SECTIONS : 0\n}\n\n/**\n * What a site start's plan is told to BUILD with (AGL-3660): the sections are\n * a budget to use, never a ceiling to stay under. Told only \"at most 8\", the\n * live prod start of 2026-10-08 planned a home of two sections, and Zach: \"that\n * didn't mean do as little as possible.\" On the Free taste the sentence shares\n * the wall's sections out — a home of five or six, the other page two or\n * three, about seven or eight in all; a paid start's home is asked to be rich.\n * `min` is the home's fewest (`aiSiteHomeMinSections`), 0 where the plan\n * plans no home or the wall cannot pay for a full one.\n */\nexport function aiSiteFullPlanSentence(input: { pages: number; across: number | null; min: number }): string {\n const { across, min } = input\n if (across === null) {\n return min > 0\n ? `Plan a full website, never a minimal one: a rich home page of ${min + 1} or more sections where the brief gives them, and every page with the sections it needs.`\n : 'Plan a full website, never a minimal one: every page with the sections it needs.'\n }\n const lead = `The ${across} sections are the budget to use, not a ceiling to stay under: plan a full website, never a minimal one`\n const others = Math.max(0, Math.floor(input.pages) - 1)\n if (min <= 0 || across < min) return `${lead}.`\n const homeHi = Math.max(min, Math.min(min + 1, across - others))\n if (!others) return `${lead}: the home page at / with ${min} to ${homeHi} sections.`\n const otherHi = Math.max(1, Math.min(3, Math.floor((across - min) / others)))\n const otherLo = Math.max(1, Math.min(2, otherHi, Math.floor((across - homeHi) / others)))\n const totalLo = Math.max(min + others, across - 1)\n const range = (lo: number, hi: number) => (lo === hi ? `${lo}` : `${lo} to ${hi}`)\n const pagesWord = others === 1 ? 'the other page' : 'each other page'\n return `${lead}: the home page at / with ${range(min, homeHi)} sections, ${pagesWord} ${range(otherLo, otherHi)}, about ${range(totalLo, across)} in total.`\n}\n\n/** Whether a planned page is the site's home: the one at `/`. */\nexport function aiSitePlanIsHome(screen: { slug: string }): boolean {\n const slug = screen.slug.trim()\n return slug === '/' || slug === ''\n}\n\n/**\n * A site plan's home page under its fewest sections (AGL-3660), as the plan\n * step's re-ask names it. A plan with no page at `/` — the owner's own home\n * stays — has no home to hold. `across` is the Free wall's section count, so\n * the re-ask says where the sections come from rather than tripping the wall.\n */\nexport function aiSiteThinHomeViolations(\n plan: Pick<AiBuildPlan, 'screens'>,\n options: { min: number; across: number | null },\n): Array<{ rule: null; code: string; message: string; paths: string[] }> {\n const index = plan.screens.findIndex(aiSitePlanIsHome)\n if (index < 0) return []\n const count = plan.screens[index].sections.length\n if (count >= options.min) return []\n const within =\n options.across === null\n ? ''\n : ` Keep the whole plan within ${options.across} sections: take them from the other page where you must.`\n return [\n {\n rule: null,\n code: AI_SITE_THIN_HOME_CODE,\n message: `The home page has ${count} ${count === 1 ? 'section' : 'sections'}, and a new site's home page reads as a full website with at least ${options.min}: ${AI_SITE_HOME_BANDS}.${within}`,\n paths: [`screens[${index}].sections`],\n },\n ]\n}\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 /**\n * The kind of site the person picked (AGL-3660), by its `AI_SITE_KINDS`\n * id; empty when nobody picked one, and the business type suggests it.\n * It sets the look the site is designed in and how its pages are arranged.\n */\n siteKind: 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/**\n * What a site job's plan and every unit it builds are told about the\n * business's name (AGL-3596): that it is the name, used as written. Said on\n * the job's own turn, never in a cached system block, because it is one\n * site's. A model handed only what the business does named it itself —\n * \"Hillside Dog Grooming\" was built as \"Austin Paws Grooming\".\n */\nexport function aiSiteNameSentence(name: string): string {\n return `The business is named “${name}”: the site names it exactly that way, in the header, the footer, the copy and the search listing, and never by any other name.`\n}\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 // A kind this deployment does not know is nobody having picked one.\n const siteKind = aiSiteKind(inputs?.['siteKind'])?.id ?? ''\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 siteKind,\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, the palette suggestion a member applies in the Theme\n * section, and the datasets its structured content is kept in (AGL-3616),\n * which the data plugin writes. Anything else a plan asks to create is a job\n * of its own.\n */\nexport const AI_SITE_CREATE_KINDS: readonly AiBuildPlanCreateKind[] = [\n 'layout',\n 'form',\n 'theme-change',\n 'dataset',\n]\n\n/** The most datasets one site start creates (AGL-3616). */\nexport const AI_SITE_DATASETS_MAX = 3\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 const datasets = plan.create.filter((entry) => entry.kind === 'dataset').length\n if (datasets > AI_SITE_DATASETS_MAX) {\n return `This plan creates ${datasets} datasets, and a site scaffold creates at most ${AI_SITE_DATASETS_MAX}. Keep the others' items on their pages.`\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 /** Each page and creation written on its own (`aiStandaloneScreenCreditRange`), not as a site start’s (AGL-3722). */\n standalonePages?: boolean\n}\n\n/**\n * Whether a creation is built with no model: a dataset a form of the plan\n * writes to (AGL-3616) starts empty, designed from its planned fields, so it\n * is no pass of the estimate.\n */\nfunction aiPlanCreationIsModelFree(plan: Pick<AiBuildPlan, 'create'>, entry: AiBuildPlan['create'][number]): boolean {\n return entry.kind === 'dataset' && aiPlanFormDatasetNames(plan).has(entry.name.trim().toLowerCase())\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) && !aiPlanCreationIsModelFree(plan, entry)).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; siteKind?: string | null } = {},\n): number {\n const screens = Math.max(0, Math.floor(pages))\n const content = aiSiteStartContent(options.siteKind)\n const passes =\n 1 +\n screens * (AI_SITE_NOMINAL_SECTIONS + 1) +\n 2 +\n (options.welcomeEmail ? 1 : 0) +\n (content === 'products' ? 1 : content === 'posts' ? AI_SITE_START_POSTS : 0)\n return passes * AI_SITE_PASS_CREDITS\n}\n\n/** How many first posts a paid blog start writes, one pass each (`AI_SITE_POSTS`, held equal by `ai-site-job.spec.ts`). */\nexport const AI_SITE_START_POSTS = 3\n\n/**\n * The first content a paid start of this kind writes (AGL-3676): a store's\n * first products, a blog's first posts, or nothing. `aiSiteContentPart`'s\n * answer for a paid start, by the kind's id.\n */\nexport function aiSiteStartContent(siteKind: string | null | undefined): 'posts' | 'products' | null {\n if (siteKind === 'store') return 'products'\n if (siteKind === 'blog') return 'posts'\n return null\n}\n\n/**\n * The measured cost of one creation a plan builds (AGL-3722): a layout's or\n * a form's as measured, a theme's as the site start's look measured, and\n * any other kind at the stand-in a single-answer creation is priced at.\n * `standalone` for one written on its own — a build's, a page job's — rather\n * than inside a site start's warm run.\n */\nexport function aiCreationMeasuredCredits(kind: string, options: { standalone?: boolean } = {}): AiMeasuredCredits {\n if (kind === 'layout') {\n return options.standalone ? AI_MEASURED_STANDALONE_CREATION_CREDITS.layout : AI_MEASURED_UNIT_CREDITS.layout\n }\n if (kind === 'form') return AI_MEASURED_UNIT_CREDITS.form\n if (kind === 'theme' || kind === 'theme-change') return AI_MEASURED_UNIT_CREDITS.theme\n return AI_MEASURED_PASS_CREDITS\n}\n\n/** A planned page's range: its sections scaled around the measured page, its ceiling one pass a section and one for its listing. */\nexport function aiScreenCreditRange(sections: number): AiCreditRange {\n return aiCreditRangeOf(aiMeasuredPageCredits(sections), (Math.max(0, sections) + 1) * AI_SITE_PASS_CREDITS)\n}\n\n/**\n * A page written on its own — an Assist build's, a page job's — as a range\n * (AGL-3722): a fixed cost and one per section as such pages measured\n * (`AI_MEASURED_STANDALONE_PAGE`), never a site start's later, cheaper\n * pages; the ceiling as `aiScreenCreditRange`'s.\n */\nexport function aiStandaloneScreenCreditRange(sections: number): AiCreditRange {\n return aiCreditRangeOf(\n aiMeasuredStandalonePageCredits(sections),\n (Math.max(0, sections) + 1) * AI_SITE_PASS_CREDITS,\n )\n}\n\n/**\n * What a plan is likely to cost, its p90 and its ceiling (AGL-3722): the\n * same units `aiPlanCreditEstimate` counts, each at what it measured; the\n * ceiling IS `aiPlanCreditEstimate`.\n */\nexport function aiPlanCreditRange(plan: AiBuildPlan, options: AiPlanPassOptions = {}): AiCreditRange {\n const creates = options.creates ?? AI_SITE_CREATE_KINDS\n let range = AI_CREDIT_RANGE_ZERO\n const screenRange = options.standalonePages ? aiStandaloneScreenCreditRange : aiScreenCreditRange\n for (const screen of plan.screens) range = aiCreditRangeAdd(range, screenRange(screen.sections.length))\n for (const entry of plan.create) {\n if (!creates.includes(entry.kind) || aiPlanCreationIsModelFree(plan, entry)) continue\n range = aiCreditRangeAdd(\n range,\n aiCreditRangeOf(aiCreationMeasuredCredits(entry.kind, { standalone: options.standalonePages }), AI_SITE_PASS_CREDITS),\n )\n }\n if (options.welcomeEmail) range = aiCreditRangeAdd(range, aiCreditRangeOf(AI_MEASURED_PASS_CREDITS, AI_SITE_PASS_CREDITS))\n return aiCreditRangeOrdered(range)\n}\n\n/**\n * `aiJobPlanCreditEstimate` as a range (AGL-3722): the same kinds, each at\n * what it measured. Only a site start writes its pages in one warm run; every\n * other kind's page is written on its own.\n */\nexport function aiJobPlanCreditRange(kind: string, plan: AiBuildPlan): AiCreditRange {\n const creates =\n kind === 'page' ? AI_PAGE_CREATE_KINDS : AI_JOB_CREATE_KINDS[kind as keyof typeof AI_JOB_CREATE_KINDS]\n return aiPlanCreditRange(plan, { ...(creates ? { creates } : {}), standalonePages: kind !== 'site' })\n}\n\n/**\n * A paid site start's range before its plan (AGL-3722): the plan, its look,\n * layout and form, and each page at the nominal sections, as measured; the\n * ceiling is `aiSiteCreditEstimate`.\n */\nexport function aiSiteCreditRange(\n pages: number,\n options: { welcomeEmail?: boolean; siteKind?: string | null } = {},\n): AiCreditRange {\n const count = Math.max(0, Math.floor(pages))\n // A paid start plans and writes on the paid routing, measured apart (AGL-3739).\n const { plan, theme, layout, form, page, email, products } = AI_MEASURED_PAID_SITE_CREDITS\n const content = aiSiteStartContent(options.siteKind)\n // A blog's posts have no measurement of their own yet: the stand-in pass.\n const parts: AiMeasuredCredits[] = [\n plan,\n theme,\n layout,\n form,\n ...Array.from({ length: count }, () => page),\n ...(options.welcomeEmail ? [email] : []),\n ...(content === 'products' ? [products] : content === 'posts' ? [AI_MEASURED_PASS_CREDITS] : []),\n ]\n return aiCreditRangeOrdered({\n likely: parts.reduce((total, part) => total + part.median, 0),\n p90: parts.reduce((total, part) => total + part.p90, 0),\n ceiling: aiSiteCreditEstimate(count, options),\n })\n}\n"],"names":["aiSiteKind","AI_BUILD_PLAN_CREATION_NOUNS","aiPlanFormDatasetNames","aiPlanUndeclaredRefs","AI_JOB_CREATE_KINDS","AI_PAGE_CREATE_KINDS","FREE_AI_TASTE_CREDITS_PER_MONTH","AI_CREDIT_RANGE_ZERO","AI_MEASURED_PAID_SITE_CREDITS","AI_MEASURED_PASS_CREDITS","AI_MEASURED_STANDALONE_CREATION_CREDITS","AI_MEASURED_UNIT_CREDITS","aiCreditRangeAdd","aiCreditRangeOf","aiCreditRangeOrdered","aiCreditsPromptFor","aiMeasuredPageCredits","aiMeasuredStandalonePageCredits","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","look","layout","form","page","section","retry","listing","AI_FREE_SITE_MAX_SECTIONS","aiFreeSiteWorstCaseCredits","creations","sections","credits","pages","Math","floor","layouts","forms","aiFreeSiteSectionsWithin","taste","before","Number","POSITIVE_INFINITY","AI_SITE_MAX_SECTIONS","aiFreeSiteCreditEstimate","aiFreeCreditsResetOn","now","next","Date","UTC","getUTCFullYear","getUTCMonth","toISOString","slice","aiFreeCreditsResetLabel","resetsOn","at","isFinite","getTime","toLocaleDateString","month","day","timeZone","aiFreeSiteCreditRange","count","aiSiteStartMeasured","ceiling","theme","AI_SITE_NOMINAL_SECTIONS","likely","median","p90","AI_SITE_HOME_FIRST_LABEL","aiFreeSitePrompt","left","home","label","aiFreeCreditsNoneLeftText","AI_SITE_HOME_MIN_SECTIONS","AI_SITE_BLOG_NAME","AI_SITE_BLOG_SLUGS","firstSegment","slug","trim","replace","split","toLowerCase","aiSiteBlogSlug","screens","taken","Set","map","screen","filter","Boolean","find","has","AI_SITE_BLOG_NAV_ID","aiSiteBlogNavPage","id","href","aiSiteIsBlogStandIn","aiSitePlanIsHome","includes","BLOG_PAGE_WORDS","test","title","aiSitePlanWithoutBlogStandIns","standIns","AI_SITE_BLOG_PAGE_CODE","aiSiteBlogStandInViolations","flatMap","index","length","names","join","rule","code","message","paths","AI_SITE_EMPTY_GALLERY_CODE","AI_SITE_GALLERY_MIN_ITEMS","AI_SITE_GALLERY_MAX_ITEMS","GALLERY_SECTION","FRAMING_SECTION","aiSiteSectionShowsWork","name","AI_SITE_GALLERY_SENTENCE","aiSiteEmptyGalleryViolations","thin","screenIndex","sectionIndex","items","path","entry","AI_SITE_THIN_HOME_CODE","AI_SITE_HOME_BANDS","aiSiteHomeMinSections","input","across","others","aiSiteFullPlanSentence","lead","homeHi","otherHi","otherLo","totalLo","range","lo","hi","pagesWord","aiSiteThinHomeViolations","options","findIndex","within","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","blurb","AI_SITE_EMAIL_TYPE","aiSiteNameSentence","aiSiteSubmissions","inputs","value","ID_CHARS","parseAiSiteJobInputs","text","key","undefined","trimmed","businessType","businessName","city","brand","audience","starter","siteKind","rawPages","NaN","isInteger","submissions","rawBatch","batchId","welcomeEmail","aiSitePagesRefusal","band","aiSiteWordsSaidAnything","words","about","aiSiteWords","read","AI_SITE_CREATE_KINDS","AI_SITE_DATASETS_MAX","aiSitePlanPrerequisites","seen","prerequisites","add","kind","push","create","aiSitePlanShapeRefusal","slugs","some","nav","datasets","aiSitePlanRefusal","shape","parts","noun","where","listed","aiPlanCreationIsModelFree","aiPlanPasses","reduce","total","creates","aiPlanCreditEstimate","aiJobPlanCreditEstimate","aiSiteCreditEstimate","content","aiSiteStartContent","passes","AI_SITE_START_POSTS","aiCreationMeasuredCredits","standalone","aiScreenCreditRange","aiStandaloneScreenCreditRange","aiPlanCreditRange","screenRange","standalonePages","aiJobPlanCreditRange","aiSiteCreditRange","email","products","Array","from","part"],"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 { aiSiteKind } from './ai-site-kinds'\nimport {\n AI_BUILD_PLAN_CREATION_NOUNS,\n aiPlanFormDatasetNames,\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'\nimport { FREE_AI_TASTE_CREDITS_PER_MONTH } from '../plan-entitlements'\nimport {\n AI_CREDIT_RANGE_ZERO,\n AI_MEASURED_PAID_SITE_CREDITS,\n AI_MEASURED_PASS_CREDITS,\n AI_MEASURED_STANDALONE_CREATION_CREDITS,\n AI_MEASURED_UNIT_CREDITS,\n aiCreditRangeAdd,\n aiCreditRangeOf,\n aiCreditRangeOrdered,\n aiCreditsPromptFor,\n aiMeasuredPageCredits,\n aiMeasuredStandalonePageCredits,\n type AiCreditRange,\n type AiCreditsPrompt,\n type AiMeasuredCredits,\n} from './ai-credit-estimate'\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 page.\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 site's look (AGL-3660): its one answer at its ceiling, on the fast tier. */\n look: number\n /**\n * The layout a site with none has built first (AGL-3660): its header and\n * footer as ONE answer in the layout language, writing its cached prefix,\n * at the most a live frame answer wrote, and never under what the live\n * guided start's layout step metered.\n */\n layout: number\n /** The saved form a guided start makes (AGL-3596), at what the live guided start's form step metered. */\n form: number\n /**\n * A language page's answer before its sections (AGL-3660): its cached\n * prefix written, its turn, and the whole room its ceiling keeps for\n * thinking. A page is ONE answer, not a pass a section.\n */\n page: number\n /** Each planned section's words in that answer, at the most a section is written in. */\n section: number\n /**\n * Room for one page asked again: the answer once more, reading its cached\n * prefix, with its thinking room and a full page of sections.\n */\n retry: number\n /** A page's listing. */\n listing: number\n}\n\n/**\n * The Free site start's wall at its worst (AGL-3594, re-derived for the\n * layout language in AGL-3660): the plan and the look as\n * `jobs/ai-job-free-site.spec.ts` derives them from their requests at their\n * ceilings; the layout, form and pages from the language requests as they\n * stand, priced as each field says. 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 // The plan, the look and a listing run on the fast tier, billed at the\n // platform's markup on Claude Haiku 5.5 since AGL-3781 (36, 7 and 3 credits\n // at Haiku 4.5's cost before).\n plan: 5,\n look: 2,\n layout: 23,\n form: 29,\n page: 43,\n section: 4,\n retry: 64,\n listing: 1,\n}\n\n/**\n * The most sections a Free site plans across its pages (AGL-3660), whatever\n * the wall leaves: a full home of five and a page of two or three. A product\n * figure, so the spend a site EXPECTS stays near what it measured; the wall\n * proves it fits.\n */\nexport const AI_FREE_SITE_MAX_SECTIONS = 8\n\n/** What a Free site start builds before its sections: the layouts it builds first, its forms and its pages. */\nexport interface AiFreeSiteCreations {\n layouts: number\n pages: number\n /** The saved forms it makes; a guided start makes one where it places a form. */\n forms: number\n}\n\n/** A Free site start's credits at its worst for this many sections, its room for one retried page included. */\nexport function aiFreeSiteWorstCaseCredits(\n creations: AiFreeSiteCreations,\n sections: number,\n credits: Readonly<AiFreeSiteWorstCase> = AI_FREE_SITE_WORST_CASE_CREDITS,\n): number {\n const pages = Math.max(1, Math.floor(creations.pages))\n return (\n credits.plan +\n credits.look +\n creations.layouts * credits.layout +\n creations.forms * credits.form +\n pages * (credits.page + credits.listing) +\n sections * credits.section +\n credits.retry\n )\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 (AGL-3660): the plan, the look, the layout and form it\n * builds first, each page's answer and listing, ROOM FOR ONE RETRIED PAGE,\n * and as many sections as the rest pays for — at most\n * `AI_FREE_SITE_MAX_SECTIONS`, and at most what the pages hold. None when the\n * rest is already past the wall.\n */\nexport function aiFreeSiteSectionsWithin(\n creations: AiFreeSiteCreations,\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 = aiFreeSiteWorstCaseCredits(creations, 0, credits)\n if (before > taste) return 0\n const paid = credits.section > 0 ? Math.floor((taste - before) / credits.section) : Number.POSITIVE_INFINITY\n return Math.min(AI_FREE_SITE_MAX_SECTIONS, pages * AI_SITE_MAX_SECTIONS, paid)\n}\n\n/**\n * What a Free site start of this many pages can cost at its worst, in\n * credits (AGL-3660): `aiFreeSiteWorstCaseCredits` for an empty site — the\n * plan, its look, the layout and form a guided start builds, each page's\n * answer and listing, the sections its plan is held to\n * (`aiFreeSiteSectionsWithin` at the month's whole taste, which is what the\n * plan step allows) and the room for one retried page.\n *\n * The CEILING a start is quoted with (\"up to 283\"), no longer the figure it\n * is admitted on (AGL-3722): that is the measured p90\n * (`aiFreeSiteCreditRange`), and a start past what is left is asked to be\n * confirmed and pauses with Resume when its credits run out, instead of\n * being held back from starting at all.\n */\nexport function aiFreeSiteCreditEstimate(\n pages: number,\n credits: Readonly<AiFreeSiteWorstCase> = AI_FREE_SITE_WORST_CASE_CREDITS,\n): number {\n const creations: AiFreeSiteCreations = { layouts: 1, forms: 1, pages: Math.max(1, Math.floor(pages)) }\n const sections = aiFreeSiteSectionsWithin(creations, FREE_AI_TASTE_CREDITS_PER_MONTH, credits)\n return aiFreeSiteWorstCaseCredits(creations, sections, credits)\n}\n\n/**\n * What a Free workspace has left of its Free AI credits this month: the less\n * of its own band's balance and its owner's, since the owner's allowance is\n * shared by every Free workspace they hold (AGL-2925) and the reservation\n * refuses at whichever wall is reached first. `resetsOn` is the UTC day the\n * month — and with it both balances — rolls over, `YYYY-MM-DD`.\n */\nexport interface AiFreeCreditsLeft {\n left: number\n total: number\n resetsOn: string\n /**\n * Both meters `left` is the lesser of, in credits net of give-backs, for the\n * usage strip's live read (AGL-3722): the owner's Free allowance across\n * their Free workspaces (`account`, `null` with no owner) and this\n * workspace's band.\n */\n used?: {\n account: number | null\n org: number\n orgBand: number | null\n /** The strip's warn state for these figures, worked out where the strip's own rule is not shipped to the browser. */\n state: 'ok' | 'warn' | 'capped'\n }\n}\n\n/** The first UTC day of the month after `now`, `YYYY-MM-DD`: when the Free credits come back. */\nexport function aiFreeCreditsResetOn(now: Date): string {\n const next = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + 1, 1))\n return next.toISOString().slice(0, 10)\n}\n\n/** A reset day as a person reads it: \"November 1\". */\nexport function aiFreeCreditsResetLabel(resetsOn: string): string {\n const at = new Date(`${resetsOn}T00:00:00.000Z`)\n return Number.isFinite(at.getTime())\n ? at.toLocaleDateString('en-US', { month: 'long', day: 'numeric', timeZone: 'UTC' })\n : 'the first of next month'\n}\n\n/**\n * What a Free site start of this many pages is likely to cost, its p90 and\n * its ceiling (AGL-3722), before a plan names its sections: the plan, the\n * look, the layout and form a guided start builds, and each page at the\n * nominal section count, at what each MEASURED in production\n * (`AI_MEASURED_UNIT_CREDITS`); the ceiling is the whole worst case the wall\n * is proven with (`aiFreeSiteCreditEstimate`). Two pages: about 137\n * (p90 213, up to 283).\n */\nexport function aiFreeSiteCreditRange(pages: number): AiCreditRange {\n const count = Math.max(1, Math.floor(pages))\n return aiCreditRangeOrdered({\n ...aiSiteStartMeasured(count),\n ceiling: aiFreeSiteCreditEstimate(count),\n })\n}\n\n/** A site start's measured likely and p90 before its plan: plan, look, layout, form and its pages at the nominal sections. */\nfunction aiSiteStartMeasured(pages: number): Pick<AiCreditRange, 'likely' | 'p90'> {\n const { plan, theme, layout, form } = AI_MEASURED_UNIT_CREDITS\n const page = aiMeasuredPageCredits(AI_SITE_NOMINAL_SECTIONS)\n return {\n likely: plan.median + theme.median + layout.median + form.median + pages * page.median,\n p90: plan.p90 + theme.p90 + layout.p90 + form.p90 + pages * page.p90,\n }\n}\n\n/** The smaller first build a site start offers: its home page alone. */\nexport const AI_SITE_HOME_FIRST_LABEL = 'Build the home page first'\n\n/**\n * What a Free site start says before it starts when what is left does not\n * cover its p90 (AGL-3722), or `null` when it fits — or when nothing is known\n * about what is left, since the reservation still decides each pass. Not a\n * refusal: the person may build what fits (the job pauses with Resume when\n * the credits run out), build the home page first, or upgrade.\n */\nexport function aiFreeSitePrompt(\n credits: Pick<AiFreeCreditsLeft, 'left' | 'resetsOn'> | null | undefined,\n pages: number,\n): AiCreditsPrompt | null {\n if (!credits || !Number.isFinite(credits.left)) return null\n const count = Math.max(1, Math.floor(pages))\n const home = aiFreeSiteCreditRange(1)\n return aiCreditsPromptFor(\n aiFreeSiteCreditRange(count),\n credits,\n count > 1 ? { label: AI_SITE_HOME_FIRST_LABEL, ...home } : null,\n )\n}\n\n/**\n * Why a Free start is refused outright (AGL-3722): nothing at all is left,\n * so there is nothing for \"build what fits\" to build.\n */\nexport function aiFreeCreditsNoneLeftText(resetsOn: string): string {\n return (\n `You have no free AI credits left this month. They are shared by all your Free workspaces and ` +\n `renew on ${aiFreeCreditsResetLabel(resetsOn)}. Upgrade this workspace to build with AI now.`\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 * The fewest sections a site start's home page is planned with (AGL-3660).\n * A new site is a full website (Zach, 2026-10-04): its home reads as one — a\n * hero, then bands such as the offer, why us, social proof, and a closing\n * call to action. A plan is an outline and nothing asked for more than a\n * ceiling, so a live Free yoga start (2026-10-07) planned its home with two\n * sections. The plan step names this count in the plan's turn and re-asks a\n * first answer whose home is under it (`aiSiteThinHomeViolations`).\n */\nexport const AI_SITE_HOME_MIN_SECTIONS = 5\n\n/**\n * The blog a paid guided start writes its first posts into (AGL-3676), and\n * the addresses it may answer at, in order of preference: the first one no\n * planned page takes.\n */\nexport const AI_SITE_BLOG_NAME = 'Blog'\nexport const AI_SITE_BLOG_SLUGS = ['blog', 'posts', 'journal', 'articles', 'writing', 'stories'] as const\n\n/** A planned page's first path segment, lower-cased: `/journal/2026` → `journal`. */\nconst firstSegment = (slug: string) => slug.trim().replace(/^\\/+/, '').split('/')[0].toLowerCase()\n\n/** The address the blog answers at, beside these planned pages: the first of `AI_SITE_BLOG_SLUGS` none of them takes. */\nexport function aiSiteBlogSlug(screens: ReadonlyArray<{ slug: string }>): string {\n const taken = new Set(screens.map((screen) => firstSegment(screen.slug)).filter(Boolean))\n return AI_SITE_BLOG_SLUGS.find((slug) => !taken.has(slug)) ?? AI_SITE_BLOG_SLUGS[0]\n}\n\n/** The header's link to the blog (AGL-3660): a path, not a page, since the blog is the posts collection. */\nexport const AI_SITE_BLOG_NAV_ID = 'aiSiteBlog'\n\n/** The nav entry a site whose start writes posts links its blog by. */\nexport function aiSiteBlogNavPage(screens: ReadonlyArray<{ slug: string }>): {\n id: string\n label: string\n slug: string\n href: string\n} {\n const slug = aiSiteBlogSlug(screens)\n return { id: AI_SITE_BLOG_NAV_ID, label: AI_SITE_BLOG_NAME, slug: `/${slug}`, href: `/${slug}` }\n}\n\n/**\n * Whether a planned page stands in for the blog a start writes (AGL-3660):\n * its address is one of the blog's, or its name says it lists posts. The\n * home page never does.\n */\nexport function aiSiteIsBlogStandIn(screen: { slug: string; title: string }): boolean {\n if (aiSitePlanIsHome(screen)) return false\n return (AI_SITE_BLOG_SLUGS as readonly string[]).includes(firstSegment(screen.slug)) || BLOG_PAGE_WORDS.test(screen.title)\n}\n\n/**\n * A start that writes posts, with its plan's stand-in pages taken out\n * (AGL-3676): the plan step asks once for a plan without one and keeps a\n * second answer that still has one, so the scaffold merges it into the blog\n * instead — the page is not built, its links go to the blog, and the blog\n * takes the address it left free. The kept screens hold their plan index,\n * which a page unit's slot is named by.\n */\nexport function aiSitePlanWithoutBlogStandIns<T extends { slug: string; title: string }>(\n screens: readonly T[],\n): { screens: T[]; standIns: T[] } {\n return {\n screens: screens.filter((screen) => !aiSiteIsBlogStandIn(screen)),\n standIns: screens.filter((screen) => aiSiteIsBlogStandIn(screen)),\n }\n}\n\n/** The code a planned page that stands in for the written blog is re-asked under. */\nexport const AI_SITE_BLOG_PAGE_CODE = 'plan-blog-page-duplicate'\n\n/** A page's name that says it lists the posts. */\nconst BLOG_PAGE_WORDS = /\\b(blog|posts?|articles?|journal|writing|stories|news)\\b/i\n\n/**\n * A site plan's pages that stand in for the blog its start writes (AGL-3660):\n * the live Slow Roads start (2026-10-08) planned an \"Articles\" page of\n * featured cards beside the posts it wrote at /blog, and the header linked\n * Articles and never the blog. A page whose address is one of the blog's, or\n * whose name says it lists posts, duplicates it; the home page never does.\n */\nexport function aiSiteBlogStandInViolations(\n plan: Pick<AiBuildPlan, 'screens'>,\n): Array<{ rule: null; code: string; message: string; paths: string[] }> {\n const standIns = plan.screens.flatMap((screen, index) => (aiSiteIsBlogStandIn(screen) ? [{ screen, index }] : []))\n if (!standIns.length) return []\n const names = standIns.map(({ screen }) => `\"${screen.title}\" at ${screen.slug}`).join(', ')\n return [\n {\n rule: null,\n code: AI_SITE_BLOG_PAGE_CODE,\n message: `${names} ${standIns.length === 1 ? 'stands' : 'stand'} in for the blog this site already gets: its first posts are written at /blog, and the header links it. Take ${standIns.length === 1 ? 'that page' : 'those pages'} out, and plan another page the brief needs, or feature the posts in a section of the home page.`,\n paths: standIns.map(({ index }) => `screens[${index}]`),\n },\n ]\n}\n\n/** The code a site plan's work section planned without its pieces is re-asked under. */\nexport const AI_SITE_EMPTY_GALLERY_CODE = 'plan-empty-gallery'\n\n/** The pieces a section that shows the work is planned with (AGL-3660). */\nexport const AI_SITE_GALLERY_MIN_ITEMS = 3\nexport const AI_SITE_GALLERY_MAX_ITEMS = 6\n\n/**\n * A planned section that shows the work: a gallery, a portfolio, a\n * collection, selected or featured work. \"Galleries\" — an audience, as in\n * \"Why galleries choose us\" — and \"how we work\" are not.\n */\nconst GALLERY_SECTION =\n /\\b(gallery|portfolio|collections?|lookbook|showcase|works|projects|(?:selected|featured|recent|our|past) (?:work|pieces|projects))\\b/i\n\n/** A section that only opens, introduces or closes a page, whatever page it is on: \"Portfolio Hero\". */\nconst FRAMING_SECTION = /\\b(hero|intro(?:duction)?|banner|header|cta|call to action|contact|inquiry|enquiry|about)\\b/i\n\n/** Whether a planned section's name says it shows the work. */\nexport function aiSiteSectionShowsWork(name: string): boolean {\n return GALLERY_SECTION.test(name) && !FRAMING_SECTION.test(name)\n}\n\n/** The sentence a site plan's turn states about a section that shows the work. */\nexport const AI_SITE_GALLERY_SENTENCE = `A section that shows the work — a gallery, portfolio, collection or selected works — counts its pieces in its items: ${AI_SITE_GALLERY_MIN_ITEMS} to ${AI_SITE_GALLERY_MAX_ITEMS}.`\n\n/**\n * A site plan's sections that show the work with fewer than three pieces\n * (AGL-3660). The live Juniper Clay start (2026-10-08) planned its Portfolio\n * page's \"Works Gallery\" with no items, and the page's works lost their place\n * to an inquiry form: a page about the work showed none.\n */\nexport function aiSiteEmptyGalleryViolations(\n plan: Pick<AiBuildPlan, 'screens'>,\n): Array<{ rule: null; code: string; message: string; paths: string[] }> {\n const thin = plan.screens.flatMap((screen, screenIndex) =>\n screen.sections.flatMap((section, sectionIndex) =>\n aiSiteSectionShowsWork(section.name) && section.items < AI_SITE_GALLERY_MIN_ITEMS\n ? [{ name: `\"${section.name}\" on ${screen.title}`, path: `screens[${screenIndex}].sections[${sectionIndex}].items` }]\n : [],\n ),\n )\n if (!thin.length) return []\n const names = thin.map((entry) => entry.name).join(', ')\n return [\n {\n rule: null,\n code: AI_SITE_EMPTY_GALLERY_CODE,\n message: `${names} ${thin.length === 1 ? 'shows' : 'show'} the work with fewer than ${AI_SITE_GALLERY_MIN_ITEMS} pieces. Plan ${thin.length === 1 ? 'it' : 'each'} with ${AI_SITE_GALLERY_MIN_ITEMS} to ${AI_SITE_GALLERY_MAX_ITEMS} items, one for each piece it shows.`,\n paths: thin.map((entry) => entry.path),\n },\n ]\n}\n\n/** The violation a site plan whose home page is under its fewest sections is re-asked under. */\nexport const AI_SITE_THIN_HOME_CODE = 'plan-thin-home'\n\n/** What a site plan's home is composed of, in the words the plan's turn and its re-ask both use. */\nexport const AI_SITE_HOME_BANDS =\n 'a hero first, then bands such as services or the offer, about or why us, and testimonials or other social proof, and a closing call to action or contact band last'\n\n/**\n * The fewest sections this site start's home is held to (AGL-3660): five, or\n * 0 — no minimum — where the Free wall cannot pay for a full home beside the\n * other pages. `across` is the sections the Free taste fits across `pages`\n * pages, each other page keeping at least one; `null` is a paid start, which\n * no wall shares out.\n *\n * ⛔ NEVER a lowered minimum. A Free guided start creates its site empty and\n * plans its layout, and the wall then fits 4 sections across two pages. Told\n * \"a home of at least 3\" beside that, the live yoga start of 2026-10-08\n * planned 4 + 3 and then 4 + 2, was refused for the wall twice, and stopped:\n * a home asked for in bands the wall cannot pay for is a start that fails.\n */\nexport function aiSiteHomeMinSections(input: { pages: number; across: number | null }): number {\n if (input.across === null) return AI_SITE_HOME_MIN_SECTIONS\n const others = Math.max(0, Math.floor(input.pages) - 1)\n return input.across - others >= AI_SITE_HOME_MIN_SECTIONS ? AI_SITE_HOME_MIN_SECTIONS : 0\n}\n\n/**\n * What a site start's plan is told to BUILD with (AGL-3660): the sections are\n * a budget to use, never a ceiling to stay under. Told only \"at most 8\", the\n * live prod start of 2026-10-08 planned a home of two sections, and Zach: \"that\n * didn't mean do as little as possible.\" On the Free taste the sentence shares\n * the wall's sections out — a home of five or six, the other page two or\n * three, about seven or eight in all; a paid start's home is asked to be rich.\n * `min` is the home's fewest (`aiSiteHomeMinSections`), 0 where the plan\n * plans no home or the wall cannot pay for a full one.\n */\nexport function aiSiteFullPlanSentence(input: { pages: number; across: number | null; min: number }): string {\n const { across, min } = input\n if (across === null) {\n return min > 0\n ? `Plan a full website, never a minimal one: a rich home page of ${min + 1} or more sections where the brief gives them, and every page with the sections it needs.`\n : 'Plan a full website, never a minimal one: every page with the sections it needs.'\n }\n const lead = `The ${across} sections are the budget to use, not a ceiling to stay under: plan a full website, never a minimal one`\n const others = Math.max(0, Math.floor(input.pages) - 1)\n if (min <= 0 || across < min) return `${lead}.`\n const homeHi = Math.max(min, Math.min(min + 1, across - others))\n if (!others) return `${lead}: the home page at / with ${min} to ${homeHi} sections.`\n const otherHi = Math.max(1, Math.min(3, Math.floor((across - min) / others)))\n const otherLo = Math.max(1, Math.min(2, otherHi, Math.floor((across - homeHi) / others)))\n const totalLo = Math.max(min + others, across - 1)\n const range = (lo: number, hi: number) => (lo === hi ? `${lo}` : `${lo} to ${hi}`)\n const pagesWord = others === 1 ? 'the other page' : 'each other page'\n return `${lead}: the home page at / with ${range(min, homeHi)} sections, ${pagesWord} ${range(otherLo, otherHi)}, about ${range(totalLo, across)} in total.`\n}\n\n/** Whether a planned page is the site's home: the one at `/`. */\nexport function aiSitePlanIsHome(screen: { slug: string }): boolean {\n const slug = screen.slug.trim()\n return slug === '/' || slug === ''\n}\n\n/**\n * A site plan's home page under its fewest sections (AGL-3660), as the plan\n * step's re-ask names it. A plan with no page at `/` — the owner's own home\n * stays — has no home to hold. `across` is the Free wall's section count, so\n * the re-ask says where the sections come from rather than tripping the wall.\n */\nexport function aiSiteThinHomeViolations(\n plan: Pick<AiBuildPlan, 'screens'>,\n options: { min: number; across: number | null },\n): Array<{ rule: null; code: string; message: string; paths: string[] }> {\n const index = plan.screens.findIndex(aiSitePlanIsHome)\n if (index < 0) return []\n const count = plan.screens[index].sections.length\n if (count >= options.min) return []\n const within =\n options.across === null\n ? ''\n : ` Keep the whole plan within ${options.across} sections: take them from the other page where you must.`\n return [\n {\n rule: null,\n code: AI_SITE_THIN_HOME_CODE,\n message: `The home page has ${count} ${count === 1 ? 'section' : 'sections'}, and a new site's home page reads as a full website with at least ${options.min}: ${AI_SITE_HOME_BANDS}.${within}`,\n paths: [`screens[${index}].sections`],\n },\n ]\n}\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 /**\n * The kind of site the person picked (AGL-3660), by its `AI_SITE_KINDS`\n * id; empty when nobody picked one, and the business type suggests it.\n * It sets the look the site is designed in and how its pages are arranged.\n */\n siteKind: 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/**\n * What a site job's plan and every unit it builds are told about the\n * business's name (AGL-3596): that it is the name, used as written. Said on\n * the job's own turn, never in a cached system block, because it is one\n * site's. A model handed only what the business does named it itself —\n * \"Hillside Dog Grooming\" was built as \"Austin Paws Grooming\".\n */\nexport function aiSiteNameSentence(name: string): string {\n return `The business is named “${name}”: the site names it exactly that way, in the header, the footer, the copy and the search listing, and never by any other name.`\n}\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 // A kind this deployment does not know is nobody having picked one.\n const siteKind = aiSiteKind(inputs?.['siteKind'])?.id ?? ''\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 siteKind,\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, the palette suggestion a member applies in the Theme\n * section, and the datasets its structured content is kept in (AGL-3616),\n * which the data plugin writes. Anything else a plan asks to create is a job\n * of its own.\n */\nexport const AI_SITE_CREATE_KINDS: readonly AiBuildPlanCreateKind[] = [\n 'layout',\n 'form',\n 'theme-change',\n 'dataset',\n]\n\n/** The most datasets one site start creates (AGL-3616). */\nexport const AI_SITE_DATASETS_MAX = 3\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 const datasets = plan.create.filter((entry) => entry.kind === 'dataset').length\n if (datasets > AI_SITE_DATASETS_MAX) {\n return `This plan creates ${datasets} datasets, and a site scaffold creates at most ${AI_SITE_DATASETS_MAX}. Keep the others' items on their pages.`\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 /** Each page and creation written on its own (`aiStandaloneScreenCreditRange`), not as a site start’s (AGL-3722). */\n standalonePages?: boolean\n}\n\n/**\n * Whether a creation is built with no model: a dataset a form of the plan\n * writes to (AGL-3616) starts empty, designed from its planned fields, so it\n * is no pass of the estimate.\n */\nfunction aiPlanCreationIsModelFree(plan: Pick<AiBuildPlan, 'create'>, entry: AiBuildPlan['create'][number]): boolean {\n return entry.kind === 'dataset' && aiPlanFormDatasetNames(plan).has(entry.name.trim().toLowerCase())\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) && !aiPlanCreationIsModelFree(plan, entry)).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; siteKind?: string | null } = {},\n): number {\n const screens = Math.max(0, Math.floor(pages))\n const content = aiSiteStartContent(options.siteKind)\n const passes =\n 1 +\n screens * (AI_SITE_NOMINAL_SECTIONS + 1) +\n 2 +\n (options.welcomeEmail ? 1 : 0) +\n (content === 'products' ? 1 : content === 'posts' ? AI_SITE_START_POSTS : 0)\n return passes * AI_SITE_PASS_CREDITS\n}\n\n/** How many first posts a paid blog start writes, one pass each (`AI_SITE_POSTS`, held equal by `ai-site-job.spec.ts`). */\nexport const AI_SITE_START_POSTS = 3\n\n/**\n * The first content a paid start of this kind writes (AGL-3676): a store's\n * first products, a blog's first posts, or nothing. `aiSiteContentPart`'s\n * answer for a paid start, by the kind's id.\n */\nexport function aiSiteStartContent(siteKind: string | null | undefined): 'posts' | 'products' | null {\n if (siteKind === 'store') return 'products'\n if (siteKind === 'blog') return 'posts'\n return null\n}\n\n/**\n * The measured cost of one creation a plan builds (AGL-3722): a layout's or\n * a form's as measured, a theme's as the site start's look measured, and\n * any other kind at the stand-in a single-answer creation is priced at.\n * `standalone` for one written on its own — a build's, a page job's — rather\n * than inside a site start's warm run.\n */\nexport function aiCreationMeasuredCredits(kind: string, options: { standalone?: boolean } = {}): AiMeasuredCredits {\n if (kind === 'layout') {\n return options.standalone ? AI_MEASURED_STANDALONE_CREATION_CREDITS.layout : AI_MEASURED_UNIT_CREDITS.layout\n }\n if (kind === 'form') return AI_MEASURED_UNIT_CREDITS.form\n if (kind === 'theme' || kind === 'theme-change') return AI_MEASURED_UNIT_CREDITS.theme\n return AI_MEASURED_PASS_CREDITS\n}\n\n/** A planned page's range: its sections scaled around the measured page, its ceiling one pass a section and one for its listing. */\nexport function aiScreenCreditRange(sections: number): AiCreditRange {\n return aiCreditRangeOf(aiMeasuredPageCredits(sections), (Math.max(0, sections) + 1) * AI_SITE_PASS_CREDITS)\n}\n\n/**\n * A page written on its own — an Assist build's, a page job's — as a range\n * (AGL-3722): a fixed cost and one per section as such pages measured\n * (`AI_MEASURED_STANDALONE_PAGE`), never a site start's later, cheaper\n * pages; the ceiling as `aiScreenCreditRange`'s.\n */\nexport function aiStandaloneScreenCreditRange(sections: number): AiCreditRange {\n return aiCreditRangeOf(\n aiMeasuredStandalonePageCredits(sections),\n (Math.max(0, sections) + 1) * AI_SITE_PASS_CREDITS,\n )\n}\n\n/**\n * What a plan is likely to cost, its p90 and its ceiling (AGL-3722): the\n * same units `aiPlanCreditEstimate` counts, each at what it measured; the\n * ceiling IS `aiPlanCreditEstimate`.\n */\nexport function aiPlanCreditRange(plan: AiBuildPlan, options: AiPlanPassOptions = {}): AiCreditRange {\n const creates = options.creates ?? AI_SITE_CREATE_KINDS\n let range = AI_CREDIT_RANGE_ZERO\n const screenRange = options.standalonePages ? aiStandaloneScreenCreditRange : aiScreenCreditRange\n for (const screen of plan.screens) range = aiCreditRangeAdd(range, screenRange(screen.sections.length))\n for (const entry of plan.create) {\n if (!creates.includes(entry.kind) || aiPlanCreationIsModelFree(plan, entry)) continue\n range = aiCreditRangeAdd(\n range,\n aiCreditRangeOf(aiCreationMeasuredCredits(entry.kind, { standalone: options.standalonePages }), AI_SITE_PASS_CREDITS),\n )\n }\n if (options.welcomeEmail) range = aiCreditRangeAdd(range, aiCreditRangeOf(AI_MEASURED_PASS_CREDITS, AI_SITE_PASS_CREDITS))\n return aiCreditRangeOrdered(range)\n}\n\n/**\n * `aiJobPlanCreditEstimate` as a range (AGL-3722): the same kinds, each at\n * what it measured. Only a site start writes its pages in one warm run; every\n * other kind's page is written on its own.\n */\nexport function aiJobPlanCreditRange(kind: string, plan: AiBuildPlan): AiCreditRange {\n const creates =\n kind === 'page' ? AI_PAGE_CREATE_KINDS : AI_JOB_CREATE_KINDS[kind as keyof typeof AI_JOB_CREATE_KINDS]\n return aiPlanCreditRange(plan, { ...(creates ? { creates } : {}), standalonePages: kind !== 'site' })\n}\n\n/**\n * A paid site start's range before its plan (AGL-3722): the plan, its look,\n * layout and form, and each page at the nominal sections, as measured; the\n * ceiling is `aiSiteCreditEstimate`.\n */\nexport function aiSiteCreditRange(\n pages: number,\n options: { welcomeEmail?: boolean; siteKind?: string | null } = {},\n): AiCreditRange {\n const count = Math.max(0, Math.floor(pages))\n // A paid start plans and writes on the paid routing, measured apart (AGL-3739).\n const { plan, theme, layout, form, page, email, products } = AI_MEASURED_PAID_SITE_CREDITS\n const content = aiSiteStartContent(options.siteKind)\n // A blog's posts have no measurement of their own yet: the stand-in pass.\n const parts: AiMeasuredCredits[] = [\n plan,\n theme,\n layout,\n form,\n ...Array.from({ length: count }, () => page),\n ...(options.welcomeEmail ? [email] : []),\n ...(content === 'products' ? [products] : content === 'posts' ? [AI_MEASURED_PASS_CREDITS] : []),\n ]\n return aiCreditRangeOrdered({\n likely: parts.reduce((total, part) => total + part.median, 0),\n p90: parts.reduce((total, part) => total + part.p90, 0),\n ceiling: aiSiteCreditEstimate(count, options),\n })\n}\n"],"names":["aiSiteKind","AI_BUILD_PLAN_CREATION_NOUNS","aiPlanFormDatasetNames","aiPlanUndeclaredRefs","AI_JOB_CREATE_KINDS","AI_PAGE_CREATE_KINDS","FREE_AI_TASTE_CREDITS_PER_MONTH","AI_CREDIT_RANGE_ZERO","AI_MEASURED_PAID_SITE_CREDITS","AI_MEASURED_PASS_CREDITS","AI_MEASURED_STANDALONE_CREATION_CREDITS","AI_MEASURED_UNIT_CREDITS","aiCreditRangeAdd","aiCreditRangeOf","aiCreditRangeOrdered","aiCreditsPromptFor","aiMeasuredPageCredits","aiMeasuredStandalonePageCredits","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","look","layout","form","page","section","retry","listing","AI_FREE_SITE_MAX_SECTIONS","aiFreeSiteWorstCaseCredits","creations","sections","credits","pages","Math","floor","layouts","forms","aiFreeSiteSectionsWithin","taste","before","Number","POSITIVE_INFINITY","AI_SITE_MAX_SECTIONS","aiFreeSiteCreditEstimate","aiFreeCreditsResetOn","now","next","Date","UTC","getUTCFullYear","getUTCMonth","toISOString","slice","aiFreeCreditsResetLabel","resetsOn","at","isFinite","getTime","toLocaleDateString","month","day","timeZone","aiFreeSiteCreditRange","count","aiSiteStartMeasured","ceiling","theme","AI_SITE_NOMINAL_SECTIONS","likely","median","p90","AI_SITE_HOME_FIRST_LABEL","aiFreeSitePrompt","left","home","label","aiFreeCreditsNoneLeftText","AI_SITE_HOME_MIN_SECTIONS","AI_SITE_BLOG_NAME","AI_SITE_BLOG_SLUGS","firstSegment","slug","trim","replace","split","toLowerCase","aiSiteBlogSlug","screens","taken","Set","map","screen","filter","Boolean","find","has","AI_SITE_BLOG_NAV_ID","aiSiteBlogNavPage","id","href","aiSiteIsBlogStandIn","aiSitePlanIsHome","includes","BLOG_PAGE_WORDS","test","title","aiSitePlanWithoutBlogStandIns","standIns","AI_SITE_BLOG_PAGE_CODE","aiSiteBlogStandInViolations","flatMap","index","length","names","join","rule","code","message","paths","AI_SITE_EMPTY_GALLERY_CODE","AI_SITE_GALLERY_MIN_ITEMS","AI_SITE_GALLERY_MAX_ITEMS","GALLERY_SECTION","FRAMING_SECTION","aiSiteSectionShowsWork","name","AI_SITE_GALLERY_SENTENCE","aiSiteEmptyGalleryViolations","thin","screenIndex","sectionIndex","items","path","entry","AI_SITE_THIN_HOME_CODE","AI_SITE_HOME_BANDS","aiSiteHomeMinSections","input","across","others","aiSiteFullPlanSentence","lead","homeHi","otherHi","otherLo","totalLo","range","lo","hi","pagesWord","aiSiteThinHomeViolations","options","findIndex","within","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","blurb","AI_SITE_EMAIL_TYPE","aiSiteNameSentence","aiSiteSubmissions","inputs","value","ID_CHARS","parseAiSiteJobInputs","text","key","undefined","trimmed","businessType","businessName","city","brand","audience","starter","siteKind","rawPages","NaN","isInteger","submissions","rawBatch","batchId","welcomeEmail","aiSitePagesRefusal","band","aiSiteWordsSaidAnything","words","about","aiSiteWords","read","AI_SITE_CREATE_KINDS","AI_SITE_DATASETS_MAX","aiSitePlanPrerequisites","seen","prerequisites","add","kind","push","create","aiSitePlanShapeRefusal","slugs","some","nav","datasets","aiSitePlanRefusal","shape","parts","noun","where","listed","aiPlanCreationIsModelFree","aiPlanPasses","reduce","total","creates","aiPlanCreditEstimate","aiJobPlanCreditEstimate","aiSiteCreditEstimate","content","aiSiteStartContent","passes","AI_SITE_START_POSTS","aiCreationMeasuredCredits","standalone","aiScreenCreditRange","aiStandaloneScreenCreditRange","aiPlanCreditRange","screenRange","standalonePages","aiJobPlanCreditRange","aiSiteCreditRange","email","products","Array","from","part"],"mappings":"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"}
@@ -180,6 +180,11 @@ export interface AiStoreFinishStep {
180
180
  * the layout version a build drafted its store's links into (AGL-3676).
181
181
  */
182
182
  opens: 'store-settings' | 'product' | 'pages' | 'layouts' | 'layout';
183
+ /**
184
+ * The record of `opens` to land on rather than its list: the store's
185
+ * settings are a page each (AGL-3713), and a step opens the one it names.
186
+ */
187
+ record?: string;
183
188
  action: string;
184
189
  }
185
190
  /**
@@ -355,6 +355,7 @@ const PATH = /^\/[a-z0-9-]+$/;
355
355
  title: 'Connect payments',
356
356
  text: 'Connect Stripe so your store can take payments and pay you out.',
357
357
  opens: 'store-settings',
358
+ record: 'payments',
358
359
  action: 'Set up payments'
359
360
  },
360
361
  {
@@ -369,6 +370,7 @@ const PATH = /^\/[a-z0-9-]+$/;
369
370
  title: 'Set shipping and tax',
370
371
  text: 'Choose your shipping rates and how tax is charged at checkout.',
371
372
  opens: 'store-settings',
373
+ record: 'shipping',
372
374
  action: 'Open store settings'
373
375
  },
374
376
  {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-site-store-pages.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 * A STORE'S OWN PAGES (AGL-3676, third round).\n *\n * Zach, 2026-10-10: \"We seem to be missing the account pages for the\n * shop/store AI generation, make sure all they would have to do is setup the\n * payment info and update products to finish up their storefront.\" The\n * Ember & Oak Candle Co start had Home, Shop, Gift sets, About, Shipping &\n * care and Contact, and a product page per product; a shopper had nowhere to\n * sign in, see an order, find what they saved, or read the store's privacy\n * policy and terms.\n *\n * So a paid store start gets the pages every storefront has, written by code\n * from the platform's own elements — never asked of the model:\n *\n * - ACCOUNT at /account: the commerce plugin's Customer account (sign in,\n * create an account, profile, addresses, order history with status and\n * tracking, sign out) and Saved items (the Wishlist the product page's\n * Save fills, kept for a guest in the browser and on the account once\n * they sign in);\n * - CART at /cart: the Cart in full, with its checkout, for a shopper who\n * goes to the address rather than the header's cart drawer;\n * - SHIPPING & RETURNS at /shipping-returns, PRIVACY POLICY at /privacy and\n * TERMS OF SALE at /terms: editable drafts in the Markdown element, every\n * fact only the merchant knows left as a [bracketed placeholder] — never a\n * return window, a price or a promise the brief did not give.\n *\n * The account and the cart are a shopper's own pages, written UNLISTED\n * (noindex, out of the sitemap); the policies are indexable like every other\n * page.\n *\n * An order's own page is the commerce plugin's built-in /order-status, which\n * every order email links with its signed key; the Account page says so.\n *\n * ── Outside the plan's page count ───────────────────────────────────────\n *\n * They are STORE PAGES, not planned pages: the plan's four to eight pages are\n * the content the model designs, and these are added beside them, so they\n * never crowd one out. They still count toward the plan's pages allowance\n * (`screensPerHost`), as every page a site has does; a store is Starter and\n * up, whose allowance holds both.\n *\n * A page the plan already has stands for its store page instead of a second\n * one: a planned \"Shipping & care\" is the store's shipping page, and a page\n * the plan put at /privacy is its privacy policy. The header and footer link\n * that page.\n *\n * Pure: names, paths, words and node trees. Safe for the console to import.\n */\n\n/** A store page, by what it is. Persisted in job outputs (`proposal.storePage`); never renamed. */\nexport type AiStorePageKey = 'account' | 'cart' | 'shipping' | 'privacy' | 'terms'\n\nexport interface AiStorePageDefinition {\n key: AiStorePageKey\n /** The page's name in Pages, and its heading. */\n title: string\n slug: string\n /** What the footer's link to it says. */\n link: string\n seo: { title: string; description: string }\n /**\n * A shopper's own page, not one to find by search: written UNLISTED, the\n * page's Access setting the console offers, so the tenant serves it\n * `noindex` and leaves it out of the sitemap. Only the account and the\n * cart; the policies, like every content and landing page, stay indexable.\n */\n noindex?: true\n}\n\n/** Every store page, in the order the job writes them and the footer links them. */\nexport const AI_STORE_PAGES: readonly AiStorePageDefinition[] = [\n {\n key: 'account',\n title: 'Your account',\n slug: 'account',\n link: 'Your account',\n noindex: true,\n seo: { title: 'Your account', description: 'Sign in to see your orders, saved addresses and saved items.' },\n },\n {\n key: 'cart',\n title: 'Your cart',\n slug: 'cart',\n link: 'Cart',\n noindex: true,\n seo: { title: 'Your cart', description: 'Review your cart and check out.' },\n },\n {\n key: 'shipping',\n title: 'Shipping & returns',\n slug: 'shipping-returns',\n link: 'Shipping & returns',\n seo: { title: 'Shipping & returns', description: 'How we ship your order, and how returns and refunds work.' },\n },\n {\n key: 'privacy',\n title: 'Privacy policy',\n slug: 'privacy',\n link: 'Privacy policy',\n seo: { title: 'Privacy policy', description: 'What we collect when you shop with us, and what we do with it.' },\n },\n {\n key: 'terms',\n title: 'Terms of sale',\n slug: 'terms',\n link: 'Terms of sale',\n seo: { title: 'Terms of sale', description: 'The terms that apply when you buy from our store.' },\n },\n]\n\n/** What the job's row for the store pages reads. */\nexport const AI_SITE_STORE_PAGES_LABEL = 'Adding your account, cart and policy pages'\n\n/** The note the row carries once they are written: what the merchant fills in. */\nexport const AI_SITE_STORE_PAGES_NOTE =\n 'Your shipping, privacy and terms pages are drafts: fill in the parts in [brackets] with your store’s own details.'\n\n/** A planned page whose words say it is the store's shipping page: \"Shipping & care\", \"Delivery & returns\". */\nconst SHIPPING_WORDS = /\\b(shipping|delivery|returns?|refunds?)\\b/i\n\n/** The words of a planned page's address and title, spaced. */\nconst wordsOf = (screen: { slug: string; title: string }) => `${screen.slug.replace(/[-/_]+/g, ' ')} ${screen.title}`\n\nconst firstSegment = (slug: string) => slug.trim().replace(/^\\/+/, '').split('/')[0].toLowerCase()\n\n/** A store page as a start builds or links it. */\nexport interface AiStorePagePlan {\n key: AiStorePageKey\n title: string\n slug: string\n /** Where the header and footer link it. */\n href: string\n /** What the footer's link says. */\n link: string\n /**\n * The planned page that stands for it, by its title, where the plan has\n * one: nothing is written for this key, and its links go to that page.\n */\n planned?: string\n}\n\n/**\n * The store pages a store plan gets: each one written, or — where a planned\n * page already holds its address, or a planned page's words say it is the\n * store's shipping page — that planned page, linked in its place.\n */\nexport function aiStorePagesOfPlan(screens: ReadonlyArray<{ slug: string; title: string; record?: unknown }>): AiStorePagePlan[] {\n const pages = screens.filter((screen) => !screen.record && firstSegment(screen.slug) !== '')\n return AI_STORE_PAGES.map((page) => {\n const holds =\n pages.find((screen) => firstSegment(screen.slug) === page.slug) ??\n (page.key === 'shipping' ? pages.find((screen) => SHIPPING_WORDS.test(wordsOf(screen))) : undefined)\n if (holds) {\n const slug = firstSegment(holds.slug)\n return { key: page.key, title: page.title, slug, href: `/${slug}`, link: holds.title.trim() || page.link, planned: holds.title }\n }\n return { key: page.key, title: page.title, slug: page.slug, href: `/${page.slug}`, link: page.link }\n })\n}\n\n/** The store pages a start writes itself: those no planned page stands for. */\nexport function aiStorePagesToWrite(pages: readonly AiStorePagePlan[]): AiStorePagePlan[] {\n return pages.filter((page) => !page.planned)\n}\n\n/** The unit input a site layout is told the store's links under. */\nexport const AI_SITE_STORE_LINKS_INPUT = 'storeLinks'\n\n/** What a store's header and footer link (AGL-3676): the account beside the cart, and the store's pages under the site's. */\nexport interface AiStoreFrameLinks {\n /** The account page's path, which the header links beside the cart. */\n account: string | null\n /** The footer's store links, in order: the account, then the policies. */\n footer: Array<{ label: string; href: string }>\n}\n\n/** The header's and footer's store links for these store pages. The cart is the header's own button. */\nexport function aiStoreFrameLinks(pages: readonly AiStorePagePlan[]): AiStoreFrameLinks {\n const account = pages.find((page) => page.key === 'account')\n return {\n account: account?.href ?? null,\n footer: pages.filter((page) => page.key !== 'cart').map((page) => ({ label: page.link, href: page.href })),\n }\n}\n\nconst PATH = /^\\/[a-z0-9-]+$/\n\n/** The store links a layout unit's inputs carry; none for any other layout. */\nexport function aiStoreFrameLinksOf(inputs: Readonly<Record<string, unknown>> | null | undefined): AiStoreFrameLinks | null {\n const raw = inputs?.[AI_SITE_STORE_LINKS_INPUT]\n if (!raw || typeof raw !== 'object') return null\n const { account, footer } = raw as Record<string, unknown>\n const links = Array.isArray(footer)\n ? footer.flatMap((entry) => {\n const { label, href } = (entry ?? {}) as Record<string, unknown>\n return typeof label === 'string' && label.trim() && typeof href === 'string' && PATH.test(href)\n ? [{ label: label.trim().slice(0, 40), href }]\n : []\n })\n : []\n const accountPath = typeof account === 'string' && PATH.test(account) ? account : null\n return accountPath || links.length ? { account: accountPath, footer: links.slice(0, 6) } : null\n}\n\n// ── The pages, as node trees ──────────────────────────────────────────────\n\n/** The canvas root every stored page hangs from (`CANVAS_ROOT_ELEMENT_ID`). */\nconst ROOT = '_@_'\n\n/** The longest one Markdown element is given: the element's own text limit. */\nexport const AI_STORE_MARKDOWN_MAX = 2000\n\n/** The commerce plugin's elements a store page places, by the ids it persists (`ai-site-store-pages.spec.ts` holds them). */\nexport const AI_STORE_ELEMENTS = {\n account: 'customer-account',\n wishlist: 'wishlist',\n cart: 'cart',\n} as const\n\n/** The host token that reads the site's own name at render (`AI_SITE_NAME_TOKEN`). */\nconst NAME = '{{host.businessName}}'\n\n/** What a store page's words may link: the site's contact page and its shop, where the plan has them. */\nexport interface AiStorePageFacts {\n /** The contact page's path, or `null` where the plan has none. */\n contactPath: string | null\n /** The shop's path, `/shop` where the plan has none. */\n shopPath: string\n /** The shipping page's path, as the terms link it. */\n shippingPath: string\n}\n\ninterface StoredNode {\n $id: string\n type: 'node'\n componentId: string\n pluginId: string\n parentId: string | null\n nodes: string[]\n props?: Record<string, unknown>\n sx?: Record<string, unknown>\n}\n\n/** How a sentence asks a shopper to get in touch: the contact page, else a reply to their order email. */\nfunction contact(facts: AiStorePageFacts, words = 'contact us'): string {\n return facts.contactPath ? `[${words}](${facts.contactPath})` : `${words} by replying to your order email`\n}\n\n/** The policy's sections, as markdown, each a `##` heading and its paragraphs. */\nexport function aiStorePolicySections(key: 'shipping' | 'privacy' | 'terms', facts: AiStorePageFacts): string[] {\n if (key === 'shipping') {\n return [\n '## Shipping\\n\\nWe ship orders from [where you ship from]. Orders are packed within [number] business days, and you will get an email with tracking as soon as yours is on its way.\\n\\nShipping costs and delivery options are shown at checkout before you pay. [List the countries or regions you ship to, and any free-shipping offer you make.]',\n `## Returns\\n\\nYou can return an item within [number] days of delivery if it is [unused and in its original packaging]. [Name anything that cannot be returned, such as gift cards or personalized items.]\\n\\nTo start a return, open the link in your order email and choose Request a return, or ${contact(facts)}. [Say who pays for return shipping.]`,\n '## Refunds\\n\\nOnce we receive and check your return, we refund it to your original payment method within [number] business days. [Say whether the original shipping cost is refunded.]',\n `## Damaged or wrong items\\n\\nIf your order arrives damaged or is not what you ordered, ${contact(facts)} within [number] days of delivery with your order number and a photo, and we will put it right.`,\n ]\n }\n if (key === 'privacy') {\n return [\n `## Who we are\\n\\n${NAME} runs this online store. This policy explains what we collect when you visit, shop or create an account, and what we do with it. [Add your business’s legal name and postal address.]`,\n '## What we collect\\n\\n- **When you order:** your name, email address, shipping and billing address, your phone number if you give one, and what you bought.\\n- **When you create an account:** your name, email address, saved addresses, order history and saved items.\\n- **When you sign up for emails:** your email address and your choice to hear from us.\\n- **Payments:** card payments are processed by Stripe. We never see or store your full card number.',\n '## How we use it\\n\\nWe use your information to process and deliver your orders, to send order and shipping emails, to answer your questions, and, only if you opt in, to send you news and offers. You can unsubscribe from marketing emails at any time with the link in each one.',\n '## Who we share it with\\n\\nWe share only what is needed with the services that run the store: our website and store platform, our payment processor (Stripe), and the carriers that deliver your order. [List any other services you use, such as an email or analytics tool.] We do not sell your personal information.',\n '## Cookies\\n\\nThis site uses cookies to keep your cart and your sign-in working. [Describe any analytics or advertising cookies you use, and how visitors choose.]',\n `## Your choices\\n\\nYou can ask to see, correct or delete the information we hold about you: ${contact(facts)}. [Say how long you keep order records, and the privacy rights that apply where you sell.]\\n\\nWe post any change to this policy on this page. Last updated: [date].`,\n ]\n }\n return [\n `## About these terms\\n\\nThese terms apply to purchases from ${NAME} through this website. By placing an order you agree to them. [Add your business’s legal name and where it is registered.]`,\n '## Orders\\n\\nYour order is an offer to buy. We accept it when we send your order confirmation email. If an item turns out to be unavailable, we will tell you and refund what you paid for it.',\n '## Prices and payment\\n\\nPrices are shown in [currency] and [include or exclude] [sales tax or VAT]. Shipping and any taxes are shown at checkout before you pay. Payment is taken securely through Stripe when you place your order.',\n `## Shipping, returns and refunds\\n\\nHow we ship, and how returns and refunds work, is set out on our [Shipping & returns](${facts.shippingPath}) page.`,\n '## Products\\n\\nWe describe and photograph our products as accurately as we can. [Note anything that naturally varies, such as handmade pieces, colors or sizes.]',\n `## Liability\\n\\n[State any limit on your liability, as the law where you sell allows.] Nothing in these terms limits your rights under consumer law.\\n\\n## Contact\\n\\nQuestions about an order? ${contact(facts, 'Contact us')}. Last updated: [date].`,\n ]\n}\n\n/** Sections joined into as few Markdown elements as fit the element's limit, in order. */\nexport function aiStoreMarkdownChunks(sections: readonly string[], max = AI_STORE_MARKDOWN_MAX): string[] {\n const chunks: string[] = []\n for (const section of sections) {\n const last = chunks[chunks.length - 1]\n if (last !== undefined && last.length + 2 + section.length <= max) chunks[chunks.length - 1] = `${last}\\n\\n${section}`\n else chunks.push(section.slice(0, max))\n }\n return chunks\n}\n\n/**\n * A store page's node tree, root first, as a page's first version stores it:\n * the page's heading over the platform's elements, in one section the\n * site's layout renders inside.\n */\nexport function aiStorePageNodes(key: AiStorePageKey, facts: AiStorePageFacts): Record<string, StoredNode> {\n const page = AI_STORE_PAGES.find((one) => one.key === key) as AiStorePageDefinition\n const nodes: Record<string, StoredNode> = {}\n const id = (role: string) => `store-${key}-${role}`\n const container = id('container')\n const add = (role: string, componentId: string, pluginId: string, props: Record<string, unknown>, sx?: Record<string, unknown>): string => {\n const nodeId = id(role)\n nodes[nodeId] = { $id: nodeId, type: 'node', componentId, pluginId, parentId: container, nodes: [], props, ...(sx ? { sx } : {}) }\n return nodeId\n }\n const children: string[] = [add('title', 'muiTypography', 'mui', { children: page.title, variant: 'h1', component: 'h1' }, { mb: 4 })]\n if (key === 'account') {\n children.push(\n add('account', AI_STORE_ELEMENTS.account, 'commerce', { signedOutHeading: 'Sign in or create an account' }),\n add(\n 'guest',\n 'muiTypography',\n 'mui',\n { children: 'Checked out as a guest? The link in your order email opens your order’s status and tracking.', variant: 'body2', component: 'p' },\n { color: 'text.secondary', mt: 3 },\n ),\n add(\n 'saved',\n AI_STORE_ELEMENTS.wishlist,\n 'commerce',\n { heading: 'Saved items', emptyText: 'Nothing saved yet. Tap Save on any product to keep it here.' },\n { mt: 8 },\n ),\n )\n } else if (key === 'cart') {\n children.push(\n add('cart', AI_STORE_ELEMENTS.cart, 'commerce', { variant: 'inline', showCoupon: true }),\n add('shop', 'muiButton', 'mui', { children: 'Continue shopping', variant: 'outlined', href: facts.shopPath }, { mt: 4 }),\n )\n } else {\n aiStoreMarkdownChunks(aiStorePolicySections(key, facts)).forEach((content, index) => {\n children.push(add(`text${index + 1}`, 'markdown', 'mui', { content }))\n })\n }\n const section = id('section')\n nodes[ROOT] = { $id: ROOT, type: 'node', componentId: 'div', pluginId: 'mui', parentId: null, nodes: [section] }\n nodes[section] = {\n $id: section,\n type: 'node',\n componentId: 'section',\n pluginId: 'mui',\n parentId: ROOT,\n nodes: [container],\n props: { element: 'section', ariaLabel: page.title },\n }\n nodes[container] = {\n $id: container,\n type: 'node',\n componentId: 'muiContainer',\n pluginId: 'mui',\n parentId: section,\n nodes: children,\n props: { maxWidth: key === 'account' || key === 'cart' ? 'lg' : 'md' },\n sx: { py: { xs: 6, md: 10 } },\n }\n return nodes\n}\n\n// ── Finishing the store ───────────────────────────────────────────────────\n\n/** One thing left before an AI-built store sells, as its done page lists it. */\nexport interface AiStoreFinishStep {\n id: 'payments' | 'products' | 'shipping' | 'policies' | 'links'\n title: string\n text: string\n /**\n * The record route kind whose list the step opens (`plugin-record-routes`),\n * `pages` for the site's Pages, `layouts` for its Layouts, or `layout` for\n * the layout version a build drafted its store's links into (AGL-3676).\n */\n opens: 'store-settings' | 'product' | 'pages' | 'layouts' | 'layout'\n action: string\n}\n\n/**\n * What is left once an AI store is built (Zach, 2026-10-10): connect\n * payments, review the products and their prices, set shipping and tax, and\n * fill in the policies' brackets. Everything else — the storefront, the\n * account, cart and policy pages, the header's cart and account — is done.\n */\nexport const AI_STORE_FINISH_STEPS: readonly AiStoreFinishStep[] = [\n {\n id: 'payments',\n title: 'Connect payments',\n text: 'Connect Stripe so your store can take payments and pay you out.',\n opens: 'store-settings',\n action: 'Set up payments',\n },\n {\n id: 'products',\n title: 'Review your products and prices',\n text: 'Your first products are listed at suggested prices. Review your prices, photos and stock.',\n opens: 'product',\n action: 'Open products',\n },\n {\n id: 'shipping',\n title: 'Set shipping and tax',\n text: 'Choose your shipping rates and how tax is charged at checkout.',\n opens: 'store-settings',\n action: 'Open store settings',\n },\n {\n id: 'policies',\n title: 'Fill in your policies',\n text: 'Your Shipping & returns, Privacy policy and Terms of sale pages are drafts: replace the parts in [brackets].',\n opens: 'pages',\n action: 'Open pages',\n },\n]\n"],"names":["AI_STORE_PAGES","key","title","slug","link","noindex","seo","description","AI_SITE_STORE_PAGES_LABEL","AI_SITE_STORE_PAGES_NOTE","SHIPPING_WORDS","wordsOf","screen","replace","firstSegment","trim","split","toLowerCase","aiStorePagesOfPlan","screens","pages","filter","record","map","page","holds","find","test","undefined","href","planned","aiStorePagesToWrite","AI_SITE_STORE_LINKS_INPUT","aiStoreFrameLinks","account","footer","label","PATH","aiStoreFrameLinksOf","inputs","raw","links","Array","isArray","flatMap","entry","slice","accountPath","length","ROOT","AI_STORE_MARKDOWN_MAX","AI_STORE_ELEMENTS","wishlist","cart","NAME","contact","facts","words","contactPath","aiStorePolicySections","shippingPath","aiStoreMarkdownChunks","sections","max","chunks","section","last","push","aiStorePageNodes","one","nodes","id","role","container","add","componentId","pluginId","props","sx","nodeId","$id","type","parentId","children","variant","component","mb","signedOutHeading","color","mt","heading","emptyText","showCoupon","shopPath","forEach","content","index","element","ariaLabel","maxWidth","py","xs","md","AI_STORE_FINISH_STEPS","text","opens","action"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-site-store-pages.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 * A STORE'S OWN PAGES (AGL-3676, third round).\n *\n * Zach, 2026-10-10: \"We seem to be missing the account pages for the\n * shop/store AI generation, make sure all they would have to do is setup the\n * payment info and update products to finish up their storefront.\" The\n * Ember & Oak Candle Co start had Home, Shop, Gift sets, About, Shipping &\n * care and Contact, and a product page per product; a shopper had nowhere to\n * sign in, see an order, find what they saved, or read the store's privacy\n * policy and terms.\n *\n * So a paid store start gets the pages every storefront has, written by code\n * from the platform's own elements — never asked of the model:\n *\n * - ACCOUNT at /account: the commerce plugin's Customer account (sign in,\n * create an account, profile, addresses, order history with status and\n * tracking, sign out) and Saved items (the Wishlist the product page's\n * Save fills, kept for a guest in the browser and on the account once\n * they sign in);\n * - CART at /cart: the Cart in full, with its checkout, for a shopper who\n * goes to the address rather than the header's cart drawer;\n * - SHIPPING & RETURNS at /shipping-returns, PRIVACY POLICY at /privacy and\n * TERMS OF SALE at /terms: editable drafts in the Markdown element, every\n * fact only the merchant knows left as a [bracketed placeholder] — never a\n * return window, a price or a promise the brief did not give.\n *\n * The account and the cart are a shopper's own pages, written UNLISTED\n * (noindex, out of the sitemap); the policies are indexable like every other\n * page.\n *\n * An order's own page is the commerce plugin's built-in /order-status, which\n * every order email links with its signed key; the Account page says so.\n *\n * ── Outside the plan's page count ───────────────────────────────────────\n *\n * They are STORE PAGES, not planned pages: the plan's four to eight pages are\n * the content the model designs, and these are added beside them, so they\n * never crowd one out. They still count toward the plan's pages allowance\n * (`screensPerHost`), as every page a site has does; a store is Starter and\n * up, whose allowance holds both.\n *\n * A page the plan already has stands for its store page instead of a second\n * one: a planned \"Shipping & care\" is the store's shipping page, and a page\n * the plan put at /privacy is its privacy policy. The header and footer link\n * that page.\n *\n * Pure: names, paths, words and node trees. Safe for the console to import.\n */\n\n/** A store page, by what it is. Persisted in job outputs (`proposal.storePage`); never renamed. */\nexport type AiStorePageKey = 'account' | 'cart' | 'shipping' | 'privacy' | 'terms'\n\nexport interface AiStorePageDefinition {\n key: AiStorePageKey\n /** The page's name in Pages, and its heading. */\n title: string\n slug: string\n /** What the footer's link to it says. */\n link: string\n seo: { title: string; description: string }\n /**\n * A shopper's own page, not one to find by search: written UNLISTED, the\n * page's Access setting the console offers, so the tenant serves it\n * `noindex` and leaves it out of the sitemap. Only the account and the\n * cart; the policies, like every content and landing page, stay indexable.\n */\n noindex?: true\n}\n\n/** Every store page, in the order the job writes them and the footer links them. */\nexport const AI_STORE_PAGES: readonly AiStorePageDefinition[] = [\n {\n key: 'account',\n title: 'Your account',\n slug: 'account',\n link: 'Your account',\n noindex: true,\n seo: { title: 'Your account', description: 'Sign in to see your orders, saved addresses and saved items.' },\n },\n {\n key: 'cart',\n title: 'Your cart',\n slug: 'cart',\n link: 'Cart',\n noindex: true,\n seo: { title: 'Your cart', description: 'Review your cart and check out.' },\n },\n {\n key: 'shipping',\n title: 'Shipping & returns',\n slug: 'shipping-returns',\n link: 'Shipping & returns',\n seo: { title: 'Shipping & returns', description: 'How we ship your order, and how returns and refunds work.' },\n },\n {\n key: 'privacy',\n title: 'Privacy policy',\n slug: 'privacy',\n link: 'Privacy policy',\n seo: { title: 'Privacy policy', description: 'What we collect when you shop with us, and what we do with it.' },\n },\n {\n key: 'terms',\n title: 'Terms of sale',\n slug: 'terms',\n link: 'Terms of sale',\n seo: { title: 'Terms of sale', description: 'The terms that apply when you buy from our store.' },\n },\n]\n\n/** What the job's row for the store pages reads. */\nexport const AI_SITE_STORE_PAGES_LABEL = 'Adding your account, cart and policy pages'\n\n/** The note the row carries once they are written: what the merchant fills in. */\nexport const AI_SITE_STORE_PAGES_NOTE =\n 'Your shipping, privacy and terms pages are drafts: fill in the parts in [brackets] with your store’s own details.'\n\n/** A planned page whose words say it is the store's shipping page: \"Shipping & care\", \"Delivery & returns\". */\nconst SHIPPING_WORDS = /\\b(shipping|delivery|returns?|refunds?)\\b/i\n\n/** The words of a planned page's address and title, spaced. */\nconst wordsOf = (screen: { slug: string; title: string }) => `${screen.slug.replace(/[-/_]+/g, ' ')} ${screen.title}`\n\nconst firstSegment = (slug: string) => slug.trim().replace(/^\\/+/, '').split('/')[0].toLowerCase()\n\n/** A store page as a start builds or links it. */\nexport interface AiStorePagePlan {\n key: AiStorePageKey\n title: string\n slug: string\n /** Where the header and footer link it. */\n href: string\n /** What the footer's link says. */\n link: string\n /**\n * The planned page that stands for it, by its title, where the plan has\n * one: nothing is written for this key, and its links go to that page.\n */\n planned?: string\n}\n\n/**\n * The store pages a store plan gets: each one written, or — where a planned\n * page already holds its address, or a planned page's words say it is the\n * store's shipping page — that planned page, linked in its place.\n */\nexport function aiStorePagesOfPlan(screens: ReadonlyArray<{ slug: string; title: string; record?: unknown }>): AiStorePagePlan[] {\n const pages = screens.filter((screen) => !screen.record && firstSegment(screen.slug) !== '')\n return AI_STORE_PAGES.map((page) => {\n const holds =\n pages.find((screen) => firstSegment(screen.slug) === page.slug) ??\n (page.key === 'shipping' ? pages.find((screen) => SHIPPING_WORDS.test(wordsOf(screen))) : undefined)\n if (holds) {\n const slug = firstSegment(holds.slug)\n return { key: page.key, title: page.title, slug, href: `/${slug}`, link: holds.title.trim() || page.link, planned: holds.title }\n }\n return { key: page.key, title: page.title, slug: page.slug, href: `/${page.slug}`, link: page.link }\n })\n}\n\n/** The store pages a start writes itself: those no planned page stands for. */\nexport function aiStorePagesToWrite(pages: readonly AiStorePagePlan[]): AiStorePagePlan[] {\n return pages.filter((page) => !page.planned)\n}\n\n/** The unit input a site layout is told the store's links under. */\nexport const AI_SITE_STORE_LINKS_INPUT = 'storeLinks'\n\n/** What a store's header and footer link (AGL-3676): the account beside the cart, and the store's pages under the site's. */\nexport interface AiStoreFrameLinks {\n /** The account page's path, which the header links beside the cart. */\n account: string | null\n /** The footer's store links, in order: the account, then the policies. */\n footer: Array<{ label: string; href: string }>\n}\n\n/** The header's and footer's store links for these store pages. The cart is the header's own button. */\nexport function aiStoreFrameLinks(pages: readonly AiStorePagePlan[]): AiStoreFrameLinks {\n const account = pages.find((page) => page.key === 'account')\n return {\n account: account?.href ?? null,\n footer: pages.filter((page) => page.key !== 'cart').map((page) => ({ label: page.link, href: page.href })),\n }\n}\n\nconst PATH = /^\\/[a-z0-9-]+$/\n\n/** The store links a layout unit's inputs carry; none for any other layout. */\nexport function aiStoreFrameLinksOf(inputs: Readonly<Record<string, unknown>> | null | undefined): AiStoreFrameLinks | null {\n const raw = inputs?.[AI_SITE_STORE_LINKS_INPUT]\n if (!raw || typeof raw !== 'object') return null\n const { account, footer } = raw as Record<string, unknown>\n const links = Array.isArray(footer)\n ? footer.flatMap((entry) => {\n const { label, href } = (entry ?? {}) as Record<string, unknown>\n return typeof label === 'string' && label.trim() && typeof href === 'string' && PATH.test(href)\n ? [{ label: label.trim().slice(0, 40), href }]\n : []\n })\n : []\n const accountPath = typeof account === 'string' && PATH.test(account) ? account : null\n return accountPath || links.length ? { account: accountPath, footer: links.slice(0, 6) } : null\n}\n\n// ── The pages, as node trees ──────────────────────────────────────────────\n\n/** The canvas root every stored page hangs from (`CANVAS_ROOT_ELEMENT_ID`). */\nconst ROOT = '_@_'\n\n/** The longest one Markdown element is given: the element's own text limit. */\nexport const AI_STORE_MARKDOWN_MAX = 2000\n\n/** The commerce plugin's elements a store page places, by the ids it persists (`ai-site-store-pages.spec.ts` holds them). */\nexport const AI_STORE_ELEMENTS = {\n account: 'customer-account',\n wishlist: 'wishlist',\n cart: 'cart',\n} as const\n\n/** The host token that reads the site's own name at render (`AI_SITE_NAME_TOKEN`). */\nconst NAME = '{{host.businessName}}'\n\n/** What a store page's words may link: the site's contact page and its shop, where the plan has them. */\nexport interface AiStorePageFacts {\n /** The contact page's path, or `null` where the plan has none. */\n contactPath: string | null\n /** The shop's path, `/shop` where the plan has none. */\n shopPath: string\n /** The shipping page's path, as the terms link it. */\n shippingPath: string\n}\n\ninterface StoredNode {\n $id: string\n type: 'node'\n componentId: string\n pluginId: string\n parentId: string | null\n nodes: string[]\n props?: Record<string, unknown>\n sx?: Record<string, unknown>\n}\n\n/** How a sentence asks a shopper to get in touch: the contact page, else a reply to their order email. */\nfunction contact(facts: AiStorePageFacts, words = 'contact us'): string {\n return facts.contactPath ? `[${words}](${facts.contactPath})` : `${words} by replying to your order email`\n}\n\n/** The policy's sections, as markdown, each a `##` heading and its paragraphs. */\nexport function aiStorePolicySections(key: 'shipping' | 'privacy' | 'terms', facts: AiStorePageFacts): string[] {\n if (key === 'shipping') {\n return [\n '## Shipping\\n\\nWe ship orders from [where you ship from]. Orders are packed within [number] business days, and you will get an email with tracking as soon as yours is on its way.\\n\\nShipping costs and delivery options are shown at checkout before you pay. [List the countries or regions you ship to, and any free-shipping offer you make.]',\n `## Returns\\n\\nYou can return an item within [number] days of delivery if it is [unused and in its original packaging]. [Name anything that cannot be returned, such as gift cards or personalized items.]\\n\\nTo start a return, open the link in your order email and choose Request a return, or ${contact(facts)}. [Say who pays for return shipping.]`,\n '## Refunds\\n\\nOnce we receive and check your return, we refund it to your original payment method within [number] business days. [Say whether the original shipping cost is refunded.]',\n `## Damaged or wrong items\\n\\nIf your order arrives damaged or is not what you ordered, ${contact(facts)} within [number] days of delivery with your order number and a photo, and we will put it right.`,\n ]\n }\n if (key === 'privacy') {\n return [\n `## Who we are\\n\\n${NAME} runs this online store. This policy explains what we collect when you visit, shop or create an account, and what we do with it. [Add your business’s legal name and postal address.]`,\n '## What we collect\\n\\n- **When you order:** your name, email address, shipping and billing address, your phone number if you give one, and what you bought.\\n- **When you create an account:** your name, email address, saved addresses, order history and saved items.\\n- **When you sign up for emails:** your email address and your choice to hear from us.\\n- **Payments:** card payments are processed by Stripe. We never see or store your full card number.',\n '## How we use it\\n\\nWe use your information to process and deliver your orders, to send order and shipping emails, to answer your questions, and, only if you opt in, to send you news and offers. You can unsubscribe from marketing emails at any time with the link in each one.',\n '## Who we share it with\\n\\nWe share only what is needed with the services that run the store: our website and store platform, our payment processor (Stripe), and the carriers that deliver your order. [List any other services you use, such as an email or analytics tool.] We do not sell your personal information.',\n '## Cookies\\n\\nThis site uses cookies to keep your cart and your sign-in working. [Describe any analytics or advertising cookies you use, and how visitors choose.]',\n `## Your choices\\n\\nYou can ask to see, correct or delete the information we hold about you: ${contact(facts)}. [Say how long you keep order records, and the privacy rights that apply where you sell.]\\n\\nWe post any change to this policy on this page. Last updated: [date].`,\n ]\n }\n return [\n `## About these terms\\n\\nThese terms apply to purchases from ${NAME} through this website. By placing an order you agree to them. [Add your business’s legal name and where it is registered.]`,\n '## Orders\\n\\nYour order is an offer to buy. We accept it when we send your order confirmation email. If an item turns out to be unavailable, we will tell you and refund what you paid for it.',\n '## Prices and payment\\n\\nPrices are shown in [currency] and [include or exclude] [sales tax or VAT]. Shipping and any taxes are shown at checkout before you pay. Payment is taken securely through Stripe when you place your order.',\n `## Shipping, returns and refunds\\n\\nHow we ship, and how returns and refunds work, is set out on our [Shipping & returns](${facts.shippingPath}) page.`,\n '## Products\\n\\nWe describe and photograph our products as accurately as we can. [Note anything that naturally varies, such as handmade pieces, colors or sizes.]',\n `## Liability\\n\\n[State any limit on your liability, as the law where you sell allows.] Nothing in these terms limits your rights under consumer law.\\n\\n## Contact\\n\\nQuestions about an order? ${contact(facts, 'Contact us')}. Last updated: [date].`,\n ]\n}\n\n/** Sections joined into as few Markdown elements as fit the element's limit, in order. */\nexport function aiStoreMarkdownChunks(sections: readonly string[], max = AI_STORE_MARKDOWN_MAX): string[] {\n const chunks: string[] = []\n for (const section of sections) {\n const last = chunks[chunks.length - 1]\n if (last !== undefined && last.length + 2 + section.length <= max) chunks[chunks.length - 1] = `${last}\\n\\n${section}`\n else chunks.push(section.slice(0, max))\n }\n return chunks\n}\n\n/**\n * A store page's node tree, root first, as a page's first version stores it:\n * the page's heading over the platform's elements, in one section the\n * site's layout renders inside.\n */\nexport function aiStorePageNodes(key: AiStorePageKey, facts: AiStorePageFacts): Record<string, StoredNode> {\n const page = AI_STORE_PAGES.find((one) => one.key === key) as AiStorePageDefinition\n const nodes: Record<string, StoredNode> = {}\n const id = (role: string) => `store-${key}-${role}`\n const container = id('container')\n const add = (role: string, componentId: string, pluginId: string, props: Record<string, unknown>, sx?: Record<string, unknown>): string => {\n const nodeId = id(role)\n nodes[nodeId] = { $id: nodeId, type: 'node', componentId, pluginId, parentId: container, nodes: [], props, ...(sx ? { sx } : {}) }\n return nodeId\n }\n const children: string[] = [add('title', 'muiTypography', 'mui', { children: page.title, variant: 'h1', component: 'h1' }, { mb: 4 })]\n if (key === 'account') {\n children.push(\n add('account', AI_STORE_ELEMENTS.account, 'commerce', { signedOutHeading: 'Sign in or create an account' }),\n add(\n 'guest',\n 'muiTypography',\n 'mui',\n { children: 'Checked out as a guest? The link in your order email opens your order’s status and tracking.', variant: 'body2', component: 'p' },\n { color: 'text.secondary', mt: 3 },\n ),\n add(\n 'saved',\n AI_STORE_ELEMENTS.wishlist,\n 'commerce',\n { heading: 'Saved items', emptyText: 'Nothing saved yet. Tap Save on any product to keep it here.' },\n { mt: 8 },\n ),\n )\n } else if (key === 'cart') {\n children.push(\n add('cart', AI_STORE_ELEMENTS.cart, 'commerce', { variant: 'inline', showCoupon: true }),\n add('shop', 'muiButton', 'mui', { children: 'Continue shopping', variant: 'outlined', href: facts.shopPath }, { mt: 4 }),\n )\n } else {\n aiStoreMarkdownChunks(aiStorePolicySections(key, facts)).forEach((content, index) => {\n children.push(add(`text${index + 1}`, 'markdown', 'mui', { content }))\n })\n }\n const section = id('section')\n nodes[ROOT] = { $id: ROOT, type: 'node', componentId: 'div', pluginId: 'mui', parentId: null, nodes: [section] }\n nodes[section] = {\n $id: section,\n type: 'node',\n componentId: 'section',\n pluginId: 'mui',\n parentId: ROOT,\n nodes: [container],\n props: { element: 'section', ariaLabel: page.title },\n }\n nodes[container] = {\n $id: container,\n type: 'node',\n componentId: 'muiContainer',\n pluginId: 'mui',\n parentId: section,\n nodes: children,\n props: { maxWidth: key === 'account' || key === 'cart' ? 'lg' : 'md' },\n sx: { py: { xs: 6, md: 10 } },\n }\n return nodes\n}\n\n// ── Finishing the store ───────────────────────────────────────────────────\n\n/** One thing left before an AI-built store sells, as its done page lists it. */\nexport interface AiStoreFinishStep {\n id: 'payments' | 'products' | 'shipping' | 'policies' | 'links'\n title: string\n text: string\n /**\n * The record route kind whose list the step opens (`plugin-record-routes`),\n * `pages` for the site's Pages, `layouts` for its Layouts, or `layout` for\n * the layout version a build drafted its store's links into (AGL-3676).\n */\n opens: 'store-settings' | 'product' | 'pages' | 'layouts' | 'layout'\n /**\n * The record of `opens` to land on rather than its list: the store's\n * settings are a page each (AGL-3713), and a step opens the one it names.\n */\n record?: string\n action: string\n}\n\n/**\n * What is left once an AI store is built (Zach, 2026-10-10): connect\n * payments, review the products and their prices, set shipping and tax, and\n * fill in the policies' brackets. Everything else — the storefront, the\n * account, cart and policy pages, the header's cart and account — is done.\n */\nexport const AI_STORE_FINISH_STEPS: readonly AiStoreFinishStep[] = [\n {\n id: 'payments',\n title: 'Connect payments',\n text: 'Connect Stripe so your store can take payments and pay you out.',\n opens: 'store-settings',\n record: 'payments',\n action: 'Set up payments',\n },\n {\n id: 'products',\n title: 'Review your products and prices',\n text: 'Your first products are listed at suggested prices. Review your prices, photos and stock.',\n opens: 'product',\n action: 'Open products',\n },\n {\n id: 'shipping',\n title: 'Set shipping and tax',\n text: 'Choose your shipping rates and how tax is charged at checkout.',\n opens: 'store-settings',\n record: 'shipping',\n action: 'Open store settings',\n },\n {\n id: 'policies',\n title: 'Fill in your policies',\n text: 'Your Shipping & returns, Privacy policy and Terms of sale pages are drafts: replace the parts in [brackets].',\n opens: 'pages',\n action: 'Open pages',\n },\n]\n"],"names":["AI_STORE_PAGES","key","title","slug","link","noindex","seo","description","AI_SITE_STORE_PAGES_LABEL","AI_SITE_STORE_PAGES_NOTE","SHIPPING_WORDS","wordsOf","screen","replace","firstSegment","trim","split","toLowerCase","aiStorePagesOfPlan","screens","pages","filter","record","map","page","holds","find","test","undefined","href","planned","aiStorePagesToWrite","AI_SITE_STORE_LINKS_INPUT","aiStoreFrameLinks","account","footer","label","PATH","aiStoreFrameLinksOf","inputs","raw","links","Array","isArray","flatMap","entry","slice","accountPath","length","ROOT","AI_STORE_MARKDOWN_MAX","AI_STORE_ELEMENTS","wishlist","cart","NAME","contact","facts","words","contactPath","aiStorePolicySections","shippingPath","aiStoreMarkdownChunks","sections","max","chunks","section","last","push","aiStorePageNodes","one","nodes","id","role","container","add","componentId","pluginId","props","sx","nodeId","$id","type","parentId","children","variant","component","mb","signedOutHeading","color","mt","heading","emptyText","showCoupon","shopPath","forEach","content","index","element","ariaLabel","maxWidth","py","xs","md","AI_STORE_FINISH_STEPS","text","opens","action"],"mappings":"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"}
@@ -51,12 +51,12 @@ import { AI_CATALOG_PROVIDERS, AI_MODEL_CATALOG } from "./providers/catalog.js";
51
51
  'job.campaign': 'jobCampaignModel',
52
52
  'job.workflow': 'jobWorkflowModel'
53
53
  };
54
- const MODEL_OPTIONS = [
54
+ /** Every current catalog model: a retired one is served by its successor, so it is not offered. */ const MODEL_OPTIONS = [
55
55
  {
56
56
  value: AI_SETTING_PLATFORM,
57
57
  label: 'Platform default'
58
58
  },
59
- ...AI_MODEL_CATALOG.map((entry)=>({
59
+ ...AI_MODEL_CATALOG.filter((entry)=>!entry.successor).map((entry)=>({
60
60
  value: entry.id,
61
61
  label: `${entry.label} (${entry.provider})`
62
62
  }))