@aglyn/plugins-ai 1.0.0-beta.244 → 1.0.0-beta.246

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 (116) hide show
  1. package/package.json +17 -17
  2. package/src/lib/activity/ai-activity-actions.js +3 -1
  3. package/src/lib/activity/ai-activity-actions.js.map +1 -1
  4. package/src/lib/components/ai-job-links.js +7 -0
  5. package/src/lib/components/ai-job-links.js.map +1 -1
  6. package/src/lib/components/ai-product-zones.d.ts +3 -1
  7. package/src/lib/components/ai-product-zones.js.map +1 -1
  8. package/src/lib/components/ai-products-hub-card.component.js +1 -1
  9. package/src/lib/components/ai-products-hub-card.component.js.map +1 -1
  10. package/src/lib/components/ai-products-proposals.component.js +17 -9
  11. package/src/lib/components/ai-products-proposals.component.js.map +1 -1
  12. package/src/lib/components/ai-site-start-card.component.js +6 -4
  13. package/src/lib/components/ai-site-start-card.component.js.map +1 -1
  14. package/src/lib/components/ai-upsell-dialog.component.js +1 -1
  15. package/src/lib/components/ai-upsell-dialog.component.js.map +1 -1
  16. package/src/lib/jobs/ai-build-capabilities.js +162 -2
  17. package/src/lib/jobs/ai-build-capabilities.js.map +1 -1
  18. package/src/lib/jobs/ai-job-build-step.d.ts +7 -0
  19. package/src/lib/jobs/ai-job-build-step.js +85 -16
  20. package/src/lib/jobs/ai-job-build-step.js.map +1 -1
  21. package/src/lib/jobs/ai-job-duplicate-step.d.ts +123 -0
  22. package/src/lib/jobs/ai-job-duplicate-step.js +269 -0
  23. package/src/lib/jobs/ai-job-duplicate-step.js.map +1 -0
  24. package/src/lib/jobs/ai-job-layout-language.js +13 -0
  25. package/src/lib/jobs/ai-job-layout-language.js.map +1 -1
  26. package/src/lib/jobs/ai-job-navigation-step.d.ts +157 -0
  27. package/src/lib/jobs/ai-job-navigation-step.js +439 -0
  28. package/src/lib/jobs/ai-job-navigation-step.js.map +1 -0
  29. package/src/lib/jobs/ai-job-page-language.d.ts +2 -1
  30. package/src/lib/jobs/ai-job-page-language.js +21 -5
  31. package/src/lib/jobs/ai-job-page-language.js.map +1 -1
  32. package/src/lib/jobs/ai-job-page-sections.js +1 -1
  33. package/src/lib/jobs/ai-job-page-sections.js.map +1 -1
  34. package/src/lib/jobs/ai-job-plan-step.d.ts +4 -2
  35. package/src/lib/jobs/ai-job-plan-step.js +16 -7
  36. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  37. package/src/lib/jobs/ai-job-products-step.d.ts +2 -2
  38. package/src/lib/jobs/ai-job-products-step.js +2 -2
  39. package/src/lib/jobs/ai-job-products-step.js.map +1 -1
  40. package/src/lib/jobs/ai-job-settings-step.d.ts +272 -0
  41. package/src/lib/jobs/ai-job-settings-step.js +695 -0
  42. package/src/lib/jobs/ai-job-settings-step.js.map +1 -0
  43. package/src/lib/jobs/ai-job-site-content.d.ts +20 -6
  44. package/src/lib/jobs/ai-job-site-content.js +28 -12
  45. package/src/lib/jobs/ai-job-site-content.js.map +1 -1
  46. package/src/lib/jobs/ai-job-site-datasets.d.ts +2 -0
  47. package/src/lib/jobs/ai-job-site-datasets.js +10 -3
  48. package/src/lib/jobs/ai-job-site-datasets.js.map +1 -1
  49. package/src/lib/jobs/ai-job-site-step.d.ts +3 -1
  50. package/src/lib/jobs/ai-job-site-step.js +70 -10
  51. package/src/lib/jobs/ai-job-site-step.js.map +1 -1
  52. package/src/lib/jobs/ai-site-publish.d.ts +62 -4
  53. package/src/lib/jobs/ai-site-publish.js +203 -56
  54. package/src/lib/jobs/ai-site-publish.js.map +1 -1
  55. package/src/lib/layout-language/ai-layout-compiler.d.ts +7 -4
  56. package/src/lib/layout-language/ai-layout-compiler.js +26 -25
  57. package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
  58. package/src/lib/layout-language/ai-layout-frame-hollows.d.ts +41 -0
  59. package/src/lib/layout-language/ai-layout-frame-hollows.js +330 -0
  60. package/src/lib/layout-language/ai-layout-frame-hollows.js.map +1 -0
  61. package/src/lib/layout-language/ai-layout-frame.d.ts +18 -1
  62. package/src/lib/layout-language/ai-layout-frame.js +83 -13
  63. package/src/lib/layout-language/ai-layout-frame.js.map +1 -1
  64. package/src/lib/layout-language/ai-layout-listings.d.ts +35 -0
  65. package/src/lib/layout-language/ai-layout-listings.js +34 -0
  66. package/src/lib/layout-language/ai-layout-listings.js.map +1 -1
  67. package/src/lib/layout-language/ai-layout-tree.d.ts +2 -0
  68. package/src/lib/layout-language/ai-layout-tree.js +17 -0
  69. package/src/lib/layout-language/ai-layout-tree.js.map +1 -1
  70. package/src/lib/model/ai-build-job.d.ts +23 -0
  71. package/src/lib/model/ai-build-job.js +102 -13
  72. package/src/lib/model/ai-build-job.js.map +1 -1
  73. package/src/lib/model/ai-build-plan.d.ts +25 -0
  74. package/src/lib/model/ai-build-plan.js +45 -1
  75. package/src/lib/model/ai-build-plan.js.map +1 -1
  76. package/src/lib/model/ai-credit-estimate.d.ts +52 -0
  77. package/src/lib/model/ai-credit-estimate.js +51 -0
  78. package/src/lib/model/ai-credit-estimate.js.map +1 -1
  79. package/src/lib/model/ai-job-activity.js +4 -1
  80. package/src/lib/model/ai-job-activity.js.map +1 -1
  81. package/src/lib/model/ai-jobs.types.d.ts +10 -2
  82. package/src/lib/model/ai-jobs.types.js +4 -1
  83. package/src/lib/model/ai-jobs.types.js.map +1 -1
  84. package/src/lib/model/ai-layout-menus.d.ts +104 -0
  85. package/src/lib/model/ai-layout-menus.js +518 -0
  86. package/src/lib/model/ai-layout-menus.js.map +1 -0
  87. package/src/lib/model/ai-products.d.ts +31 -1
  88. package/src/lib/model/ai-products.js +34 -1
  89. package/src/lib/model/ai-products.js.map +1 -1
  90. package/src/lib/model/ai-site-inventory.d.ts +15 -0
  91. package/src/lib/model/ai-site-inventory.js.map +1 -1
  92. package/src/lib/model/ai-site-job.d.ts +10 -0
  93. package/src/lib/model/ai-site-job.js +36 -9
  94. package/src/lib/model/ai-site-job.js.map +1 -1
  95. package/src/lib/model/ai-site-store-pages.js +1 -1
  96. package/src/lib/model/ai-site-store-pages.js.map +1 -1
  97. package/src/lib/runtime/ai-doctrine-validators.d.ts +53 -0
  98. package/src/lib/runtime/ai-doctrine-validators.js +166 -25
  99. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  100. package/src/lib/runtime/ai-doctrine.js +7 -2
  101. package/src/lib/runtime/ai-doctrine.js.map +1 -1
  102. package/src/lib/runtime/ai-palette.generated.js +1 -1
  103. package/src/lib/runtime/ai-palette.generated.js.map +1 -1
  104. package/src/lib/runtime/ai-products-generation.d.ts +4 -2
  105. package/src/lib/runtime/ai-products-generation.js +12 -6
  106. package/src/lib/runtime/ai-products-generation.js.map +1 -1
  107. package/src/lib/runtime/site-inventory.d.ts +12 -0
  108. package/src/lib/runtime/site-inventory.js +29 -1
  109. package/src/lib/runtime/site-inventory.js.map +1 -1
  110. package/src/lib/server.js +12 -0
  111. package/src/lib/server.js.map +1 -1
  112. package/src/lib/testing/stand-in-dataset-index.js +37 -3
  113. package/src/lib/testing/stand-in-dataset-index.js.map +1 -1
  114. package/src/lib/tools/ai-products-tool.d.ts +3 -1
  115. package/src/lib/tools/ai-products-tool.js +15 -5
  116. package/src/lib/tools/ai-products-tool.js.map +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-build-plan.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { AiTool } from '../providers/contract'\nimport type { AiBuildStorePages } from './ai-build-store-pages'\n\n/**\n * The plan a generation job builds from (AGL-2935): what the job's plan step\n * (`jobs/ai-job-plan-step.ts`) returns, before a single node is generated.\n *\n * This module is the plan's MODEL — its shape, its tool schema and its\n * reader — and imports nothing at runtime, so the doctrine validators, the\n * plan step, the job document and the proposal can all read it without\n * importing each other.\n *\n * A plan is REFERENCES, never documents (AGL-2937). It names what the site\n * already has by inventory id, what the job will create by a short name, and\n * which screens it builds from which of those — so the doctrine can be held\n * against the plan while fixing it costs a sentence, not a regenerated page,\n * and the customer reads what the job will do before it spends a credit on\n * doing it.\n *\n * `new:<name>` is how an entry refers to something the plan itself creates;\n * any other reference is an id from the site inventory.\n */\n\nexport const AI_PLAN_NEW_REF_PREFIX = 'new:'\n\n/** Records a plan may reuse, by the inventory list they come from. */\nexport type AiBuildPlanReuseKind =\n | 'component'\n | 'layout'\n | 'template'\n | 'form'\n | 'dataset'\n | 'collection'\n | 'screen'\n | 'theme'\n\nexport const AI_BUILD_PLAN_REUSE_KINDS: readonly AiBuildPlanReuseKind[] = [\n 'component',\n 'layout',\n 'template',\n 'form',\n 'dataset',\n 'collection',\n 'screen',\n 'theme',\n]\n\n/** What a plan may create. A theme change is a draft through the theme door, never a literal. */\nexport type AiBuildPlanCreateKind =\n | 'component'\n | 'layout'\n | 'template'\n | 'form'\n | 'theme-change'\n | 'dataset'\n | 'email'\n\n/**\n * What each creation is called in a sentence, and where a member makes one.\n *\n * Total over the union deliberately: a refusal that cannot name a creation is\n * a refusal a member cannot act on, so widening `AiBuildPlanCreateKind`\n * without saying where the new thing is made does not compile.\n */\nexport const AI_BUILD_PLAN_CREATION_NOUNS: Record<\n AiBuildPlanCreateKind,\n { noun: string; where: string }\n> = {\n component: { noun: 'component', where: 'on the Components page' },\n form: { noun: 'form', where: 'on the Forms page' },\n layout: { noun: 'layout', where: 'on the Layouts page' },\n template: { noun: 'template', where: 'in the Templates library' },\n 'theme-change': { noun: 'theme change', where: 'in the Theme section' },\n dataset: { noun: 'dataset', where: 'on the Datasets page' },\n email: { noun: 'email design', where: 'in Emails → Templates' },\n}\n\nexport const AI_BUILD_PLAN_CREATE_KINDS: readonly AiBuildPlanCreateKind[] = [\n 'component',\n 'layout',\n 'template',\n 'form',\n 'theme-change',\n 'dataset',\n 'email',\n]\n\nexport interface AiBuildPlanReuse {\n kind: AiBuildPlanReuseKind\n /** The inventory id. */\n id: string\n /** What the plan uses it for, in a few words. */\n purpose: string\n}\n\nexport interface AiBuildPlanCreate {\n kind: AiBuildPlanCreateKind\n /** Unique within the plan; referenced as `new:<name>`. */\n name: string\n /** Why nothing the site has will do — shown in the proposal. */\n why: string\n /** The inventory id this starts from as a duplicate, or null. */\n duplicateOf: string | null\n /**\n * A component: its props as `name:type`, an icon as `name:icon`\n * (AGL-3054). A layout: the regions it has, from the closed vocabulary\n * `AI_LAYOUT_REGIONS` holds (AGL-3024) — a layout has no props, and a\n * region named here is what the built layout is held to. A template: the\n * binding tokens its page shows, from the subject's own closed catalog\n * (`AI_TEMPLATE_SUBJECT_DEFINITIONS[subject].tokens`, AGL-3143) — the\n * template's analogue of a layout's regions, and what the built template is\n * held to. A form or a dataset: its field names. A theme change: the\n * palette paths it adds or changes.\n */\n fields: string[]\n /**\n * A form only: the dataset each of its submissions is also written to as a\n * record (AGL-3616, Zach 2026-10-10: a volunteer sign-up writing to a\n * Volunteers dataset) — `new:<name>` of a dataset this plan creates, or a\n * dataset id from the inventory. Said by the plan, never inferred from a\n * section's `uses`. Absent or null on every other creation, and on a form\n * whose submissions only reach the Inbox.\n */\n writesTo?: string | null\n /**\n * The id the draft this creation becomes is written under, where a unit of\n * the job builds it (AGL-3079). Minted when the job keeps the plan, never\n * part of the model's answer.\n */\n id?: string\n}\n\nexport interface AiBuildPlanSection {\n /** A few words: \"hero\", \"services grid\", \"contact form\". */\n name: string\n /** What the section places: inventory ids or `new:<name>` references. */\n uses: string[]\n /** How many repeated items it shows (cards, rows, tiles); 0 when none. */\n items: number\n}\n\n/**\n * A screen that is one dataset's record template (AGL-3475): the data plugin\n * serves it once per record at `/{base}/{slug}`, its copy bound with\n * `{{item.<field>}}`. The plan names the binding and the build drafts the\n * page; the member saves the binding in Page Properties → Record pages, since\n * a binding is a route and a job writes drafts only (rule 13).\n */\nexport interface AiBuildPlanRecord {\n /** The dataset: an inventory id or `new:<name>`. */\n dataset: string\n /** The path its record pages live under, one or more segments: `services`, `services/residential`. */\n base: string\n}\n\nexport interface AiBuildPlanScreen {\n title: string\n slug: string\n /** The layout it renders inside. */\n layout: string | null\n /** The template it applies, or null for a one-off page. */\n template: string | null\n /** The inventory screen it starts from as a duplicate, or null. */\n duplicateOf: string | null\n /** Whether the site navigation gains an entry for it. */\n nav: boolean\n seoTitle: string\n seoDescription: string\n sections: AiBuildPlanSection[]\n /**\n * The dataset this page is the record template of, or null for a page that\n * is one page. A plan kept before record templates carries none, which\n * reads as null.\n */\n record: AiBuildPlanRecord | null\n /**\n * The id the draft screen is written under, where a unit of the job builds\n * it (AGL-3079). Minted when the job keeps the plan, never part of the\n * model's answer.\n */\n id?: string\n}\n\n/** The third-party players a plan may embed: the hosts the Video embed element plays. */\nexport type AiBuildPlanEmbedHost = 'youtube' | 'vimeo'\n\nexport const AI_BUILD_PLAN_EMBED_HOSTS: readonly AiBuildPlanEmbedHost[] = ['youtube', 'vimeo']\n\n/** What each host is called in a sentence. */\nexport const AI_BUILD_PLAN_EMBED_HOST_NAMES: Record<AiBuildPlanEmbedHost, string> = {\n youtube: 'YouTube',\n vimeo: 'Vimeo',\n}\n\n/**\n * A third-party player the brief asks for (rule 16, AGL-3433). It loads its\n * host's code on the page that carries it, so it is planned, where the member\n * reads that cost before confirming, and a build admits only what the\n * confirmed plan lists.\n */\nexport interface AiBuildPlanEmbed {\n host: AiBuildPlanEmbedHost\n /** Where it plays: a screen of the plan by its slug, or `new:<name>` for a component the plan creates. */\n where: string\n /** The brief's own words asking for it, which the plan step finds in the brief. */\n asked: string\n /** The video's link as the brief gives it, or null for the site owner to paste. */\n url: string | null\n}\n\n/** How the items that depend on a failed item behave (AGL-3616). */\nexport type AiBuildItemDegrade = 'omit' | 'fallback'\n\nexport const AI_BUILD_ITEM_DEGRADES: readonly AiBuildItemDegrade[] = ['omit', 'fallback']\n\n/** One argument value of an item: scalars and lists of text only. */\nexport type AiBuildItemArg = string | number | boolean | string[]\n\n/**\n * One thing a `build` plan makes that its creations and screens do not\n * (AGL-3616): an operation another plugin — or this one — registered as an\n * AI capability, with the arguments its schema declares. `reuse`, `create`\n * and `screens` keep their meaning; an item is everything else.\n */\nexport interface AiBuildItem {\n /** Stable within the plan: `i0`, `i1`… in plan order. */\n slot: string\n /** The capability's operation. */\n op: string\n /** Unique within the plan, creations included; referenced as `new:<name>`. */\n name: string\n /** Why the brief needs it, in one sentence. */\n why: string\n /** What it is built after: `new:<name>` of a creation or another item. */\n dependsOn: string[]\n /** How the items that depend on it behave when it fails. */\n degrade: AiBuildItemDegrade\n /** The operation's arguments, in the shape its capability declares. */\n args: Record<string, AiBuildItemArg>\n /** The id its draft is written under, minted when the job keeps the plan; never the model's. */\n id?: string\n /** The most it can cost in credits, from its capability, recorded when the plan is kept. */\n credits?: number\n}\n\nexport interface AiBuildPlan {\n reuse: AiBuildPlanReuse[]\n create: AiBuildPlanCreate[]\n screens: AiBuildPlanScreen[]\n /**\n * The third-party players the brief asks for. Absent is none: a plan kept\n * before the list existed, and every plan whose brief asks for no player.\n */\n embeds?: AiBuildPlanEmbed[]\n /**\n * What a `build` plan makes beyond its creations and pages (AGL-3616).\n * Absent is none: every other kind's plan.\n */\n items?: AiBuildItem[]\n /**\n * The store pages a `build` adds where its plan turns the site into a store\n * (AGL-3676): worked out by code as the plan is kept, never by the model,\n * and built by a unit of their own after every planned one. Absent is none.\n */\n storePages?: AiBuildStorePages\n}\n\n/**\n * How much one plan may hold. A job builds what one proposal can describe\n * and one person can read before confirming; a brief asking for more is\n * several jobs.\n */\nexport const AI_BUILD_PLAN_LIMITS = {\n reuse: 40,\n create: 20,\n screens: 12,\n sections: 16,\n uses: 8,\n fields: 30,\n embeds: 4,\n text: 200,\n seoDescription: 320,\n items: 500,\n /** A build plan's items (AGL-3616). */\n buildItems: 16,\n /** What one item depends on. */\n dependsOn: 8,\n /** The arguments one item carries, as JSON. */\n argsChars: 2_000,\n} as const\n\nconst string = (description: string) => ({ type: 'string', description })\nconst nullableString = (description: string) => ({\n anyOf: [{ type: 'string' }, { type: 'null' }],\n description,\n})\nconst strings = (description: string) => ({\n type: 'array',\n items: { type: 'string' },\n description,\n})\n\n/**\n * The ceiling `parseAiBuildPlan` cuts a field's text at, in words (AGL-3022).\n * The reader repairs an overlong value by cutting it, and a model never told\n * where the cut falls writes a rationale that stops mid-sentence there.\n */\nconst atMost = (limit: number = AI_BUILD_PLAN_LIMITS.text) => `at most ${limit} characters`\n\n/**\n * The plan as a strict tool's input schema. Only the keywords strict\n * structured output accepts: every object closes its properties and lists\n * every one as required, an optional value is a `null` branch, and no\n * length or count bound appears as a keyword — those are enforced by\n * `parseAiBuildPlan`, which every answer passes through anyway.\n *\n * A text ceiling is stated in words instead, on each field the model writes.\n * A reference is copied from the inventory or from a creation's name, so its\n * length is not the model's to choose. A search title and description are\n * held by rule 10 to ceilings far inside the reader's, which the doctrine\n * states, so naming the reader's here would contradict the rule.\n */\nexport const AI_BUILD_PLAN_TOOL: AiTool = {\n name: 'submit_build_plan',\n description:\n 'Submit the build plan: what the site already has that this job reuses, what it creates and why, and the pages it builds from them.',\n strict: true,\n inputSchema: {\n type: 'object',\n additionalProperties: false,\n required: ['reuse', 'create', 'screens'],\n properties: {\n reuse: {\n type: 'array',\n description: 'Records the site already has that this plan uses, by inventory id.',\n items: {\n type: 'object',\n additionalProperties: false,\n required: ['kind', 'id', 'purpose'],\n properties: {\n kind: { type: 'string', enum: [...AI_BUILD_PLAN_REUSE_KINDS] },\n id: string('The id from the site inventory.'),\n purpose: string(`What the plan uses it for, in a few words, ${atMost()}.`),\n },\n },\n },\n create: {\n type: 'array',\n description:\n 'What the plan creates. Creating is the exception: each entry says why nothing listed will do.',\n items: {\n type: 'object',\n additionalProperties: false,\n required: ['kind', 'name', 'why', 'duplicateOf', 'fields'],\n properties: {\n kind: { type: 'string', enum: [...AI_BUILD_PLAN_CREATE_KINDS] },\n name: string(\n `Unique within the plan, ${atMost()}; other entries refer to it as new:<name>.`,\n ),\n why: string(`Why nothing the site already has will do, in one sentence of ${atMost()}.`),\n duplicateOf: nullableString(\n 'The inventory id this starts from as a duplicate, or null.',\n ),\n fields: strings(\n `A component: its props as name:type, an icon as name:icon. A layout: the regions it has, each one of header, nav, sidebar, main, footer — list every region the finished layout has, not only the ones it gains. A form or dataset: its field names. A theme change: the palette paths it adds or changes. Each ${atMost()}.`,\n ),\n },\n },\n },\n screens: {\n type: 'array',\n description: 'The pages the job builds, each a new draft.',\n items: {\n type: 'object',\n additionalProperties: false,\n required: [\n 'title',\n 'slug',\n 'layout',\n 'template',\n 'duplicateOf',\n 'nav',\n 'seoTitle',\n 'seoDescription',\n 'sections',\n ],\n properties: {\n title: string(`The page name, ${atMost()}.`),\n slug: string(\n `The path, ${atMost()}: lowercase words joined by hyphens, e.g. /services/roof-repair.`,\n ),\n layout: nullableString(\n 'The layout it renders inside: an inventory id or new:<name>.',\n ),\n template: nullableString(\n 'The template it applies (inventory id or new:<name>), or null for a one-off page.',\n ),\n duplicateOf: nullableString(\n 'The inventory page it starts from as a duplicate, or null.',\n ),\n nav: {\n type: 'boolean',\n description: 'Whether the site navigation gains an entry for it.',\n },\n seoTitle: string('The search-result title.'),\n seoDescription: string('The search-result description.'),\n sections: {\n type: 'array',\n description: 'The sections, top to bottom.',\n items: {\n type: 'object',\n additionalProperties: false,\n required: ['name', 'uses', 'items'],\n properties: {\n name: string(`A few words, ${atMost()}: hero, services grid, contact form.`),\n uses: strings('What the section places: inventory ids or new:<name>.'),\n items: {\n type: 'integer',\n description: 'How many repeated items it shows; 0 when none.',\n },\n },\n },\n },\n },\n },\n },\n },\n },\n}\n\n/** Words that ask for a video, or a link to one. */\nexport const AI_PLAN_ASKS_FOR_VIDEO = /\\b(?:videos?|films?|clips?|youtube|vimeo|player)\\b|youtu\\.be/i\n\nconst EMBEDS_SCHEMA = {\n type: 'array',\n description:\n 'A YouTube or Vimeo player the brief asks for, on a screen or a created component. Empty unless the brief asks; a library film is a Video.',\n items: {\n type: 'object',\n additionalProperties: false,\n required: ['host', 'where', 'asked', 'url'],\n properties: {\n host: { type: 'string', enum: [...AI_BUILD_PLAN_EMBED_HOSTS] },\n where: string('A screen slug, or new:<name> of a component.'),\n asked: string('The words of the brief that ask for it, copied exactly.'),\n url: nullableString('The video link the brief gives, or null.'),\n },\n },\n}\n\n/**\n * The plan tool with its list of third-party players, offered only to a brief\n * that asks for a video (AGL-3433). Every other plan's request is the plain\n * tool, byte for byte, so the list costs nothing on the plans that cannot use\n * it, and a model answering a brief that names no video has nowhere to plan a\n * player it was never asked for.\n */\nexport const AI_BUILD_PLAN_EMBEDS_TOOL: AiTool = {\n ...AI_BUILD_PLAN_TOOL,\n inputSchema: {\n ...AI_BUILD_PLAN_TOOL.inputSchema,\n required: [...(AI_BUILD_PLAN_TOOL.inputSchema['required'] as string[]), 'embeds'],\n properties: {\n ...(AI_BUILD_PLAN_TOOL.inputSchema['properties'] as Record<string, unknown>),\n embeds: EMBEDS_SCHEMA,\n },\n },\n}\n\nconst RECORD_SCHEMA = {\n anyOf: [\n {\n type: 'object',\n additionalProperties: false,\n required: ['dataset', 'base'],\n properties: {\n dataset: string('The dataset: an inventory id or new:<name>.'),\n base: string('The path its record pages live under, e.g. services for /services/roofing.'),\n },\n },\n { type: 'null' },\n ],\n description:\n \"When this page is one dataset's record template, serving a page per record at /<base>/<record address> with its copy bound as {{item.<field>}}: the dataset and the base. Otherwise null.\",\n}\n\nconst WRITES_TO_SCHEMA = nullableString(\n 'A form only, and only where each submission should also be kept as a record (a volunteer sign-up, an RSVP, an inquiry): the dataset it writes to, as new:<name> of a dataset this plan creates or a dataset id from the inventory. Otherwise null.',\n)\n\n/**\n * A plan tool whose screens each name the dataset they are the record\n * template of, or null, and whose creations each name the dataset a form\n * writes its submissions to, or null (AGL-3616).\n */\nfunction withRecords(tool: AiTool): AiTool {\n const properties = tool.inputSchema['properties'] as Record<string, unknown>\n const screens = properties['screens'] as { items: Record<string, unknown> }\n const create = properties['create'] as { items: Record<string, unknown> }\n return {\n ...tool,\n inputSchema: {\n ...tool.inputSchema,\n properties: {\n ...properties,\n create: {\n ...create,\n items: {\n ...create.items,\n required: [...(create.items['required'] as string[]), 'writesTo'],\n properties: {\n ...(create.items['properties'] as Record<string, unknown>),\n writesTo: WRITES_TO_SCHEMA,\n },\n },\n },\n screens: {\n ...screens,\n items: {\n ...screens.items,\n required: [...(screens.items['required'] as string[]), 'record'],\n properties: {\n ...(screens.items['properties'] as Record<string, unknown>),\n record: RECORD_SCHEMA,\n },\n },\n },\n },\n },\n }\n}\n\n/**\n * The plan tools with a record template on every screen (AGL-3475) and the\n * dataset a form writes to on every creation (AGL-3616), offered only where\n * the job may bind a dataset: a site with a dataset, or a job that may create\n * one. Record pages are a Starter feature, and every other plan's\n * request is the plain tool, byte for byte, so the field costs a Free taste\n * nothing and a model planning a site with no dataset has none to bind.\n */\nexport const AI_BUILD_PLAN_RECORDS_TOOL: AiTool = withRecords(AI_BUILD_PLAN_TOOL)\nexport const AI_BUILD_PLAN_EMBEDS_RECORDS_TOOL: AiTool = withRecords(AI_BUILD_PLAN_EMBEDS_TOOL)\n\nconst ITEMS_SCHEMA = {\n type: 'array',\n description:\n 'What the plan makes beyond its creations and pages, each one operation from the list in the request. Empty when the brief asks for nothing else.',\n items: {\n type: 'object',\n additionalProperties: false,\n required: ['op', 'name', 'why', 'dependsOn', 'degrade', 'args'],\n properties: {\n op: string('The operation, exactly as the request lists it.'),\n name: string(`Unique within the plan, creations included, ${atMost()}; other entries refer to it as new:<name>.`),\n why: string(`Why the brief needs it, in one sentence of ${atMost()}.`),\n dependsOn: strings('What it is built after: new:<name> of a creation or another item. Empty when nothing.'),\n degrade: {\n type: 'string',\n enum: [...AI_BUILD_ITEM_DEGRADES],\n description:\n 'omit: what depends on it is built without it if it fails. fallback: what depends on it uses something the site already has instead.',\n },\n args: string('Its arguments as one JSON object, with exactly the fields the request lists for the operation.'),\n },\n },\n}\n\n/**\n * Any plan tool with a build's items (AGL-3616), offered only to a `build`\n * job. The items' arguments travel as one JSON string whose fields the\n * request lists per operation, so the tool stays byte for byte the same\n * whichever plugins registered operations, and the prompt cache keeps it.\n */\nexport function withBuildItems(tool: AiTool): AiTool {\n return {\n ...tool,\n inputSchema: {\n ...tool.inputSchema,\n required: [...(tool.inputSchema['required'] as string[]), 'items'],\n properties: {\n ...(tool.inputSchema['properties'] as Record<string, unknown>),\n items: ITEMS_SCHEMA,\n },\n },\n }\n}\n\n/** The plan tool a brief is answered with, on a job that may or may not bind a dataset. */\nexport function aiBuildPlanToolFor(\n brief: string,\n options: { records?: boolean; items?: boolean } = {},\n): AiTool {\n const video = AI_PLAN_ASKS_FOR_VIDEO.test(brief)\n const tool = options.records\n ? video\n ? AI_BUILD_PLAN_EMBEDS_RECORDS_TOOL\n : AI_BUILD_PLAN_RECORDS_TOOL\n : video\n ? AI_BUILD_PLAN_EMBEDS_TOOL\n : AI_BUILD_PLAN_TOOL\n return options.items ? withBuildItems(tool) : tool\n}\n\nexport type AiBuildPlanParse =\n | { ok: true; plan: AiBuildPlan; repairs: string[] }\n | { ok: false; error: string }\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\nclass PlanShapeError extends Error {}\n\n/**\n * An item's arguments as the plan keeps them: the model writes one JSON\n * string, a kept plan stores the object. Values are scalars and lists of\n * text; anything else is a shape the item cannot be built from.\n */\nfunction buildItemArgs(value: unknown, path: string): Record<string, AiBuildItemArg> {\n let parsed: unknown = value\n if (value === undefined || value === null || value === '') return {}\n if (typeof value === 'string') {\n if (value.length > AI_BUILD_PLAN_LIMITS.argsChars) {\n throw new PlanShapeError(`${path} is over ${AI_BUILD_PLAN_LIMITS.argsChars} characters`)\n }\n try {\n parsed = JSON.parse(value)\n } catch {\n throw new PlanShapeError(`${path} is not a JSON object`)\n }\n }\n if (!isRecord(parsed)) throw new PlanShapeError(`${path} is not a JSON object`)\n const args: Record<string, AiBuildItemArg> = {}\n for (const [key, raw] of Object.entries(parsed)) {\n if (raw === null || raw === undefined) continue\n if (typeof raw === 'string' || typeof raw === 'boolean' || (typeof raw === 'number' && Number.isFinite(raw))) {\n args[key] = raw\n } else if (Array.isArray(raw) && raw.every((one) => typeof one === 'string')) {\n args[key] = raw as string[]\n } else {\n throw new PlanShapeError(`${path}.${key} is not text, a number, true or false, or a list of text`)\n }\n }\n return args\n}\n\n/**\n * Read a plan out of a tool call, or name what makes it unreadable.\n *\n * Structure is refused — a missing list, an unknown kind, more screens than\n * a job holds, two creations under one name — because a plan that reads\n * differently from what the model meant is worse than a re-ask. Length is\n * repaired: copy past a ceiling is cut and the cut is listed.\n */\n/**\n * A layout's regions, one word each. A plan sometimes writes the whole list as\n * one entry — \"regions: header, nav, main, footer\" — which names regions the\n * platform builds but reads as one unknown word, and the plan was refused for\n * it (job AvVxbFbzQx). A leading label is dropped and a list is split on its\n * commas, slashes, plus signs and \"and\", so each region is checked on its own;\n * a duplicate keeps its first place.\n */\nexport function aiPlanLayoutRegionWords(fields: readonly string[]): string[] {\n const words: string[] = []\n for (const field of fields) {\n const listed = /^\\s*(?:the\\s+)?(?:layout\\s+)?(?:regions?|areas?)\\s*[:=-]\\s*(.+)$/i.exec(field)?.[1] ?? field\n const parts = /[,/+]|\\band\\b/i.test(listed)\n ? listed.split(/\\s*(?:,|\\/|\\+|\\band\\b)\\s*/i)\n : [listed]\n for (const part of parts) {\n const word = part.trim()\n if (word && !words.some((seen) => seen.toLowerCase() === word.toLowerCase())) words.push(word)\n }\n }\n return words\n}\n\nexport function parseAiBuildPlan(input: unknown): AiBuildPlanParse {\n const repairs: string[] = []\n const text = (\n value: unknown,\n path: string,\n limit: number = AI_BUILD_PLAN_LIMITS.text,\n ): string => {\n if (typeof value !== 'string') throw new PlanShapeError(`${path} is not text`)\n const trimmed = value.trim()\n if (trimmed.length > limit) {\n repairs.push(`${path} was over ${limit} characters; truncated`)\n // On the last word break inside the ceiling, not through a word: a\n // rationale is read by the member confirming the plan, and half a word\n // reads as a broken answer rather than a long one (AGL-3022). A run of\n // `limit` characters with no break in it still has to be cut where the\n // ceiling falls.\n const cut = trimmed.slice(0, limit)\n const brk = cut.search(/\\s\\S*$/)\n return (brk > 0 ? cut.slice(0, brk) : cut).trimEnd()\n }\n return trimmed\n }\n const nullable = (value: unknown, path: string): string | null =>\n value === null || value === undefined || value === '' ? null : text(value, path) || null\n const list = (value: unknown, path: string, limit: number): unknown[] => {\n if (!Array.isArray(value)) throw new PlanShapeError(`${path} is not a list`)\n if (value.length > limit) {\n throw new PlanShapeError(`${path} lists ${value.length}; one plan holds at most ${limit}`)\n }\n return value\n }\n const record = (value: unknown, path: string): Record<string, unknown> => {\n if (!isRecord(value)) throw new PlanShapeError(`${path} is not an object`)\n return value\n }\n const oneOf = <T extends string>(value: unknown, allowed: readonly T[], path: string): T => {\n if (typeof value !== 'string' || !(allowed as readonly string[]).includes(value)) {\n throw new PlanShapeError(`${path} is not one of ${allowed.join(', ')}`)\n }\n return value as T\n }\n // Absent or null is a page that is one page. A base is held to the shape a\n // record page's address takes, so the binding the member saves from it is\n // one the save route admits.\n const planRecord = (value: unknown, path: string): AiBuildPlanRecord | null => {\n if (value === null || value === undefined) return null\n const entry = record(value, path)\n const dataset = text(entry['dataset'], `${path}.dataset`)\n if (!dataset) throw new PlanShapeError(`${path}.dataset is empty`)\n const base = aiPlanRecordBase(text(entry['base'], `${path}.base`))\n if (!base) throw new PlanShapeError(`${path}.base is not a path of lowercase words joined by hyphens`)\n return { dataset, base }\n }\n\n try {\n const root = record(input, 'plan')\n const reuse = list(root['reuse'], 'reuse', AI_BUILD_PLAN_LIMITS.reuse).map((raw, index) => {\n const entry = record(raw, `reuse[${index}]`)\n const id = text(entry['id'], `reuse[${index}].id`)\n if (!id) throw new PlanShapeError(`reuse[${index}].id is empty`)\n return {\n kind: oneOf(entry['kind'], AI_BUILD_PLAN_REUSE_KINDS, `reuse[${index}].kind`),\n id,\n purpose: text(entry['purpose'], `reuse[${index}].purpose`),\n }\n })\n const names = new Set<string>()\n const create = list(root['create'], 'create', AI_BUILD_PLAN_LIMITS.create).map(\n (raw, index) => {\n const entry = record(raw, `create[${index}]`)\n const name = text(entry['name'], `create[${index}].name`)\n if (!name) throw new PlanShapeError(`create[${index}].name is empty`)\n const key = name.toLowerCase()\n if (names.has(key)) {\n throw new PlanShapeError(`create[${index}].name \"${name}\" is used twice`)\n }\n names.add(key)\n const kind = oneOf(entry['kind'], AI_BUILD_PLAN_CREATE_KINDS, `create[${index}].kind`)\n const fields = list(entry['fields'] ?? [], `create[${index}].fields`, AI_BUILD_PLAN_LIMITS.fields)\n .map((field, fieldIndex) => text(field, `create[${index}].fields[${fieldIndex}]`))\n .filter(Boolean)\n // Only a form writes its submissions to a dataset (AGL-3616); kept\n // only where it says one, so a plan without one reads as it always did.\n const writesTo = kind === 'form' ? nullable(entry['writesTo'], `create[${index}].writesTo`) : null\n return {\n kind,\n name,\n why: text(entry['why'], `create[${index}].why`),\n duplicateOf: nullable(entry['duplicateOf'], `create[${index}].duplicateOf`),\n fields: kind === 'layout' ? aiPlanLayoutRegionWords(fields) : fields,\n ...(writesTo ? { writesTo } : {}),\n }\n },\n )\n const screens = list(root['screens'], 'screens', AI_BUILD_PLAN_LIMITS.screens).map(\n (raw, index) => {\n const entry = record(raw, `screens[${index}]`)\n if (typeof entry['nav'] !== 'boolean') {\n throw new PlanShapeError(`screens[${index}].nav is not true or false`)\n }\n return {\n title: text(entry['title'], `screens[${index}].title`),\n slug: text(entry['slug'], `screens[${index}].slug`),\n layout: nullable(entry['layout'], `screens[${index}].layout`),\n template: nullable(entry['template'], `screens[${index}].template`),\n duplicateOf: nullable(entry['duplicateOf'], `screens[${index}].duplicateOf`),\n nav: entry['nav'],\n seoTitle: text(entry['seoTitle'], `screens[${index}].seoTitle`),\n seoDescription: text(\n entry['seoDescription'],\n `screens[${index}].seoDescription`,\n AI_BUILD_PLAN_LIMITS.seoDescription,\n ),\n sections: list(\n entry['sections'],\n `screens[${index}].sections`,\n AI_BUILD_PLAN_LIMITS.sections,\n ).map((rawSection, sectionIndex) => {\n const path = `screens[${index}].sections[${sectionIndex}]`\n const section = record(rawSection, path)\n const items = Number(section['items'] ?? 0)\n if (!Number.isInteger(items) || items < 0 || items > AI_BUILD_PLAN_LIMITS.items) {\n throw new PlanShapeError(`${path}.items is not a count`)\n }\n return {\n name: text(section['name'], `${path}.name`),\n uses: list(section['uses'] ?? [], `${path}.uses`, AI_BUILD_PLAN_LIMITS.uses)\n .map((ref, refIndex) => text(ref, `${path}.uses[${refIndex}]`))\n .filter(Boolean),\n items,\n }\n }),\n record: planRecord(entry['record'], `screens[${index}].record`),\n }\n },\n )\n const embeds = list(root['embeds'] ?? [], 'embeds', AI_BUILD_PLAN_LIMITS.embeds).map(\n (raw, index) => {\n const entry = record(raw, `embeds[${index}]`)\n const where = text(entry['where'], `embeds[${index}].where`)\n if (!where) throw new PlanShapeError(`embeds[${index}].where is empty`)\n return {\n host: oneOf(entry['host'], AI_BUILD_PLAN_EMBED_HOSTS, `embeds[${index}].host`),\n where,\n asked: text(entry['asked'], `embeds[${index}].asked`),\n url: nullable(entry['url'], `embeds[${index}].url`),\n }\n },\n )\n const items = list(root['items'] ?? [], 'items', AI_BUILD_PLAN_LIMITS.buildItems).map(\n (raw, index): AiBuildItem => {\n const path = `items[${index}]`\n const entry = record(raw, path)\n const op = text(entry['op'], `${path}.op`)\n if (!op) throw new PlanShapeError(`${path}.op is empty`)\n const name = text(entry['name'], `${path}.name`)\n if (!name) throw new PlanShapeError(`${path}.name is empty`)\n const key = name.toLowerCase()\n if (names.has(key)) throw new PlanShapeError(`${path}.name \"${name}\" is used twice`)\n names.add(key)\n return {\n slot: `i${index}`,\n op,\n name,\n why: text(entry['why'] ?? '', `${path}.why`),\n dependsOn: list(entry['dependsOn'] ?? [], `${path}.dependsOn`, AI_BUILD_PLAN_LIMITS.dependsOn)\n .map((ref, refIndex) => text(ref, `${path}.dependsOn[${refIndex}]`))\n .filter(Boolean),\n degrade:\n entry['degrade'] === undefined || entry['degrade'] === null\n ? 'omit'\n : oneOf(entry['degrade'], AI_BUILD_ITEM_DEGRADES, `${path}.degrade`),\n args: buildItemArgs(entry['args'], `${path}.args`),\n ...(typeof entry['id'] === 'string' && entry['id'] ? { id: entry['id'] } : {}),\n ...(typeof entry['credits'] === 'number' && Number.isFinite(entry['credits'])\n ? { credits: Math.max(0, Math.floor(entry['credits'])) }\n : {}),\n }\n },\n )\n // A plan whose brief asks for no player carries no list, as it did before the list existed;\n // so does a plan with no items.\n return {\n ok: true,\n plan: {\n reuse,\n create,\n screens,\n ...(embeds.length ? { embeds } : {}),\n ...(items.length ? { items } : {}),\n },\n repairs,\n }\n } catch (error) {\n if (error instanceof PlanShapeError) return { ok: false, error: error.message }\n throw error\n }\n}\n\n/**\n * A record template's base as the data plugin's save route stores it\n * (`normalizeRecordPageBase`, which this plugin may not import): one to four\n * lowercase segments of letters, digits and hyphens, with no leading or\n * trailing slash; null for a base that is no such path.\n */\nexport function aiPlanRecordBase(base: string): string | null {\n const segments = base.trim().toLowerCase().split('/').filter(Boolean)\n if (!segments.length || segments.length > 4) return null\n return segments.every(\n (segment) => segment.length <= 60 && /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(segment),\n )\n ? segments.join('/')\n : null\n}\n\n/** Whether a reference names something the plan creates. */\nexport function isAiPlanNewRef(ref: string | null | undefined): ref is string {\n return typeof ref === 'string' && ref.startsWith(AI_PLAN_NEW_REF_PREFIX)\n}\n\n/** The creation a `new:<name>` reference names, case-insensitively. */\nexport function aiPlanCreateFor(\n plan: AiBuildPlan,\n ref: string,\n): AiBuildPlanCreate | undefined {\n if (!isAiPlanNewRef(ref)) return undefined\n const name = ref.slice(AI_PLAN_NEW_REF_PREFIX.length).trim().toLowerCase()\n return plan.create.find((entry) => entry.name.toLowerCase() === name)\n}\n\n/** The operation a build's dataset item is planned as (AGL-3616), owned by the data plugin. */\nexport const AI_PLAN_DATASET_ITEM_OP = 'dataset'\n\n/**\n * The datasets this plan makes that one of its forms writes its submissions\n * to (AGL-3616), by name lowercased: a dataset creation of a site start, or a\n * build's dataset item. Such a dataset starts with no records — its records\n * are the submissions — so a site start designs it from its planned fields\n * with no model, and no page lists it.\n */\nexport function aiPlanFormDatasetNames(plan: Pick<AiBuildPlan, 'create'> & Partial<Pick<AiBuildPlan, 'items'>>): Set<string> {\n const names = new Set<string>()\n for (const entry of plan.create) {\n if (entry.kind !== 'form' || !isAiPlanNewRef(entry.writesTo)) continue\n const name = entry.writesTo.slice(AI_PLAN_NEW_REF_PREFIX.length).trim().toLowerCase()\n const made =\n plan.create.some((one) => one.kind === 'dataset' && one.name.trim().toLowerCase() === name) ||\n (plan.items ?? []).some((one) => one.op === AI_PLAN_DATASET_ITEM_OP && one.name.trim().toLowerCase() === name)\n if (made) names.add(name)\n }\n return names\n}\n\n/** The item a `new:<name>` reference names, case-insensitively (AGL-3616). */\nexport function aiPlanItemFor(\n plan: Pick<AiBuildPlan, 'items'>,\n ref: string,\n): AiBuildItem | undefined {\n if (!isAiPlanNewRef(ref)) return undefined\n const name = ref.slice(AI_PLAN_NEW_REF_PREFIX.length).trim().toLowerCase()\n return (plan.items ?? []).find((entry) => entry.name.toLowerCase() === name)\n}\n\n/** A slug as two spellings of one path compare: lowercase, one leading slash, no trailing one. */\nexport function aiPlanSlugKey(slug: string): string {\n const path = slug.trim().toLowerCase().replace(/^\\/*/, '/').replace(/\\/+$/, '')\n return path || '/'\n}\n\n/**\n * The players the plan lists for one output (AGL-3433): a screen by its slug,\n * or a creation by its name. A build admits these and no other.\n */\nexport function aiPlanEmbedsFor(\n plan: Pick<AiBuildPlan, 'embeds'> | null | undefined,\n target: { slug: string } | { create: string },\n): AiBuildPlanEmbed[] {\n const key =\n 'slug' in target\n ? aiPlanSlugKey(target.slug)\n : `${AI_PLAN_NEW_REF_PREFIX}${target.create.trim().toLowerCase()}`\n return (plan?.embeds ?? []).filter((embed) =>\n 'slug' in target\n ? !isAiPlanNewRef(embed.where) && aiPlanSlugKey(embed.where) === key\n : embed.where.trim().toLowerCase() === key,\n )\n}\n\n/** The host a video link plays from, or null for a link neither host serves. */\nexport function aiEmbedHostOf(url: string): AiBuildPlanEmbedHost | null {\n try {\n const parsed = new URL(url.trim())\n if (parsed.protocol !== 'https:') return null\n const host = parsed.hostname.replace(/^(?:www|m)\\./, '')\n if (host === 'youtu.be' || host === 'youtube.com' || host === 'youtube-nocookie.com') {\n return 'youtube'\n }\n if (host === 'vimeo.com' || host === 'player.vimeo.com') return 'vimeo'\n } catch {\n /* not a link */\n }\n return null\n}\n\n/**\n * One video however its link is spelled — `youtu.be/<id>`, a watch link, an\n * embed or a Shorts link — as `<host>:<id>`, the way the Video embed element\n * reads a link; null for a link it cannot play.\n */\nexport function aiEmbedVideoKey(url: string): string | null {\n const host = aiEmbedHostOf(url)\n if (!host) return null\n const parsed = new URL(url.trim())\n const parts = parsed.pathname.split('/').filter(Boolean)\n const id =\n host === 'vimeo'\n ? parts.find((part) => /^\\d{6,}$/.test(part))\n : parsed.hostname.endsWith('youtu.be')\n ? parts[0]\n : (parsed.searchParams.get('v') ??\n (parts[0] === 'embed' || parts[0] === 'shorts' ? parts[1] : undefined))\n return id && /^[\\w-]{6,20}$/.test(id) ? `${host}:${id}` : null\n}\n\n/**\n * A `new:<name>` reference that names no entry of the plan's create list: a\n * screen's own layout or template, or what one of its sections places, with\n * that section's index.\n */\nexport type AiPlanUndeclaredRef = {\n /** The name the reference gives the creation, as written after `new:`. */\n name: string\n /** Where the plan writes it: `screens[0].template`, `screens[0].sections[4].uses[0]`. */\n path: string\n /** The screen that writes it, by index. */\n screenIndex: number\n} & ({ field: 'layout' | 'template'; sectionIndex: null } | { field: 'uses'; sectionIndex: number })\n\n/**\n * Every `new:<name>` reference the plan's create list does not carry, in plan\n * order: each screen's layout, then its template, then what its sections\n * place. A plan that places a creation it never declares has nothing to build\n * it from, so the plan rules refuse each (AGL-3040), and the page and\n * scaffold doors still name one in a plan confirmed before they did.\n */\nexport function aiPlanUndeclaredRefs(plan: AiBuildPlan): AiPlanUndeclaredRef[] {\n const found: AiPlanUndeclaredRef[] = []\n const undeclared = (ref: string | null): ref is string =>\n isAiPlanNewRef(ref) && !aiPlanCreateFor(plan, ref) && !aiPlanItemFor(plan, ref)\n const nameOf = (ref: string) => ref.slice(AI_PLAN_NEW_REF_PREFIX.length).trim()\n plan.screens.forEach((screen, screenIndex) => {\n for (const field of ['layout', 'template'] as const) {\n const ref = screen[field]\n if (!undeclared(ref)) continue\n found.push({\n name: nameOf(ref),\n path: `screens[${screenIndex}].${field}`,\n screenIndex,\n field,\n sectionIndex: null,\n })\n }\n screen.sections.forEach((section, sectionIndex) => {\n section.uses.forEach((ref, useIndex) => {\n if (!undeclared(ref)) return\n found.push({\n name: nameOf(ref),\n path: `screens[${screenIndex}].sections[${sectionIndex}].uses[${useIndex}]`,\n screenIndex,\n field: 'uses',\n sectionIndex,\n })\n })\n })\n })\n return found\n}\n"],"names":["AI_PLAN_NEW_REF_PREFIX","AI_BUILD_PLAN_REUSE_KINDS","AI_BUILD_PLAN_CREATION_NOUNS","component","noun","where","form","layout","template","dataset","email","AI_BUILD_PLAN_CREATE_KINDS","AI_BUILD_PLAN_EMBED_HOSTS","AI_BUILD_PLAN_EMBED_HOST_NAMES","youtube","vimeo","AI_BUILD_ITEM_DEGRADES","AI_BUILD_PLAN_LIMITS","reuse","create","screens","sections","uses","fields","embeds","text","seoDescription","items","buildItems","dependsOn","argsChars","string","description","type","nullableString","anyOf","strings","atMost","limit","AI_BUILD_PLAN_TOOL","name","strict","inputSchema","additionalProperties","required","properties","kind","enum","id","purpose","why","duplicateOf","title","slug","nav","seoTitle","AI_PLAN_ASKS_FOR_VIDEO","EMBEDS_SCHEMA","host","asked","url","AI_BUILD_PLAN_EMBEDS_TOOL","RECORD_SCHEMA","base","WRITES_TO_SCHEMA","withRecords","tool","writesTo","record","AI_BUILD_PLAN_RECORDS_TOOL","AI_BUILD_PLAN_EMBEDS_RECORDS_TOOL","ITEMS_SCHEMA","op","degrade","args","withBuildItems","aiBuildPlanToolFor","brief","options","video","test","records","isRecord","value","Array","isArray","PlanShapeError","Error","buildItemArgs","path","parsed","undefined","length","JSON","parse","key","raw","Object","entries","Number","isFinite","every","one","aiPlanLayoutRegionWords","words","field","listed","exec","parts","split","part","word","trim","some","seen","toLowerCase","push","parseAiBuildPlan","input","repairs","trimmed","cut","slice","brk","search","trimEnd","nullable","list","oneOf","allowed","includes","join","planRecord","entry","aiPlanRecordBase","root","map","index","names","Set","has","add","fieldIndex","filter","Boolean","rawSection","sectionIndex","section","isInteger","ref","refIndex","slot","credits","Math","max","floor","ok","plan","error","message","segments","segment","isAiPlanNewRef","startsWith","aiPlanCreateFor","find","AI_PLAN_DATASET_ITEM_OP","aiPlanFormDatasetNames","made","aiPlanItemFor","aiPlanSlugKey","replace","aiPlanEmbedsFor","target","embed","aiEmbedHostOf","URL","protocol","hostname","aiEmbedVideoKey","pathname","endsWith","searchParams","get","aiPlanUndeclaredRefs","found","undeclared","nameOf","forEach","screen","screenIndex","useIndex"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-build-plan.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { AiTool } from '../providers/contract'\nimport type { AiBuildStorePages } from './ai-build-store-pages'\n\n/**\n * The plan a generation job builds from (AGL-2935): what the job's plan step\n * (`jobs/ai-job-plan-step.ts`) returns, before a single node is generated.\n *\n * This module is the plan's MODEL — its shape, its tool schema and its\n * reader — and imports nothing at runtime, so the doctrine validators, the\n * plan step, the job document and the proposal can all read it without\n * importing each other.\n *\n * A plan is REFERENCES, never documents (AGL-2937). It names what the site\n * already has by inventory id, what the job will create by a short name, and\n * which screens it builds from which of those — so the doctrine can be held\n * against the plan while fixing it costs a sentence, not a regenerated page,\n * and the customer reads what the job will do before it spends a credit on\n * doing it.\n *\n * `new:<name>` is how an entry refers to something the plan itself creates;\n * any other reference is an id from the site inventory.\n */\n\nexport const AI_PLAN_NEW_REF_PREFIX = 'new:'\n\n/** Records a plan may reuse, by the inventory list they come from. */\nexport type AiBuildPlanReuseKind =\n | 'component'\n | 'layout'\n | 'template'\n | 'form'\n | 'dataset'\n | 'collection'\n | 'screen'\n | 'theme'\n\nexport const AI_BUILD_PLAN_REUSE_KINDS: readonly AiBuildPlanReuseKind[] = [\n 'component',\n 'layout',\n 'template',\n 'form',\n 'dataset',\n 'collection',\n 'screen',\n 'theme',\n]\n\n/** What a plan may create. A theme change is a draft through the theme door, never a literal. */\nexport type AiBuildPlanCreateKind =\n | 'component'\n | 'layout'\n | 'template'\n | 'form'\n | 'theme-change'\n | 'dataset'\n | 'email'\n\n/**\n * What each creation is called in a sentence, and where a member makes one.\n *\n * Total over the union deliberately: a refusal that cannot name a creation is\n * a refusal a member cannot act on, so widening `AiBuildPlanCreateKind`\n * without saying where the new thing is made does not compile.\n */\nexport const AI_BUILD_PLAN_CREATION_NOUNS: Record<\n AiBuildPlanCreateKind,\n { noun: string; where: string }\n> = {\n component: { noun: 'component', where: 'on the Components page' },\n form: { noun: 'form', where: 'on the Forms page' },\n layout: { noun: 'layout', where: 'on the Layouts page' },\n template: { noun: 'template', where: 'in the Templates library' },\n 'theme-change': { noun: 'theme change', where: 'in the Theme section' },\n dataset: { noun: 'dataset', where: 'on the Datasets page' },\n email: { noun: 'email design', where: 'in Emails → Templates' },\n}\n\nexport const AI_BUILD_PLAN_CREATE_KINDS: readonly AiBuildPlanCreateKind[] = [\n 'component',\n 'layout',\n 'template',\n 'form',\n 'theme-change',\n 'dataset',\n 'email',\n]\n\nexport interface AiBuildPlanReuse {\n kind: AiBuildPlanReuseKind\n /** The inventory id. */\n id: string\n /** What the plan uses it for, in a few words. */\n purpose: string\n}\n\nexport interface AiBuildPlanCreate {\n kind: AiBuildPlanCreateKind\n /** Unique within the plan; referenced as `new:<name>`. */\n name: string\n /** Why nothing the site has will do — shown in the proposal. */\n why: string\n /** The inventory id this starts from as a duplicate, or null. */\n duplicateOf: string | null\n /**\n * A component: its props as `name:type`, an icon as `name:icon`\n * (AGL-3054). A layout: the regions it has, from the closed vocabulary\n * `AI_LAYOUT_REGIONS` holds (AGL-3024) — a layout has no props, and a\n * region named here is what the built layout is held to. A template: the\n * binding tokens its page shows, from the subject's own closed catalog\n * (`AI_TEMPLATE_SUBJECT_DEFINITIONS[subject].tokens`, AGL-3143) — the\n * template's analogue of a layout's regions, and what the built template is\n * held to. A form or a dataset: its field names. A theme change: the\n * palette paths it adds or changes.\n */\n fields: string[]\n /**\n * A form only: the dataset each of its submissions is also written to as a\n * record (AGL-3616, Zach 2026-10-10: a volunteer sign-up writing to a\n * Volunteers dataset) — `new:<name>` of a dataset this plan creates, or a\n * dataset id from the inventory. Said by the plan, never inferred from a\n * section's `uses`. Absent or null on every other creation, and on a form\n * whose submissions only reach the Inbox.\n */\n writesTo?: string | null\n /**\n * The id the draft this creation becomes is written under, where a unit of\n * the job builds it (AGL-3079). Minted when the job keeps the plan, never\n * part of the model's answer.\n */\n id?: string\n}\n\nexport interface AiBuildPlanSection {\n /** A few words: \"hero\", \"services grid\", \"contact form\". */\n name: string\n /** What the section places: inventory ids or `new:<name>` references. */\n uses: string[]\n /** How many repeated items it shows (cards, rows, tiles); 0 when none. */\n items: number\n}\n\n/**\n * A screen that is one dataset's record template (AGL-3475): the data plugin\n * serves it once per record at `/{base}/{slug}`, its copy bound with\n * `{{item.<field>}}`. The plan names the binding and the build drafts the\n * page; the member saves the binding in Page Properties → Record pages, since\n * a binding is a route and a job writes drafts only (rule 13).\n */\nexport interface AiBuildPlanRecord {\n /** The dataset: an inventory id or `new:<name>`. */\n dataset: string\n /** The path its record pages live under, one or more segments: `services`, `services/residential`. */\n base: string\n}\n\nexport interface AiBuildPlanScreen {\n title: string\n slug: string\n /** The layout it renders inside. */\n layout: string | null\n /** The template it applies, or null for a one-off page. */\n template: string | null\n /** The inventory screen it starts from as a duplicate, or null. */\n duplicateOf: string | null\n /** Whether the site navigation gains an entry for it. */\n nav: boolean\n seoTitle: string\n seoDescription: string\n sections: AiBuildPlanSection[]\n /**\n * The dataset this page is the record template of, or null for a page that\n * is one page. A plan kept before record templates carries none, which\n * reads as null.\n */\n record: AiBuildPlanRecord | null\n /**\n * The id the draft screen is written under, where a unit of the job builds\n * it (AGL-3079). Minted when the job keeps the plan, never part of the\n * model's answer.\n */\n id?: string\n}\n\n/** The third-party players a plan may embed: the hosts the Video embed element plays. */\nexport type AiBuildPlanEmbedHost = 'youtube' | 'vimeo'\n\nexport const AI_BUILD_PLAN_EMBED_HOSTS: readonly AiBuildPlanEmbedHost[] = ['youtube', 'vimeo']\n\n/** What each host is called in a sentence. */\nexport const AI_BUILD_PLAN_EMBED_HOST_NAMES: Record<AiBuildPlanEmbedHost, string> = {\n youtube: 'YouTube',\n vimeo: 'Vimeo',\n}\n\n/**\n * A third-party player the brief asks for (rule 16, AGL-3433). It loads its\n * host's code on the page that carries it, so it is planned, where the member\n * reads that cost before confirming, and a build admits only what the\n * confirmed plan lists.\n */\nexport interface AiBuildPlanEmbed {\n host: AiBuildPlanEmbedHost\n /** Where it plays: a screen of the plan by its slug, or `new:<name>` for a component the plan creates. */\n where: string\n /** The brief's own words asking for it, which the plan step finds in the brief. */\n asked: string\n /** The video's link as the brief gives it, or null for the site owner to paste. */\n url: string | null\n}\n\n/** How the items that depend on a failed item behave (AGL-3616). */\nexport type AiBuildItemDegrade = 'omit' | 'fallback'\n\nexport const AI_BUILD_ITEM_DEGRADES: readonly AiBuildItemDegrade[] = ['omit', 'fallback']\n\n/** One argument value of an item: scalars and lists of text only. */\nexport type AiBuildItemArg = string | number | boolean | string[]\n\n/**\n * One thing a `build` plan makes that its creations and screens do not\n * (AGL-3616): an operation another plugin — or this one — registered as an\n * AI capability, with the arguments its schema declares. `reuse`, `create`\n * and `screens` keep their meaning; an item is everything else.\n */\nexport interface AiBuildItem {\n /** Stable within the plan: `i0`, `i1`… in plan order. */\n slot: string\n /** The capability's operation. */\n op: string\n /** Unique within the plan, creations included; referenced as `new:<name>`. */\n name: string\n /** Why the brief needs it, in one sentence. */\n why: string\n /** What it is built after: `new:<name>` of a creation or another item. */\n dependsOn: string[]\n /** How the items that depend on it behave when it fails. */\n degrade: AiBuildItemDegrade\n /** The operation's arguments, in the shape its capability declares. */\n args: Record<string, AiBuildItemArg>\n /** The id its draft is written under, minted when the job keeps the plan; never the model's. */\n id?: string\n /** The most it can cost in credits, from its capability, recorded when the plan is kept. */\n credits?: number\n}\n\nexport interface AiBuildPlan {\n reuse: AiBuildPlanReuse[]\n create: AiBuildPlanCreate[]\n screens: AiBuildPlanScreen[]\n /**\n * The third-party players the brief asks for. Absent is none: a plan kept\n * before the list existed, and every plan whose brief asks for no player.\n */\n embeds?: AiBuildPlanEmbed[]\n /**\n * What a `build` plan makes beyond its creations and pages (AGL-3616).\n * Absent is none: every other kind's plan.\n */\n items?: AiBuildItem[]\n /**\n * The store pages a `build` adds where its plan turns the site into a store\n * (AGL-3676): worked out by code as the plan is kept, never by the model,\n * and built by a unit of their own after every planned one. Absent is none.\n */\n storePages?: AiBuildStorePages\n /**\n * The language a SITE plan's brief and copy are written in (AGL-3750), as\n * the Languages card stores a code (`aiPlanLanguage`). Absent is the site's\n * own language: every other kind's plan, a plan that said null, and one\n * whose code was not a language code, which is dropped rather than refused.\n */\n language?: string\n}\n\n/**\n * How much one plan may hold. A job builds what one proposal can describe\n * and one person can read before confirming; a brief asking for more is\n * several jobs.\n */\nexport const AI_BUILD_PLAN_LIMITS = {\n reuse: 40,\n create: 20,\n screens: 12,\n sections: 16,\n uses: 8,\n fields: 30,\n embeds: 4,\n text: 200,\n seoDescription: 320,\n items: 500,\n /** A build plan's items (AGL-3616). */\n buildItems: 16,\n /** What one item depends on. */\n dependsOn: 8,\n /** The arguments one item carries, as JSON. */\n argsChars: 2_000,\n} as const\n\nconst string = (description: string) => ({ type: 'string', description })\nconst nullableString = (description: string) => ({\n anyOf: [{ type: 'string' }, { type: 'null' }],\n description,\n})\nconst strings = (description: string) => ({\n type: 'array',\n items: { type: 'string' },\n description,\n})\n\n/**\n * The ceiling `parseAiBuildPlan` cuts a field's text at, in words (AGL-3022).\n * The reader repairs an overlong value by cutting it, and a model never told\n * where the cut falls writes a rationale that stops mid-sentence there.\n */\nconst atMost = (limit: number = AI_BUILD_PLAN_LIMITS.text) => `at most ${limit} characters`\n\n/**\n * The plan as a strict tool's input schema. Only the keywords strict\n * structured output accepts: every object closes its properties and lists\n * every one as required, an optional value is a `null` branch, and no\n * length or count bound appears as a keyword — those are enforced by\n * `parseAiBuildPlan`, which every answer passes through anyway.\n *\n * A text ceiling is stated in words instead, on each field the model writes.\n * A reference is copied from the inventory or from a creation's name, so its\n * length is not the model's to choose. A search title and description are\n * held by rule 10 to ceilings far inside the reader's, which the doctrine\n * states, so naming the reader's here would contradict the rule.\n */\nexport const AI_BUILD_PLAN_TOOL: AiTool = {\n name: 'submit_build_plan',\n description:\n 'Submit the build plan: what the site already has that this job reuses, what it creates and why, and the pages it builds from them.',\n strict: true,\n inputSchema: {\n type: 'object',\n additionalProperties: false,\n required: ['reuse', 'create', 'screens'],\n properties: {\n reuse: {\n type: 'array',\n description: 'Records the site already has that this plan uses, by inventory id.',\n items: {\n type: 'object',\n additionalProperties: false,\n required: ['kind', 'id', 'purpose'],\n properties: {\n kind: { type: 'string', enum: [...AI_BUILD_PLAN_REUSE_KINDS] },\n id: string('The id from the site inventory.'),\n purpose: string(`What the plan uses it for, in a few words, ${atMost()}.`),\n },\n },\n },\n create: {\n type: 'array',\n description:\n 'What the plan creates. Creating is the exception: each entry says why nothing listed will do.',\n items: {\n type: 'object',\n additionalProperties: false,\n required: ['kind', 'name', 'why', 'duplicateOf', 'fields'],\n properties: {\n kind: { type: 'string', enum: [...AI_BUILD_PLAN_CREATE_KINDS] },\n name: string(\n `Unique within the plan, ${atMost()}; other entries refer to it as new:<name>.`,\n ),\n why: string(`Why nothing the site already has will do, in one sentence of ${atMost()}.`),\n duplicateOf: nullableString(\n 'The inventory id this starts from as a duplicate, or null.',\n ),\n fields: strings(\n `A component: its props as name:type, an icon as name:icon. A layout: the regions it has, each one of header, nav, sidebar, main, footer — list every region the finished layout has, not only the ones it gains. A form or dataset: its field names. A theme change: the palette paths it adds or changes. Each ${atMost()}.`,\n ),\n },\n },\n },\n screens: {\n type: 'array',\n description: 'The pages the job builds, each a new draft.',\n items: {\n type: 'object',\n additionalProperties: false,\n required: [\n 'title',\n 'slug',\n 'layout',\n 'template',\n 'duplicateOf',\n 'nav',\n 'seoTitle',\n 'seoDescription',\n 'sections',\n ],\n properties: {\n title: string(`The page name, ${atMost()}.`),\n slug: string(\n `The path, ${atMost()}: lowercase words joined by hyphens, e.g. /services/roof-repair.`,\n ),\n layout: nullableString(\n 'The layout it renders inside: an inventory id or new:<name>.',\n ),\n template: nullableString(\n 'The template it applies (inventory id or new:<name>), or null for a one-off page.',\n ),\n duplicateOf: nullableString(\n 'The inventory page it starts from as a duplicate, or null.',\n ),\n nav: {\n type: 'boolean',\n description: 'Whether the site navigation gains an entry for it.',\n },\n seoTitle: string('The search-result title.'),\n seoDescription: string('The search-result description.'),\n sections: {\n type: 'array',\n description: 'The sections, top to bottom.',\n items: {\n type: 'object',\n additionalProperties: false,\n required: ['name', 'uses', 'items'],\n properties: {\n name: string(`A few words, ${atMost()}: hero, services grid, contact form.`),\n uses: strings('What the section places: inventory ids or new:<name>.'),\n items: {\n type: 'integer',\n description: 'How many repeated items it shows; 0 when none.',\n },\n },\n },\n },\n },\n },\n },\n },\n },\n}\n\n/** Words that ask for a video, or a link to one. */\nexport const AI_PLAN_ASKS_FOR_VIDEO = /\\b(?:videos?|films?|clips?|youtube|vimeo|player)\\b|youtu\\.be/i\n\nconst EMBEDS_SCHEMA = {\n type: 'array',\n description:\n 'A YouTube or Vimeo player the brief asks for, on a screen or a created component. Empty unless the brief asks; a library film is a Video.',\n items: {\n type: 'object',\n additionalProperties: false,\n required: ['host', 'where', 'asked', 'url'],\n properties: {\n host: { type: 'string', enum: [...AI_BUILD_PLAN_EMBED_HOSTS] },\n where: string('A screen slug, or new:<name> of a component.'),\n asked: string('The words of the brief that ask for it, copied exactly.'),\n url: nullableString('The video link the brief gives, or null.'),\n },\n },\n}\n\n/**\n * The plan tool with its list of third-party players, offered only to a brief\n * that asks for a video (AGL-3433). Every other plan's request is the plain\n * tool, byte for byte, so the list costs nothing on the plans that cannot use\n * it, and a model answering a brief that names no video has nowhere to plan a\n * player it was never asked for.\n */\nexport const AI_BUILD_PLAN_EMBEDS_TOOL: AiTool = {\n ...AI_BUILD_PLAN_TOOL,\n inputSchema: {\n ...AI_BUILD_PLAN_TOOL.inputSchema,\n required: [...(AI_BUILD_PLAN_TOOL.inputSchema['required'] as string[]), 'embeds'],\n properties: {\n ...(AI_BUILD_PLAN_TOOL.inputSchema['properties'] as Record<string, unknown>),\n embeds: EMBEDS_SCHEMA,\n },\n },\n}\n\nconst RECORD_SCHEMA = {\n anyOf: [\n {\n type: 'object',\n additionalProperties: false,\n required: ['dataset', 'base'],\n properties: {\n dataset: string('The dataset: an inventory id or new:<name>.'),\n base: string('The path its record pages live under, e.g. services for /services/roofing.'),\n },\n },\n { type: 'null' },\n ],\n description:\n \"When this page is one dataset's record template, serving a page per record at /<base>/<record address> with its copy bound as {{item.<field>}}: the dataset and the base. Otherwise null.\",\n}\n\nconst WRITES_TO_SCHEMA = nullableString(\n 'A form only, and only where each submission should also be kept as a record (a volunteer sign-up, an RSVP, an inquiry): the dataset it writes to, as new:<name> of a dataset this plan creates or a dataset id from the inventory. Otherwise null.',\n)\n\n/**\n * A plan tool whose screens each name the dataset they are the record\n * template of, or null, and whose creations each name the dataset a form\n * writes its submissions to, or null (AGL-3616).\n */\nfunction withRecords(tool: AiTool): AiTool {\n const properties = tool.inputSchema['properties'] as Record<string, unknown>\n const screens = properties['screens'] as { items: Record<string, unknown> }\n const create = properties['create'] as { items: Record<string, unknown> }\n return {\n ...tool,\n inputSchema: {\n ...tool.inputSchema,\n properties: {\n ...properties,\n create: {\n ...create,\n items: {\n ...create.items,\n required: [...(create.items['required'] as string[]), 'writesTo'],\n properties: {\n ...(create.items['properties'] as Record<string, unknown>),\n writesTo: WRITES_TO_SCHEMA,\n },\n },\n },\n screens: {\n ...screens,\n items: {\n ...screens.items,\n required: [...(screens.items['required'] as string[]), 'record'],\n properties: {\n ...(screens.items['properties'] as Record<string, unknown>),\n record: RECORD_SCHEMA,\n },\n },\n },\n },\n },\n }\n}\n\n/**\n * The plan tools with a record template on every screen (AGL-3475) and the\n * dataset a form writes to on every creation (AGL-3616), offered only where\n * the job may bind a dataset: a site with a dataset, or a job that may create\n * one. Record pages are a Starter feature, and every other plan's\n * request is the plain tool, byte for byte, so the field costs a Free taste\n * nothing and a model planning a site with no dataset has none to bind.\n */\nexport const AI_BUILD_PLAN_RECORDS_TOOL: AiTool = withRecords(AI_BUILD_PLAN_TOOL)\nexport const AI_BUILD_PLAN_EMBEDS_RECORDS_TOOL: AiTool = withRecords(AI_BUILD_PLAN_EMBEDS_TOOL)\n\nconst ITEMS_SCHEMA = {\n type: 'array',\n description:\n 'What the plan makes beyond its creations and pages, each one operation from the list in the request. Empty when the brief asks for nothing else.',\n items: {\n type: 'object',\n additionalProperties: false,\n required: ['op', 'name', 'why', 'dependsOn', 'degrade', 'args'],\n properties: {\n op: string('The operation, exactly as the request lists it.'),\n name: string(`Unique within the plan, creations included, ${atMost()}; other entries refer to it as new:<name>.`),\n why: string(`Why the brief needs it, in one sentence of ${atMost()}.`),\n dependsOn: strings('What it is built after: new:<name> of a creation or another item. Empty when nothing.'),\n degrade: {\n type: 'string',\n enum: [...AI_BUILD_ITEM_DEGRADES],\n description:\n 'omit: what depends on it is built without it if it fails. fallback: what depends on it uses something the site already has instead.',\n },\n args: string('Its arguments as one JSON object, with exactly the fields the request lists for the operation.'),\n },\n },\n}\n\n/**\n * Any plan tool with a build's items (AGL-3616), offered only to a `build`\n * job. The items' arguments travel as one JSON string whose fields the\n * request lists per operation, so the tool stays byte for byte the same\n * whichever plugins registered operations, and the prompt cache keeps it.\n */\nexport function withBuildItems(tool: AiTool): AiTool {\n return {\n ...tool,\n inputSchema: {\n ...tool.inputSchema,\n required: [...(tool.inputSchema['required'] as string[]), 'items'],\n properties: {\n ...(tool.inputSchema['properties'] as Record<string, unknown>),\n items: ITEMS_SCHEMA,\n },\n },\n }\n}\n\n/** The Languages card's own pattern for one language code (`LOCALE_PATTERN`). */\nexport const AI_PLAN_LANGUAGE_PATTERN = /^[a-z]{2}(-[A-Za-z]{2,4})?$/\n\n/**\n * A plan's language as the Languages card would store it (AGL-3750), or\n * `null`: a two-letter language, optionally with a region or a script, in\n * BCP 47 casing — `es`, `pt-BR`, `zh-Hant`; `_` reads as `-`. Anything the\n * card would not accept is no language at all, so a plan carrying one is\n * kept without it.\n */\nexport function aiPlanLanguage(value: unknown): string | null {\n if (typeof value !== 'string') return null\n const match = /^([A-Za-z]{2})(?:-([A-Za-z]{2}|[A-Za-z]{4}))?$/.exec(value.trim().replace(/_/g, '-'))\n if (!match) return null\n const [, language, sub] = match\n const lower = language.toLowerCase()\n const tag = !sub\n ? lower\n : sub.length === 2\n ? `${lower}-${sub.toUpperCase()}`\n : `${lower}-${sub[0].toUpperCase()}${sub.slice(1).toLowerCase()}`\n return AI_PLAN_LANGUAGE_PATTERN.test(tag) ? tag : null\n}\n\nconst LANGUAGE_SCHEMA = nullableString(\n \"The language the brief is written in, which the site's copy is written in too: a code such as en, es, fr or pt-BR. Null when unsure.\",\n)\n\n/**\n * Any plan tool with the site's language (AGL-3750), offered only to a\n * `site` job, whose guided start sets the site's language from it. One\n * schema for every site plan, so the tool is the same bytes across briefs\n * and the prompt cache keeps it; every other kind's tool is unchanged.\n */\nexport function withPlanLanguage(tool: AiTool): AiTool {\n return {\n ...tool,\n inputSchema: {\n ...tool.inputSchema,\n required: [...(tool.inputSchema['required'] as string[]), 'language'],\n properties: {\n ...(tool.inputSchema['properties'] as Record<string, unknown>),\n language: LANGUAGE_SCHEMA,\n },\n },\n }\n}\n\n/** The plan tool a brief is answered with, on a job that may or may not bind a dataset. */\nexport function aiBuildPlanToolFor(\n brief: string,\n options: { records?: boolean; items?: boolean; language?: boolean } = {},\n): AiTool {\n const video = AI_PLAN_ASKS_FOR_VIDEO.test(brief)\n const tool = options.records\n ? video\n ? AI_BUILD_PLAN_EMBEDS_RECORDS_TOOL\n : AI_BUILD_PLAN_RECORDS_TOOL\n : video\n ? AI_BUILD_PLAN_EMBEDS_TOOL\n : AI_BUILD_PLAN_TOOL\n const built = options.items ? withBuildItems(tool) : tool\n return options.language ? withPlanLanguage(built) : built\n}\n\nexport type AiBuildPlanParse =\n | { ok: true; plan: AiBuildPlan; repairs: string[] }\n | { ok: false; error: string }\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\nclass PlanShapeError extends Error {}\n\n/**\n * An item's arguments as the plan keeps them: the model writes one JSON\n * string, a kept plan stores the object. Values are scalars and lists of\n * text; anything else is a shape the item cannot be built from.\n */\nfunction buildItemArgs(value: unknown, path: string): Record<string, AiBuildItemArg> {\n let parsed: unknown = value\n if (value === undefined || value === null || value === '') return {}\n if (typeof value === 'string') {\n if (value.length > AI_BUILD_PLAN_LIMITS.argsChars) {\n throw new PlanShapeError(`${path} is over ${AI_BUILD_PLAN_LIMITS.argsChars} characters`)\n }\n try {\n parsed = JSON.parse(value)\n } catch {\n throw new PlanShapeError(`${path} is not a JSON object`)\n }\n }\n if (!isRecord(parsed)) throw new PlanShapeError(`${path} is not a JSON object`)\n const args: Record<string, AiBuildItemArg> = {}\n for (const [key, raw] of Object.entries(parsed)) {\n if (raw === null || raw === undefined) continue\n if (typeof raw === 'string' || typeof raw === 'boolean' || (typeof raw === 'number' && Number.isFinite(raw))) {\n args[key] = raw\n } else if (Array.isArray(raw) && raw.every((one) => typeof one === 'string')) {\n args[key] = raw as string[]\n } else {\n throw new PlanShapeError(`${path}.${key} is not text, a number, true or false, or a list of text`)\n }\n }\n return args\n}\n\n/**\n * Read a plan out of a tool call, or name what makes it unreadable.\n *\n * Structure is refused — a missing list, an unknown kind, more screens than\n * a job holds, two creations under one name — because a plan that reads\n * differently from what the model meant is worse than a re-ask. Length is\n * repaired: copy past a ceiling is cut and the cut is listed.\n */\n/**\n * A layout's regions, one word each. A plan sometimes writes the whole list as\n * one entry — \"regions: header, nav, main, footer\" — which names regions the\n * platform builds but reads as one unknown word, and the plan was refused for\n * it (job AvVxbFbzQx). A leading label is dropped and a list is split on its\n * commas, slashes, plus signs and \"and\", so each region is checked on its own;\n * a duplicate keeps its first place.\n */\nexport function aiPlanLayoutRegionWords(fields: readonly string[]): string[] {\n const words: string[] = []\n for (const field of fields) {\n const listed = /^\\s*(?:the\\s+)?(?:layout\\s+)?(?:regions?|areas?)\\s*[:=-]\\s*(.+)$/i.exec(field)?.[1] ?? field\n const parts = /[,/+]|\\band\\b/i.test(listed)\n ? listed.split(/\\s*(?:,|\\/|\\+|\\band\\b)\\s*/i)\n : [listed]\n for (const part of parts) {\n const word = part.trim()\n if (word && !words.some((seen) => seen.toLowerCase() === word.toLowerCase())) words.push(word)\n }\n }\n return words\n}\n\nexport function parseAiBuildPlan(input: unknown): AiBuildPlanParse {\n const repairs: string[] = []\n const text = (\n value: unknown,\n path: string,\n limit: number = AI_BUILD_PLAN_LIMITS.text,\n ): string => {\n if (typeof value !== 'string') throw new PlanShapeError(`${path} is not text`)\n const trimmed = value.trim()\n if (trimmed.length > limit) {\n repairs.push(`${path} was over ${limit} characters; truncated`)\n // On the last word break inside the ceiling, not through a word: a\n // rationale is read by the member confirming the plan, and half a word\n // reads as a broken answer rather than a long one (AGL-3022). A run of\n // `limit` characters with no break in it still has to be cut where the\n // ceiling falls.\n const cut = trimmed.slice(0, limit)\n const brk = cut.search(/\\s\\S*$/)\n return (brk > 0 ? cut.slice(0, brk) : cut).trimEnd()\n }\n return trimmed\n }\n const nullable = (value: unknown, path: string): string | null =>\n value === null || value === undefined || value === '' ? null : text(value, path) || null\n const list = (value: unknown, path: string, limit: number): unknown[] => {\n if (!Array.isArray(value)) throw new PlanShapeError(`${path} is not a list`)\n if (value.length > limit) {\n throw new PlanShapeError(`${path} lists ${value.length}; one plan holds at most ${limit}`)\n }\n return value\n }\n const record = (value: unknown, path: string): Record<string, unknown> => {\n if (!isRecord(value)) throw new PlanShapeError(`${path} is not an object`)\n return value\n }\n const oneOf = <T extends string>(value: unknown, allowed: readonly T[], path: string): T => {\n if (typeof value !== 'string' || !(allowed as readonly string[]).includes(value)) {\n throw new PlanShapeError(`${path} is not one of ${allowed.join(', ')}`)\n }\n return value as T\n }\n // Absent or null is a page that is one page. A base is held to the shape a\n // record page's address takes, so the binding the member saves from it is\n // one the save route admits.\n const planRecord = (value: unknown, path: string): AiBuildPlanRecord | null => {\n if (value === null || value === undefined) return null\n const entry = record(value, path)\n const dataset = text(entry['dataset'], `${path}.dataset`)\n if (!dataset) throw new PlanShapeError(`${path}.dataset is empty`)\n const base = aiPlanRecordBase(text(entry['base'], `${path}.base`))\n if (!base) throw new PlanShapeError(`${path}.base is not a path of lowercase words joined by hyphens`)\n return { dataset, base }\n }\n\n try {\n const root = record(input, 'plan')\n const reuse = list(root['reuse'], 'reuse', AI_BUILD_PLAN_LIMITS.reuse).map((raw, index) => {\n const entry = record(raw, `reuse[${index}]`)\n const id = text(entry['id'], `reuse[${index}].id`)\n if (!id) throw new PlanShapeError(`reuse[${index}].id is empty`)\n return {\n kind: oneOf(entry['kind'], AI_BUILD_PLAN_REUSE_KINDS, `reuse[${index}].kind`),\n id,\n purpose: text(entry['purpose'], `reuse[${index}].purpose`),\n }\n })\n const names = new Set<string>()\n const create = list(root['create'], 'create', AI_BUILD_PLAN_LIMITS.create).map(\n (raw, index) => {\n const entry = record(raw, `create[${index}]`)\n const name = text(entry['name'], `create[${index}].name`)\n if (!name) throw new PlanShapeError(`create[${index}].name is empty`)\n const key = name.toLowerCase()\n if (names.has(key)) {\n throw new PlanShapeError(`create[${index}].name \"${name}\" is used twice`)\n }\n names.add(key)\n const kind = oneOf(entry['kind'], AI_BUILD_PLAN_CREATE_KINDS, `create[${index}].kind`)\n const fields = list(entry['fields'] ?? [], `create[${index}].fields`, AI_BUILD_PLAN_LIMITS.fields)\n .map((field, fieldIndex) => text(field, `create[${index}].fields[${fieldIndex}]`))\n .filter(Boolean)\n // Only a form writes its submissions to a dataset (AGL-3616); kept\n // only where it says one, so a plan without one reads as it always did.\n const writesTo = kind === 'form' ? nullable(entry['writesTo'], `create[${index}].writesTo`) : null\n return {\n kind,\n name,\n why: text(entry['why'], `create[${index}].why`),\n duplicateOf: nullable(entry['duplicateOf'], `create[${index}].duplicateOf`),\n fields: kind === 'layout' ? aiPlanLayoutRegionWords(fields) : fields,\n ...(writesTo ? { writesTo } : {}),\n }\n },\n )\n const screens = list(root['screens'], 'screens', AI_BUILD_PLAN_LIMITS.screens).map(\n (raw, index) => {\n const entry = record(raw, `screens[${index}]`)\n if (typeof entry['nav'] !== 'boolean') {\n throw new PlanShapeError(`screens[${index}].nav is not true or false`)\n }\n return {\n title: text(entry['title'], `screens[${index}].title`),\n slug: text(entry['slug'], `screens[${index}].slug`),\n layout: nullable(entry['layout'], `screens[${index}].layout`),\n template: nullable(entry['template'], `screens[${index}].template`),\n duplicateOf: nullable(entry['duplicateOf'], `screens[${index}].duplicateOf`),\n nav: entry['nav'],\n seoTitle: text(entry['seoTitle'], `screens[${index}].seoTitle`),\n seoDescription: text(\n entry['seoDescription'],\n `screens[${index}].seoDescription`,\n AI_BUILD_PLAN_LIMITS.seoDescription,\n ),\n sections: list(\n entry['sections'],\n `screens[${index}].sections`,\n AI_BUILD_PLAN_LIMITS.sections,\n ).map((rawSection, sectionIndex) => {\n const path = `screens[${index}].sections[${sectionIndex}]`\n const section = record(rawSection, path)\n const items = Number(section['items'] ?? 0)\n if (!Number.isInteger(items) || items < 0 || items > AI_BUILD_PLAN_LIMITS.items) {\n throw new PlanShapeError(`${path}.items is not a count`)\n }\n return {\n name: text(section['name'], `${path}.name`),\n uses: list(section['uses'] ?? [], `${path}.uses`, AI_BUILD_PLAN_LIMITS.uses)\n .map((ref, refIndex) => text(ref, `${path}.uses[${refIndex}]`))\n .filter(Boolean),\n items,\n }\n }),\n record: planRecord(entry['record'], `screens[${index}].record`),\n }\n },\n )\n const embeds = list(root['embeds'] ?? [], 'embeds', AI_BUILD_PLAN_LIMITS.embeds).map(\n (raw, index) => {\n const entry = record(raw, `embeds[${index}]`)\n const where = text(entry['where'], `embeds[${index}].where`)\n if (!where) throw new PlanShapeError(`embeds[${index}].where is empty`)\n return {\n host: oneOf(entry['host'], AI_BUILD_PLAN_EMBED_HOSTS, `embeds[${index}].host`),\n where,\n asked: text(entry['asked'], `embeds[${index}].asked`),\n url: nullable(entry['url'], `embeds[${index}].url`),\n }\n },\n )\n const items = list(root['items'] ?? [], 'items', AI_BUILD_PLAN_LIMITS.buildItems).map(\n (raw, index): AiBuildItem => {\n const path = `items[${index}]`\n const entry = record(raw, path)\n const op = text(entry['op'], `${path}.op`)\n if (!op) throw new PlanShapeError(`${path}.op is empty`)\n const name = text(entry['name'], `${path}.name`)\n if (!name) throw new PlanShapeError(`${path}.name is empty`)\n const key = name.toLowerCase()\n if (names.has(key)) throw new PlanShapeError(`${path}.name \"${name}\" is used twice`)\n names.add(key)\n return {\n slot: `i${index}`,\n op,\n name,\n why: text(entry['why'] ?? '', `${path}.why`),\n dependsOn: list(entry['dependsOn'] ?? [], `${path}.dependsOn`, AI_BUILD_PLAN_LIMITS.dependsOn)\n .map((ref, refIndex) => text(ref, `${path}.dependsOn[${refIndex}]`))\n .filter(Boolean),\n degrade:\n entry['degrade'] === undefined || entry['degrade'] === null\n ? 'omit'\n : oneOf(entry['degrade'], AI_BUILD_ITEM_DEGRADES, `${path}.degrade`),\n args: buildItemArgs(entry['args'], `${path}.args`),\n ...(typeof entry['id'] === 'string' && entry['id'] ? { id: entry['id'] } : {}),\n ...(typeof entry['credits'] === 'number' && Number.isFinite(entry['credits'])\n ? { credits: Math.max(0, Math.floor(entry['credits'])) }\n : {}),\n }\n },\n )\n // A language that is no code is dropped, never refused (AGL-3750): the\n // site keeps the language it has.\n const language = aiPlanLanguage(root['language'])\n if (typeof root['language'] === 'string' && root['language'].trim() && !language) {\n repairs.push(`language \"${root['language'].trim().slice(0, 40)}\" is not a language code; ignored`)\n }\n // A plan whose brief asks for no player carries no list, as it did before the list existed;\n // so does a plan with no items.\n return {\n ok: true,\n plan: {\n reuse,\n create,\n screens,\n ...(embeds.length ? { embeds } : {}),\n ...(items.length ? { items } : {}),\n ...(language ? { language } : {}),\n },\n repairs,\n }\n } catch (error) {\n if (error instanceof PlanShapeError) return { ok: false, error: error.message }\n throw error\n }\n}\n\n/**\n * A record template's base as the data plugin's save route stores it\n * (`normalizeRecordPageBase`, which this plugin may not import): one to four\n * lowercase segments of letters, digits and hyphens, with no leading or\n * trailing slash; null for a base that is no such path.\n */\nexport function aiPlanRecordBase(base: string): string | null {\n const segments = base.trim().toLowerCase().split('/').filter(Boolean)\n if (!segments.length || segments.length > 4) return null\n return segments.every(\n (segment) => segment.length <= 60 && /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(segment),\n )\n ? segments.join('/')\n : null\n}\n\n/** Whether a reference names something the plan creates. */\nexport function isAiPlanNewRef(ref: string | null | undefined): ref is string {\n return typeof ref === 'string' && ref.startsWith(AI_PLAN_NEW_REF_PREFIX)\n}\n\n/** The creation a `new:<name>` reference names, case-insensitively. */\nexport function aiPlanCreateFor(\n plan: AiBuildPlan,\n ref: string,\n): AiBuildPlanCreate | undefined {\n if (!isAiPlanNewRef(ref)) return undefined\n const name = ref.slice(AI_PLAN_NEW_REF_PREFIX.length).trim().toLowerCase()\n return plan.create.find((entry) => entry.name.toLowerCase() === name)\n}\n\n/** The operation a build's dataset item is planned as (AGL-3616), owned by the data plugin. */\nexport const AI_PLAN_DATASET_ITEM_OP = 'dataset'\n\n/**\n * The datasets this plan makes that one of its forms writes its submissions\n * to (AGL-3616), by name lowercased: a dataset creation of a site start, or a\n * build's dataset item. Such a dataset starts with no records — its records\n * are the submissions — so a site start designs it from its planned fields\n * with no model, and no page lists it.\n */\nexport function aiPlanFormDatasetNames(plan: Pick<AiBuildPlan, 'create'> & Partial<Pick<AiBuildPlan, 'items'>>): Set<string> {\n const names = new Set<string>()\n for (const entry of plan.create) {\n if (entry.kind !== 'form' || !isAiPlanNewRef(entry.writesTo)) continue\n const name = entry.writesTo.slice(AI_PLAN_NEW_REF_PREFIX.length).trim().toLowerCase()\n const made =\n plan.create.some((one) => one.kind === 'dataset' && one.name.trim().toLowerCase() === name) ||\n (plan.items ?? []).some((one) => one.op === AI_PLAN_DATASET_ITEM_OP && one.name.trim().toLowerCase() === name)\n if (made) names.add(name)\n }\n return names\n}\n\n/** The item a `new:<name>` reference names, case-insensitively (AGL-3616). */\nexport function aiPlanItemFor(\n plan: Pick<AiBuildPlan, 'items'>,\n ref: string,\n): AiBuildItem | undefined {\n if (!isAiPlanNewRef(ref)) return undefined\n const name = ref.slice(AI_PLAN_NEW_REF_PREFIX.length).trim().toLowerCase()\n return (plan.items ?? []).find((entry) => entry.name.toLowerCase() === name)\n}\n\n/** A slug as two spellings of one path compare: lowercase, one leading slash, no trailing one. */\nexport function aiPlanSlugKey(slug: string): string {\n const path = slug.trim().toLowerCase().replace(/^\\/*/, '/').replace(/\\/+$/, '')\n return path || '/'\n}\n\n/**\n * The players the plan lists for one output (AGL-3433): a screen by its slug,\n * or a creation by its name. A build admits these and no other.\n */\nexport function aiPlanEmbedsFor(\n plan: Pick<AiBuildPlan, 'embeds'> | null | undefined,\n target: { slug: string } | { create: string },\n): AiBuildPlanEmbed[] {\n const key =\n 'slug' in target\n ? aiPlanSlugKey(target.slug)\n : `${AI_PLAN_NEW_REF_PREFIX}${target.create.trim().toLowerCase()}`\n return (plan?.embeds ?? []).filter((embed) =>\n 'slug' in target\n ? !isAiPlanNewRef(embed.where) && aiPlanSlugKey(embed.where) === key\n : embed.where.trim().toLowerCase() === key,\n )\n}\n\n/** The host a video link plays from, or null for a link neither host serves. */\nexport function aiEmbedHostOf(url: string): AiBuildPlanEmbedHost | null {\n try {\n const parsed = new URL(url.trim())\n if (parsed.protocol !== 'https:') return null\n const host = parsed.hostname.replace(/^(?:www|m)\\./, '')\n if (host === 'youtu.be' || host === 'youtube.com' || host === 'youtube-nocookie.com') {\n return 'youtube'\n }\n if (host === 'vimeo.com' || host === 'player.vimeo.com') return 'vimeo'\n } catch {\n /* not a link */\n }\n return null\n}\n\n/**\n * One video however its link is spelled — `youtu.be/<id>`, a watch link, an\n * embed or a Shorts link — as `<host>:<id>`, the way the Video embed element\n * reads a link; null for a link it cannot play.\n */\nexport function aiEmbedVideoKey(url: string): string | null {\n const host = aiEmbedHostOf(url)\n if (!host) return null\n const parsed = new URL(url.trim())\n const parts = parsed.pathname.split('/').filter(Boolean)\n const id =\n host === 'vimeo'\n ? parts.find((part) => /^\\d{6,}$/.test(part))\n : parsed.hostname.endsWith('youtu.be')\n ? parts[0]\n : (parsed.searchParams.get('v') ??\n (parts[0] === 'embed' || parts[0] === 'shorts' ? parts[1] : undefined))\n return id && /^[\\w-]{6,20}$/.test(id) ? `${host}:${id}` : null\n}\n\n/**\n * A `new:<name>` reference that names no entry of the plan's create list: a\n * screen's own layout or template, or what one of its sections places, with\n * that section's index.\n */\nexport type AiPlanUndeclaredRef = {\n /** The name the reference gives the creation, as written after `new:`. */\n name: string\n /** Where the plan writes it: `screens[0].template`, `screens[0].sections[4].uses[0]`. */\n path: string\n /** The screen that writes it, by index. */\n screenIndex: number\n} & ({ field: 'layout' | 'template'; sectionIndex: null } | { field: 'uses'; sectionIndex: number })\n\n/**\n * Every `new:<name>` reference the plan's create list does not carry, in plan\n * order: each screen's layout, then its template, then what its sections\n * place. A plan that places a creation it never declares has nothing to build\n * it from, so the plan rules refuse each (AGL-3040), and the page and\n * scaffold doors still name one in a plan confirmed before they did.\n */\nexport function aiPlanUndeclaredRefs(plan: AiBuildPlan): AiPlanUndeclaredRef[] {\n const found: AiPlanUndeclaredRef[] = []\n const undeclared = (ref: string | null): ref is string =>\n isAiPlanNewRef(ref) && !aiPlanCreateFor(plan, ref) && !aiPlanItemFor(plan, ref)\n const nameOf = (ref: string) => ref.slice(AI_PLAN_NEW_REF_PREFIX.length).trim()\n plan.screens.forEach((screen, screenIndex) => {\n for (const field of ['layout', 'template'] as const) {\n const ref = screen[field]\n if (!undeclared(ref)) continue\n found.push({\n name: nameOf(ref),\n path: `screens[${screenIndex}].${field}`,\n screenIndex,\n field,\n sectionIndex: null,\n })\n }\n screen.sections.forEach((section, sectionIndex) => {\n section.uses.forEach((ref, useIndex) => {\n if (!undeclared(ref)) return\n found.push({\n name: nameOf(ref),\n path: `screens[${screenIndex}].sections[${sectionIndex}].uses[${useIndex}]`,\n screenIndex,\n field: 'uses',\n sectionIndex,\n })\n })\n })\n })\n return found\n}\n"],"names":["AI_PLAN_NEW_REF_PREFIX","AI_BUILD_PLAN_REUSE_KINDS","AI_BUILD_PLAN_CREATION_NOUNS","component","noun","where","form","layout","template","dataset","email","AI_BUILD_PLAN_CREATE_KINDS","AI_BUILD_PLAN_EMBED_HOSTS","AI_BUILD_PLAN_EMBED_HOST_NAMES","youtube","vimeo","AI_BUILD_ITEM_DEGRADES","AI_BUILD_PLAN_LIMITS","reuse","create","screens","sections","uses","fields","embeds","text","seoDescription","items","buildItems","dependsOn","argsChars","string","description","type","nullableString","anyOf","strings","atMost","limit","AI_BUILD_PLAN_TOOL","name","strict","inputSchema","additionalProperties","required","properties","kind","enum","id","purpose","why","duplicateOf","title","slug","nav","seoTitle","AI_PLAN_ASKS_FOR_VIDEO","EMBEDS_SCHEMA","host","asked","url","AI_BUILD_PLAN_EMBEDS_TOOL","RECORD_SCHEMA","base","WRITES_TO_SCHEMA","withRecords","tool","writesTo","record","AI_BUILD_PLAN_RECORDS_TOOL","AI_BUILD_PLAN_EMBEDS_RECORDS_TOOL","ITEMS_SCHEMA","op","degrade","args","withBuildItems","AI_PLAN_LANGUAGE_PATTERN","aiPlanLanguage","value","match","exec","trim","replace","language","sub","lower","toLowerCase","tag","length","toUpperCase","slice","test","LANGUAGE_SCHEMA","withPlanLanguage","aiBuildPlanToolFor","brief","options","video","records","built","isRecord","Array","isArray","PlanShapeError","Error","buildItemArgs","path","parsed","undefined","JSON","parse","key","raw","Object","entries","Number","isFinite","every","one","aiPlanLayoutRegionWords","words","field","listed","parts","split","part","word","some","seen","push","parseAiBuildPlan","input","repairs","trimmed","cut","brk","search","trimEnd","nullable","list","oneOf","allowed","includes","join","planRecord","entry","aiPlanRecordBase","root","map","index","names","Set","has","add","fieldIndex","filter","Boolean","rawSection","sectionIndex","section","isInteger","ref","refIndex","slot","credits","Math","max","floor","ok","plan","error","message","segments","segment","isAiPlanNewRef","startsWith","aiPlanCreateFor","find","AI_PLAN_DATASET_ITEM_OP","aiPlanFormDatasetNames","made","aiPlanItemFor","aiPlanSlugKey","aiPlanEmbedsFor","target","embed","aiEmbedHostOf","URL","protocol","hostname","aiEmbedVideoKey","pathname","endsWith","searchParams","get","aiPlanUndeclaredRefs","found","undeclared","nameOf","forEach","screen","screenIndex","useIndex"],"mappings":"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"}
@@ -79,6 +79,58 @@ export declare const AI_MEASURED_UNIT_CREDITS: {
79
79
  readonly p90: 70;
80
80
  };
81
81
  };
82
+ /**
83
+ * MEASURED IN PRODUCTION, credits per unit of a PAID site start, whose plan
84
+ * and parts run on the paid routing (Sonnet) rather than a Free start's
85
+ * (Haiku) the table above is mostly made of: every `done` paid site job
86
+ * since 2026-10-01, read 2026-10-12 (6 jobs; products from the 4 stores; AGL-3739).
87
+ *
88
+ * plan step 81 / 86 (a Free start's plan: 20 / 42)
89
+ * page item 29 / 49 (about `AI_MEASURED_PAGE_SECTIONS` sections)
90
+ * form 32 / 39
91
+ * layout 29 / 29
92
+ * theme 4 / 4
93
+ * email 49 / 51
94
+ * products 64 / 66 (a store's first products, one answer)
95
+ *
96
+ * A 5-page store with its welcome email was quoted about 277 off the table
97
+ * above: its plan at a Free start's 21 where paid plans run about 81, and its
98
+ * first products not counted at all where they run about 64. Those stores
99
+ * came to 413-448; this table quotes one at about 404.
100
+ *
101
+ * Re-measure as the table above says, keeping only jobs whose plan step's
102
+ * `tokens.model` is the paid routing's.
103
+ */
104
+ export declare const AI_MEASURED_PAID_SITE_CREDITS: {
105
+ readonly plan: {
106
+ readonly median: 81;
107
+ readonly p90: 86;
108
+ };
109
+ readonly page: {
110
+ readonly median: 29;
111
+ readonly p90: 49;
112
+ };
113
+ readonly form: {
114
+ readonly median: 32;
115
+ readonly p90: 39;
116
+ };
117
+ readonly layout: {
118
+ readonly median: 29;
119
+ readonly p90: 29;
120
+ };
121
+ readonly theme: {
122
+ readonly median: 4;
123
+ readonly p90: 4;
124
+ };
125
+ readonly email: {
126
+ readonly median: 49;
127
+ readonly p90: 51;
128
+ };
129
+ readonly products: {
130
+ readonly median: 64;
131
+ readonly p90: 66;
132
+ };
133
+ };
82
134
  /** When the table above was read, and from when its jobs ran. */
83
135
  export declare const AI_MEASURED_AT: {
84
136
  readonly read: "2026-10-09";
@@ -72,6 +72,57 @@
72
72
  p90: 70
73
73
  }
74
74
  };
75
+ /**
76
+ * MEASURED IN PRODUCTION, credits per unit of a PAID site start, whose plan
77
+ * and parts run on the paid routing (Sonnet) rather than a Free start's
78
+ * (Haiku) the table above is mostly made of: every `done` paid site job
79
+ * since 2026-10-01, read 2026-10-12 (6 jobs; products from the 4 stores; AGL-3739).
80
+ *
81
+ * plan step 81 / 86 (a Free start's plan: 20 / 42)
82
+ * page item 29 / 49 (about `AI_MEASURED_PAGE_SECTIONS` sections)
83
+ * form 32 / 39
84
+ * layout 29 / 29
85
+ * theme 4 / 4
86
+ * email 49 / 51
87
+ * products 64 / 66 (a store's first products, one answer)
88
+ *
89
+ * A 5-page store with its welcome email was quoted about 277 off the table
90
+ * above: its plan at a Free start's 21 where paid plans run about 81, and its
91
+ * first products not counted at all where they run about 64. Those stores
92
+ * came to 413-448; this table quotes one at about 404.
93
+ *
94
+ * Re-measure as the table above says, keeping only jobs whose plan step's
95
+ * `tokens.model` is the paid routing's.
96
+ */ export const AI_MEASURED_PAID_SITE_CREDITS = {
97
+ plan: {
98
+ median: 81,
99
+ p90: 86
100
+ },
101
+ page: {
102
+ median: 29,
103
+ p90: 49
104
+ },
105
+ form: {
106
+ median: 32,
107
+ p90: 39
108
+ },
109
+ layout: {
110
+ median: 29,
111
+ p90: 29
112
+ },
113
+ theme: {
114
+ median: 4,
115
+ p90: 4
116
+ },
117
+ email: {
118
+ median: 49,
119
+ p90: 51
120
+ },
121
+ products: {
122
+ median: 64,
123
+ p90: 66
124
+ }
125
+ };
75
126
  /** When the table above was read, and from when its jobs ran. */ export const AI_MEASURED_AT = {
76
127
  read: '2026-10-09',
77
128
  since: '2026-10-01'
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-credit-estimate.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 * WHAT A JOB REALLY COSTS, NEXT TO WHAT IT CAN COST (AGL-3722).\n *\n * Every job's cost used to be quoted at its CEILING: each pass at the\n * `AI_SITE_PASS_CREDITS` (50) a pass is reserved against. A Free Assist\n * build of two pages, a quote form and a layout was quoted 600 credits and\n * refused with 164 left, when builds like it measure about a fifth of that.\n *\n * So a job is now quoted as a RANGE: what it is likely to cost (the sum of\n * the measured medians of what it builds), what it costs nine times in ten\n * (the measured p90s), and its ceiling (the old figure, unchanged). The\n * meter still charges actual use, and the reservation each pass takes is\n * still the ceiling's 50 — only the words and the Free admission move:\n * a Free job is admitted outright when its p90 fits what is left, and asked\n * to be confirmed when it does not.\n *\n * This module is pure: the console, the doors and the specs read one table.\n */\n\n/** One measured cost: the median and the 90th percentile, in credits. */\nexport interface AiMeasuredCredits {\n median: number\n p90: number\n}\n\n/**\n * MEASURED IN PRODUCTION, credits per unit, over every `done` AI job since\n * 2026-10-01 (read 2026-10-09; a whole Free site job came to 148–160):\n *\n * page item 30 / 40 (a page of about `AI_MEASURED_PAGE_SECTIONS` sections, its listing included)\n * form 29 / 31\n * layout 23 / 24 (header and footer, one answer)\n * theme 4 / 8 (a site start's look)\n * plan step 21 / 70\n *\n * HOW TO RE-MEASURE: a read-only collection-group read of `aiJobs` in\n * production where `status == 'done'` and `createdAt` is in the window; group\n * each ledger row's (`items[]`) `creditsSpent` by its `op` (the site start's\n * look is the `theme` row), take the plan step's `steps[].creditsSpent` for\n * `plan`, and each job's `creditsSpent` for the whole-site band. Put each\n * group's median and 90th percentile here, and the dates in\n * `AI_MEASURED_AT`. `ai-credit-estimate.spec.ts` holds the table, the page\n * scaling and the measured whole-site band together, so a re-measure that\n * moves one moves the spec with it.\n */\nexport const AI_MEASURED_UNIT_CREDITS = {\n page: { median: 30, p90: 40 },\n form: { median: 29, p90: 31 },\n layout: { median: 23, p90: 24 },\n theme: { median: 4, p90: 8 },\n plan: { median: 21, p90: 70 },\n} as const satisfies Record<string, AiMeasuredCredits>\n\n/** When the table above was read, and from when its jobs ran. */\nexport const AI_MEASURED_AT = { read: '2026-10-09', since: '2026-10-01' } as const\n\n/** What a whole Free two-page site job measured at, in credits, in the same window. */\nexport const AI_MEASURED_FREE_SITE_CREDITS = { low: 148, high: 160 } as const\n\n/** The sections the measured page item is about: a page is scaled around it. */\nexport const AI_MEASURED_PAGE_SECTIONS = 5\n\n/**\n * What a page of this many sections is likely to cost: the measured page\n * scaled by its sections around the ~5-section median — about 6 credits a\n * section, 8 at the p90.\n */\nexport function aiMeasuredPageCredits(sections: number): AiMeasuredCredits {\n const count = Math.max(1, Math.floor(Number.isFinite(sections) ? sections : AI_MEASURED_PAGE_SECTIONS))\n const { median, p90 } = AI_MEASURED_UNIT_CREDITS.page\n return {\n median: Math.ceil((median * count) / AI_MEASURED_PAGE_SECTIONS),\n p90: Math.ceil((p90 * count) / AI_MEASURED_PAGE_SECTIONS),\n }\n}\n\n/**\n * What a page costs when it is written ON ITS OWN — a page an Assist build or\n * a page job writes, rather than one of a site start's pages (AGL-3722).\n *\n * The table's page item is a site start's: its pages are written one after\n * another inside one job, the later ones reading the prompt the first one\n * paid to write, so a 2-section second page measured 19–28. A page written on\n * its own pays that prompt itself and reads the site it lands on, and costs\n * what a site start's FIRST page costs: a 5-section home page measured 39–78\n * (median 40, p90 78), standalone page jobs' writing step 82–105 for a\n * 2-section page, and the Assist build pages of 2026-10-09 22–89 each. Quoting\n * a build's pages at the site start's scaled median told a Free workspace\n * \"about 48 credits\" for two 4-section pages (AGL-3722, 2026-10-10).\n *\n * So it is priced as a fixed cost — the prompt, the site, the listing — and a\n * cost per section: a 4-section page about 40 (p90 78), a 5-section 46 (p90 90).\n */\nexport const AI_MEASURED_STANDALONE_PAGE = {\n fixed: { median: 16, p90: 30 },\n perSection: { median: 6, p90: 12 },\n} as const satisfies Record<string, AiMeasuredCredits>\n\n/** What a page of this many sections written on its own is likely to cost. */\nexport function aiMeasuredStandalonePageCredits(sections: number): AiMeasuredCredits {\n const count = Math.max(1, Math.floor(Number.isFinite(sections) ? sections : AI_MEASURED_PAGE_SECTIONS))\n const { fixed, perSection } = AI_MEASURED_STANDALONE_PAGE\n return {\n median: fixed.median + perSection.median * count,\n p90: fixed.p90 + perSection.p90 * count,\n }\n}\n\n/**\n * What a creation costs when it is written ON ITS OWN (AGL-3722), where it\n * measured apart from a site start's: an Assist build's layout came to 24–28\n * (2026-10-09/10), against a site start's 23/24 written in its warm prompt.\n * A form measured the same either way (25–30 on its own, 29/31 in a site\n * start), so it keeps the table's figure.\n */\nexport const AI_MEASURED_STANDALONE_CREATION_CREDITS = {\n layout: { median: 26, p90: 30 },\n} as const satisfies Record<string, AiMeasuredCredits>\n\n/**\n * What one pass of a kind with no measurement of its own is likely to cost\n * (a component, an email design, a template, an item another runner\n * builds), written on its own: a component job's writing step measured\n * 52–65, a form job's 37, a layout job's 63, and the Assist build of\n * 2026-10-09 a welcome email at 46 and a products step at 60. Until\n * 2026-10-10 this was a site start's form (29/31), which is written inside a\n * site job's warm prompt and is the cheapest single answer there is.\n */\nexport const AI_MEASURED_PASS_CREDITS: AiMeasuredCredits = { median: 50, p90: 65 }\n\n/** A job's cost as it is quoted: likely, nine times in ten, and at its worst. */\nexport interface AiCreditRange {\n /** The sum of the measured medians: what the job is likely to spend. */\n likely: number\n /** The sum of the measured p90s: what a Free job is admitted on. */\n p90: number\n /** Every pass at its reserve: what it can spend at most. */\n ceiling: number\n}\n\nexport const AI_CREDIT_RANGE_ZERO: AiCreditRange = { likely: 0, p90: 0, ceiling: 0 }\n\n/** Two ranges added. */\nexport function aiCreditRangeAdd(a: AiCreditRange, b: AiCreditRange): AiCreditRange {\n return { likely: a.likely + b.likely, p90: a.p90 + b.p90, ceiling: a.ceiling + b.ceiling }\n}\n\n/** A measured cost as a range with its own ceiling, the likely and p90 never above it. */\nexport function aiCreditRangeOf(measured: AiMeasuredCredits, ceiling: number): AiCreditRange {\n const top = Math.max(0, Math.ceil(ceiling))\n return { likely: Math.min(measured.median, top), p90: Math.min(measured.p90, top), ceiling: top }\n}\n\n/** A range whose likely and p90 are each at least the last, and at most the ceiling. */\nexport function aiCreditRangeOrdered(range: AiCreditRange): AiCreditRange {\n const ceiling = Math.max(0, Math.ceil(range.ceiling))\n const likely = Math.min(Math.max(0, Math.ceil(range.likely)), ceiling)\n const p90 = Math.min(Math.max(likely, Math.ceil(range.p90)), ceiling)\n return { likely, p90, ceiling }\n}\n\nconst n = (value: number) => value.toLocaleString('en-US')\n\n/**\n * The range as every surface quotes it: \"About 106 credits (up to 650)\".\n * A job whose likely figure is its ceiling says one number.\n */\nexport function aiCreditRangeText(range: AiCreditRange): string {\n if (range.likely >= range.ceiling) return `About ${n(range.ceiling)} credits`\n return `About ${n(range.likely)} credits (up to ${n(range.ceiling)})`\n}\n\n/**\n * Whether a Free job of this range needs the person's go-ahead before it\n * starts (AGL-3722): its p90 is more than what is left. A job whose p90\n * fits starts with no prompt; one that does not starts only when confirmed,\n * builds as much as what is left pays for, and pauses with Resume.\n */\nexport function aiCreditsNeedConfirm(range: Pick<AiCreditRange, 'p90'>, left: number): boolean {\n return Number.isFinite(left) && range.p90 > Math.max(0, Math.floor(left))\n}\n\n/** The smaller first build a Free job that does not fit is offered (AGL-3722). */\nexport interface AiCreditsSmaller {\n /** \"Build the home page first\". */\n label: string\n likely: number\n p90: number\n ceiling: number\n}\n\n/**\n * What a Free job that does not fit what is left says before it starts, and\n * the choices it offers: build what fits, a smaller first build, or Upgrade.\n * The door that refuses an unconfirmed start answers with it (status 409,\n * `code: AI_CREDITS_CONFIRM_CODE`), and the plan card and the guided start\n * draw it from the same figures.\n */\nexport interface AiCreditsPrompt extends AiCreditRange {\n left: number\n resetsOn: string\n smaller: AiCreditsSmaller | null\n}\n\n/** The `code` a door answers an unconfirmed Free start with. */\nexport const AI_CREDITS_CONFIRM_CODE = 'credits-confirm'\n\n/** \"Build what fits\": the go-ahead for a Free job past what is left. */\nexport const AI_CREDITS_BUILD_WHAT_FITS = 'Build what fits'\n\n/** The smaller first build's button: \"Build the home page first (about 64 credits)\". */\nexport function aiCreditsSmallerText(smaller: Pick<AiCreditsSmaller, 'label' | 'likely'>): string {\n return `${smaller.label} (about ${n(smaller.likely)} credits)`\n}\n\n/** Which way on a prompt offers, in the order it offers them. */\nexport type AiCreditsChoiceKey = 'fits' | 'smaller' | 'upgrade'\n\n/** One way on, as its option button reads it: a short label and the figure under it (AGL-3722). */\nexport interface AiCreditsChoice {\n key: AiCreditsChoiceKey\n label: string\n detail: string\n /** The one choice drawn as the primary button. */\n recommended: boolean\n}\n\nconst credit = (count: number) => `${n(count)} ${count === 1 ? 'credit' : 'credits'}`\n\n/**\n * The prompt's ways on, as its three option buttons read them (Zach,\n * 2026-10-10: \"more appealing and consistent\"): build what fits, the smaller\n * first build when there is one, and Upgrade when it can be offered — each a\n * short sentence-case label with its figure under it. The smaller build is\n * the recommended one when its p90 fits what is left, since it then finishes;\n * otherwise building what fits is.\n */\nexport function aiCreditsChoices(\n prompt: Pick<AiCreditsPrompt, 'left' | 'smaller'>,\n offers: { smaller: boolean; upgrade: boolean },\n): AiCreditsChoice[] {\n const left = Math.max(0, Math.floor(prompt.left))\n const smaller = offers.smaller ? prompt.smaller : null\n const smallerFits = smaller !== null && smaller.p90 <= left\n const choices: AiCreditsChoice[] = [\n {\n key: 'fits',\n label: AI_CREDITS_BUILD_WHAT_FITS,\n detail: `Uses your ${credit(left)} left, then pauses`,\n recommended: !smallerFits,\n },\n ]\n if (smaller) {\n choices.push({\n key: 'smaller',\n label: smaller.label,\n detail: smallerFits ? `About ${credit(smaller.likely)}, fits what you have left` : `About ${credit(smaller.likely)}`,\n recommended: smallerFits,\n })\n }\n if (offers.upgrade) choices.push({ key: 'upgrade', label: 'Upgrade', detail: 'Get more credits', recommended: false })\n return choices\n}\n\n/** A reset day as a person reads it: \"November 1\". */\nfunction resetLabel(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 * The plain sentence (Zach, 2026-10-09): \"This build is about N credits (up\n * to M). You have L left, so it will build as much as it can and pause when\n * your credits run out. You can upgrade or resume when they renew.\"\n */\nexport function aiCreditsPromptText(prompt: AiCreditsPrompt, noun: 'build' | 'site' = 'build'): string {\n const size =\n prompt.likely >= prompt.ceiling\n ? `about ${n(prompt.ceiling)} credits`\n : `about ${n(prompt.likely)} credits (up to ${n(prompt.ceiling)})`\n const left = Math.max(0, Math.floor(prompt.left))\n return (\n `This ${noun} is ${size}. You have ${n(left)} left, so it will build as much as it can and pause ` +\n `when your credits run out. You can upgrade or resume when they renew on ${resetLabel(prompt.resetsOn)}.`\n )\n}\n\n/**\n * The record a confirmed Free start keeps (AGL-3722): who said go, when,\n * what was left then and the range they were shown.\n */\nexport interface AiJobCreditsConfirmation extends AiCreditRange {\n by: string\n at: Date | string\n left: number\n}\n\n/** The confirmation a door keeps on the job for the prompt the person confirmed, or `null` when there was none. */\nexport function aiCreditsConfirmation(\n prompt: AiCreditsPrompt | null,\n by: string,\n at: Date,\n): AiJobCreditsConfirmation | null {\n if (!prompt) return null\n return { by, at, left: prompt.left, likely: prompt.likely, p90: prompt.p90, ceiling: prompt.ceiling }\n}\n\n/** The prompt a range needs against what is left, or `null` when it fits. */\nexport function aiCreditsPromptFor(\n range: AiCreditRange,\n credits: { left: number; resetsOn: string } | null | undefined,\n smaller: AiCreditsSmaller | null = null,\n): AiCreditsPrompt | null {\n if (!credits || !aiCreditsNeedConfirm(range, credits.left)) return null\n return { ...range, left: Math.max(0, Math.floor(credits.left)), resetsOn: credits.resetsOn, smaller }\n}\n"],"names":["AI_MEASURED_UNIT_CREDITS","page","median","p90","form","layout","theme","plan","AI_MEASURED_AT","read","since","AI_MEASURED_FREE_SITE_CREDITS","low","high","AI_MEASURED_PAGE_SECTIONS","aiMeasuredPageCredits","sections","count","Math","max","floor","Number","isFinite","ceil","AI_MEASURED_STANDALONE_PAGE","fixed","perSection","aiMeasuredStandalonePageCredits","AI_MEASURED_STANDALONE_CREATION_CREDITS","AI_MEASURED_PASS_CREDITS","AI_CREDIT_RANGE_ZERO","likely","ceiling","aiCreditRangeAdd","a","b","aiCreditRangeOf","measured","top","min","aiCreditRangeOrdered","range","n","value","toLocaleString","aiCreditRangeText","aiCreditsNeedConfirm","left","AI_CREDITS_CONFIRM_CODE","AI_CREDITS_BUILD_WHAT_FITS","aiCreditsSmallerText","smaller","label","credit","aiCreditsChoices","prompt","offers","smallerFits","choices","key","detail","recommended","push","upgrade","resetLabel","resetsOn","at","Date","getTime","toLocaleDateString","month","day","timeZone","aiCreditsPromptText","noun","size","aiCreditsConfirmation","by","aiCreditsPromptFor","credits"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-credit-estimate.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 * WHAT A JOB REALLY COSTS, NEXT TO WHAT IT CAN COST (AGL-3722).\n *\n * Every job's cost used to be quoted at its CEILING: each pass at the\n * `AI_SITE_PASS_CREDITS` (50) a pass is reserved against. A Free Assist\n * build of two pages, a quote form and a layout was quoted 600 credits and\n * refused with 164 left, when builds like it measure about a fifth of that.\n *\n * So a job is now quoted as a RANGE: what it is likely to cost (the sum of\n * the measured medians of what it builds), what it costs nine times in ten\n * (the measured p90s), and its ceiling (the old figure, unchanged). The\n * meter still charges actual use, and the reservation each pass takes is\n * still the ceiling's 50 — only the words and the Free admission move:\n * a Free job is admitted outright when its p90 fits what is left, and asked\n * to be confirmed when it does not.\n *\n * This module is pure: the console, the doors and the specs read one table.\n */\n\n/** One measured cost: the median and the 90th percentile, in credits. */\nexport interface AiMeasuredCredits {\n median: number\n p90: number\n}\n\n/**\n * MEASURED IN PRODUCTION, credits per unit, over every `done` AI job since\n * 2026-10-01 (read 2026-10-09; a whole Free site job came to 148–160):\n *\n * page item 30 / 40 (a page of about `AI_MEASURED_PAGE_SECTIONS` sections, its listing included)\n * form 29 / 31\n * layout 23 / 24 (header and footer, one answer)\n * theme 4 / 8 (a site start's look)\n * plan step 21 / 70\n *\n * HOW TO RE-MEASURE: a read-only collection-group read of `aiJobs` in\n * production where `status == 'done'` and `createdAt` is in the window; group\n * each ledger row's (`items[]`) `creditsSpent` by its `op` (the site start's\n * look is the `theme` row), take the plan step's `steps[].creditsSpent` for\n * `plan`, and each job's `creditsSpent` for the whole-site band. Put each\n * group's median and 90th percentile here, and the dates in\n * `AI_MEASURED_AT`. `ai-credit-estimate.spec.ts` holds the table, the page\n * scaling and the measured whole-site band together, so a re-measure that\n * moves one moves the spec with it.\n */\nexport const AI_MEASURED_UNIT_CREDITS = {\n page: { median: 30, p90: 40 },\n form: { median: 29, p90: 31 },\n layout: { median: 23, p90: 24 },\n theme: { median: 4, p90: 8 },\n plan: { median: 21, p90: 70 },\n} as const satisfies Record<string, AiMeasuredCredits>\n\n/**\n * MEASURED IN PRODUCTION, credits per unit of a PAID site start, whose plan\n * and parts run on the paid routing (Sonnet) rather than a Free start's\n * (Haiku) the table above is mostly made of: every `done` paid site job\n * since 2026-10-01, read 2026-10-12 (6 jobs; products from the 4 stores; AGL-3739).\n *\n * plan step 81 / 86 (a Free start's plan: 20 / 42)\n * page item 29 / 49 (about `AI_MEASURED_PAGE_SECTIONS` sections)\n * form 32 / 39\n * layout 29 / 29\n * theme 4 / 4\n * email 49 / 51\n * products 64 / 66 (a store's first products, one answer)\n *\n * A 5-page store with its welcome email was quoted about 277 off the table\n * above: its plan at a Free start's 21 where paid plans run about 81, and its\n * first products not counted at all where they run about 64. Those stores\n * came to 413-448; this table quotes one at about 404.\n *\n * Re-measure as the table above says, keeping only jobs whose plan step's\n * `tokens.model` is the paid routing's.\n */\nexport const AI_MEASURED_PAID_SITE_CREDITS = {\n plan: { median: 81, p90: 86 },\n page: { median: 29, p90: 49 },\n form: { median: 32, p90: 39 },\n layout: { median: 29, p90: 29 },\n theme: { median: 4, p90: 4 },\n email: { median: 49, p90: 51 },\n products: { median: 64, p90: 66 },\n} as const satisfies Record<string, AiMeasuredCredits>\n\n/** When the table above was read, and from when its jobs ran. */\nexport const AI_MEASURED_AT = { read: '2026-10-09', since: '2026-10-01' } as const\n\n/** What a whole Free two-page site job measured at, in credits, in the same window. */\nexport const AI_MEASURED_FREE_SITE_CREDITS = { low: 148, high: 160 } as const\n\n/** The sections the measured page item is about: a page is scaled around it. */\nexport const AI_MEASURED_PAGE_SECTIONS = 5\n\n/**\n * What a page of this many sections is likely to cost: the measured page\n * scaled by its sections around the ~5-section median — about 6 credits a\n * section, 8 at the p90.\n */\nexport function aiMeasuredPageCredits(sections: number): AiMeasuredCredits {\n const count = Math.max(1, Math.floor(Number.isFinite(sections) ? sections : AI_MEASURED_PAGE_SECTIONS))\n const { median, p90 } = AI_MEASURED_UNIT_CREDITS.page\n return {\n median: Math.ceil((median * count) / AI_MEASURED_PAGE_SECTIONS),\n p90: Math.ceil((p90 * count) / AI_MEASURED_PAGE_SECTIONS),\n }\n}\n\n/**\n * What a page costs when it is written ON ITS OWN — a page an Assist build or\n * a page job writes, rather than one of a site start's pages (AGL-3722).\n *\n * The table's page item is a site start's: its pages are written one after\n * another inside one job, the later ones reading the prompt the first one\n * paid to write, so a 2-section second page measured 19–28. A page written on\n * its own pays that prompt itself and reads the site it lands on, and costs\n * what a site start's FIRST page costs: a 5-section home page measured 39–78\n * (median 40, p90 78), standalone page jobs' writing step 82–105 for a\n * 2-section page, and the Assist build pages of 2026-10-09 22–89 each. Quoting\n * a build's pages at the site start's scaled median told a Free workspace\n * \"about 48 credits\" for two 4-section pages (AGL-3722, 2026-10-10).\n *\n * So it is priced as a fixed cost — the prompt, the site, the listing — and a\n * cost per section: a 4-section page about 40 (p90 78), a 5-section 46 (p90 90).\n */\nexport const AI_MEASURED_STANDALONE_PAGE = {\n fixed: { median: 16, p90: 30 },\n perSection: { median: 6, p90: 12 },\n} as const satisfies Record<string, AiMeasuredCredits>\n\n/** What a page of this many sections written on its own is likely to cost. */\nexport function aiMeasuredStandalonePageCredits(sections: number): AiMeasuredCredits {\n const count = Math.max(1, Math.floor(Number.isFinite(sections) ? sections : AI_MEASURED_PAGE_SECTIONS))\n const { fixed, perSection } = AI_MEASURED_STANDALONE_PAGE\n return {\n median: fixed.median + perSection.median * count,\n p90: fixed.p90 + perSection.p90 * count,\n }\n}\n\n/**\n * What a creation costs when it is written ON ITS OWN (AGL-3722), where it\n * measured apart from a site start's: an Assist build's layout came to 24–28\n * (2026-10-09/10), against a site start's 23/24 written in its warm prompt.\n * A form measured the same either way (25–30 on its own, 29/31 in a site\n * start), so it keeps the table's figure.\n */\nexport const AI_MEASURED_STANDALONE_CREATION_CREDITS = {\n layout: { median: 26, p90: 30 },\n} as const satisfies Record<string, AiMeasuredCredits>\n\n/**\n * What one pass of a kind with no measurement of its own is likely to cost\n * (a component, an email design, a template, an item another runner\n * builds), written on its own: a component job's writing step measured\n * 52–65, a form job's 37, a layout job's 63, and the Assist build of\n * 2026-10-09 a welcome email at 46 and a products step at 60. Until\n * 2026-10-10 this was a site start's form (29/31), which is written inside a\n * site job's warm prompt and is the cheapest single answer there is.\n */\nexport const AI_MEASURED_PASS_CREDITS: AiMeasuredCredits = { median: 50, p90: 65 }\n\n/** A job's cost as it is quoted: likely, nine times in ten, and at its worst. */\nexport interface AiCreditRange {\n /** The sum of the measured medians: what the job is likely to spend. */\n likely: number\n /** The sum of the measured p90s: what a Free job is admitted on. */\n p90: number\n /** Every pass at its reserve: what it can spend at most. */\n ceiling: number\n}\n\nexport const AI_CREDIT_RANGE_ZERO: AiCreditRange = { likely: 0, p90: 0, ceiling: 0 }\n\n/** Two ranges added. */\nexport function aiCreditRangeAdd(a: AiCreditRange, b: AiCreditRange): AiCreditRange {\n return { likely: a.likely + b.likely, p90: a.p90 + b.p90, ceiling: a.ceiling + b.ceiling }\n}\n\n/** A measured cost as a range with its own ceiling, the likely and p90 never above it. */\nexport function aiCreditRangeOf(measured: AiMeasuredCredits, ceiling: number): AiCreditRange {\n const top = Math.max(0, Math.ceil(ceiling))\n return { likely: Math.min(measured.median, top), p90: Math.min(measured.p90, top), ceiling: top }\n}\n\n/** A range whose likely and p90 are each at least the last, and at most the ceiling. */\nexport function aiCreditRangeOrdered(range: AiCreditRange): AiCreditRange {\n const ceiling = Math.max(0, Math.ceil(range.ceiling))\n const likely = Math.min(Math.max(0, Math.ceil(range.likely)), ceiling)\n const p90 = Math.min(Math.max(likely, Math.ceil(range.p90)), ceiling)\n return { likely, p90, ceiling }\n}\n\nconst n = (value: number) => value.toLocaleString('en-US')\n\n/**\n * The range as every surface quotes it: \"About 106 credits (up to 650)\".\n * A job whose likely figure is its ceiling says one number.\n */\nexport function aiCreditRangeText(range: AiCreditRange): string {\n if (range.likely >= range.ceiling) return `About ${n(range.ceiling)} credits`\n return `About ${n(range.likely)} credits (up to ${n(range.ceiling)})`\n}\n\n/**\n * Whether a Free job of this range needs the person's go-ahead before it\n * starts (AGL-3722): its p90 is more than what is left. A job whose p90\n * fits starts with no prompt; one that does not starts only when confirmed,\n * builds as much as what is left pays for, and pauses with Resume.\n */\nexport function aiCreditsNeedConfirm(range: Pick<AiCreditRange, 'p90'>, left: number): boolean {\n return Number.isFinite(left) && range.p90 > Math.max(0, Math.floor(left))\n}\n\n/** The smaller first build a Free job that does not fit is offered (AGL-3722). */\nexport interface AiCreditsSmaller {\n /** \"Build the home page first\". */\n label: string\n likely: number\n p90: number\n ceiling: number\n}\n\n/**\n * What a Free job that does not fit what is left says before it starts, and\n * the choices it offers: build what fits, a smaller first build, or Upgrade.\n * The door that refuses an unconfirmed start answers with it (status 409,\n * `code: AI_CREDITS_CONFIRM_CODE`), and the plan card and the guided start\n * draw it from the same figures.\n */\nexport interface AiCreditsPrompt extends AiCreditRange {\n left: number\n resetsOn: string\n smaller: AiCreditsSmaller | null\n}\n\n/** The `code` a door answers an unconfirmed Free start with. */\nexport const AI_CREDITS_CONFIRM_CODE = 'credits-confirm'\n\n/** \"Build what fits\": the go-ahead for a Free job past what is left. */\nexport const AI_CREDITS_BUILD_WHAT_FITS = 'Build what fits'\n\n/** The smaller first build's button: \"Build the home page first (about 64 credits)\". */\nexport function aiCreditsSmallerText(smaller: Pick<AiCreditsSmaller, 'label' | 'likely'>): string {\n return `${smaller.label} (about ${n(smaller.likely)} credits)`\n}\n\n/** Which way on a prompt offers, in the order it offers them. */\nexport type AiCreditsChoiceKey = 'fits' | 'smaller' | 'upgrade'\n\n/** One way on, as its option button reads it: a short label and the figure under it (AGL-3722). */\nexport interface AiCreditsChoice {\n key: AiCreditsChoiceKey\n label: string\n detail: string\n /** The one choice drawn as the primary button. */\n recommended: boolean\n}\n\nconst credit = (count: number) => `${n(count)} ${count === 1 ? 'credit' : 'credits'}`\n\n/**\n * The prompt's ways on, as its three option buttons read them (Zach,\n * 2026-10-10: \"more appealing and consistent\"): build what fits, the smaller\n * first build when there is one, and Upgrade when it can be offered — each a\n * short sentence-case label with its figure under it. The smaller build is\n * the recommended one when its p90 fits what is left, since it then finishes;\n * otherwise building what fits is.\n */\nexport function aiCreditsChoices(\n prompt: Pick<AiCreditsPrompt, 'left' | 'smaller'>,\n offers: { smaller: boolean; upgrade: boolean },\n): AiCreditsChoice[] {\n const left = Math.max(0, Math.floor(prompt.left))\n const smaller = offers.smaller ? prompt.smaller : null\n const smallerFits = smaller !== null && smaller.p90 <= left\n const choices: AiCreditsChoice[] = [\n {\n key: 'fits',\n label: AI_CREDITS_BUILD_WHAT_FITS,\n detail: `Uses your ${credit(left)} left, then pauses`,\n recommended: !smallerFits,\n },\n ]\n if (smaller) {\n choices.push({\n key: 'smaller',\n label: smaller.label,\n detail: smallerFits ? `About ${credit(smaller.likely)}, fits what you have left` : `About ${credit(smaller.likely)}`,\n recommended: smallerFits,\n })\n }\n if (offers.upgrade) choices.push({ key: 'upgrade', label: 'Upgrade', detail: 'Get more credits', recommended: false })\n return choices\n}\n\n/** A reset day as a person reads it: \"November 1\". */\nfunction resetLabel(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 * The plain sentence (Zach, 2026-10-09): \"This build is about N credits (up\n * to M). You have L left, so it will build as much as it can and pause when\n * your credits run out. You can upgrade or resume when they renew.\"\n */\nexport function aiCreditsPromptText(prompt: AiCreditsPrompt, noun: 'build' | 'site' = 'build'): string {\n const size =\n prompt.likely >= prompt.ceiling\n ? `about ${n(prompt.ceiling)} credits`\n : `about ${n(prompt.likely)} credits (up to ${n(prompt.ceiling)})`\n const left = Math.max(0, Math.floor(prompt.left))\n return (\n `This ${noun} is ${size}. You have ${n(left)} left, so it will build as much as it can and pause ` +\n `when your credits run out. You can upgrade or resume when they renew on ${resetLabel(prompt.resetsOn)}.`\n )\n}\n\n/**\n * The record a confirmed Free start keeps (AGL-3722): who said go, when,\n * what was left then and the range they were shown.\n */\nexport interface AiJobCreditsConfirmation extends AiCreditRange {\n by: string\n at: Date | string\n left: number\n}\n\n/** The confirmation a door keeps on the job for the prompt the person confirmed, or `null` when there was none. */\nexport function aiCreditsConfirmation(\n prompt: AiCreditsPrompt | null,\n by: string,\n at: Date,\n): AiJobCreditsConfirmation | null {\n if (!prompt) return null\n return { by, at, left: prompt.left, likely: prompt.likely, p90: prompt.p90, ceiling: prompt.ceiling }\n}\n\n/** The prompt a range needs against what is left, or `null` when it fits. */\nexport function aiCreditsPromptFor(\n range: AiCreditRange,\n credits: { left: number; resetsOn: string } | null | undefined,\n smaller: AiCreditsSmaller | null = null,\n): AiCreditsPrompt | null {\n if (!credits || !aiCreditsNeedConfirm(range, credits.left)) return null\n return { ...range, left: Math.max(0, Math.floor(credits.left)), resetsOn: credits.resetsOn, smaller }\n}\n"],"names":["AI_MEASURED_UNIT_CREDITS","page","median","p90","form","layout","theme","plan","AI_MEASURED_PAID_SITE_CREDITS","email","products","AI_MEASURED_AT","read","since","AI_MEASURED_FREE_SITE_CREDITS","low","high","AI_MEASURED_PAGE_SECTIONS","aiMeasuredPageCredits","sections","count","Math","max","floor","Number","isFinite","ceil","AI_MEASURED_STANDALONE_PAGE","fixed","perSection","aiMeasuredStandalonePageCredits","AI_MEASURED_STANDALONE_CREATION_CREDITS","AI_MEASURED_PASS_CREDITS","AI_CREDIT_RANGE_ZERO","likely","ceiling","aiCreditRangeAdd","a","b","aiCreditRangeOf","measured","top","min","aiCreditRangeOrdered","range","n","value","toLocaleString","aiCreditRangeText","aiCreditsNeedConfirm","left","AI_CREDITS_CONFIRM_CODE","AI_CREDITS_BUILD_WHAT_FITS","aiCreditsSmallerText","smaller","label","credit","aiCreditsChoices","prompt","offers","smallerFits","choices","key","detail","recommended","push","upgrade","resetLabel","resetsOn","at","Date","getTime","toLocaleDateString","month","day","timeZone","aiCreditsPromptText","noun","size","aiCreditsConfirmation","by","aiCreditsPromptFor","credits"],"mappings":"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"}
@@ -158,7 +158,10 @@ export function aiJobsActivity(jobs) {
158
158
  products: 'products',
159
159
  experiment: 'experiment',
160
160
  insight: 'answer',
161
- build: 'build'
161
+ build: 'build',
162
+ navigation: 'menu',
163
+ duplicate: 'copy',
164
+ settings: 'settings'
162
165
  };
163
166
  /** The noun for what a job of this kind makes, or `AI job` for a kind with none. */ export function aiJobKindNoun(kind) {
164
167
  var _KIND_NOUNS_kind;