@aglyn/plugins-ai 1.0.0-beta.225 → 1.0.0-beta.227

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 (161) hide show
  1. package/README.md +2 -1
  2. package/package.json +17 -17
  3. package/src/lib/billing/ai-month-meter.js +5 -3
  4. package/src/lib/billing/ai-month-meter.js.map +1 -1
  5. package/src/lib/billing/ai-overage-ledger.js +3 -1
  6. package/src/lib/billing/ai-overage-ledger.js.map +1 -1
  7. package/src/lib/components/ai-brief-dialog.component.d.ts +6 -2
  8. package/src/lib/components/ai-brief-dialog.component.js +35 -17
  9. package/src/lib/components/ai-brief-dialog.component.js.map +1 -1
  10. package/src/lib/components/ai-describe-automation.component.js +17 -2
  11. package/src/lib/components/ai-describe-automation.component.js.map +1 -1
  12. package/src/lib/components/ai-insight-dialog.component.js +24 -2
  13. package/src/lib/components/ai-insight-dialog.component.js.map +1 -1
  14. package/src/lib/components/ai-job-events.d.ts +14 -0
  15. package/src/lib/components/ai-job-events.js +76 -0
  16. package/src/lib/components/ai-job-events.js.map +1 -0
  17. package/src/lib/components/ai-job-follow.component.d.ts +49 -0
  18. package/src/lib/components/ai-job-follow.component.js +210 -0
  19. package/src/lib/components/ai-job-follow.component.js.map +1 -0
  20. package/src/lib/components/ai-job-links.d.ts +42 -0
  21. package/src/lib/components/ai-job-links.js +140 -0
  22. package/src/lib/components/ai-job-links.js.map +1 -0
  23. package/src/lib/components/ai-job-plan.component.d.ts +13 -6
  24. package/src/lib/components/ai-job-plan.component.js +29 -3
  25. package/src/lib/components/ai-job-plan.component.js.map +1 -1
  26. package/src/lib/components/ai-job-requests.d.ts +24 -0
  27. package/src/lib/components/ai-job-requests.js +76 -0
  28. package/src/lib/components/ai-job-requests.js.map +1 -0
  29. package/src/lib/components/ai-jobs-indicator.component.d.ts +25 -0
  30. package/src/lib/components/ai-jobs-indicator.component.js +80 -0
  31. package/src/lib/components/ai-jobs-indicator.component.js.map +1 -0
  32. package/src/lib/components/ai-jobs-store.d.ts +81 -0
  33. package/src/lib/components/ai-jobs-store.js +218 -0
  34. package/src/lib/components/ai-jobs-store.js.map +1 -0
  35. package/src/lib/components/ai-page-brief-dialog.component.d.ts +3 -3
  36. package/src/lib/components/ai-page-brief-dialog.component.js.map +1 -1
  37. package/src/lib/components/ai-site-build-page.component.d.ts +23 -0
  38. package/src/lib/components/ai-site-build-page.component.js +377 -0
  39. package/src/lib/components/ai-site-build-page.component.js.map +1 -0
  40. package/src/lib/components/ai-site-start-card.component.d.ts +1 -1
  41. package/src/lib/components/ai-site-start-card.component.js +522 -130
  42. package/src/lib/components/ai-site-start-card.component.js.map +1 -1
  43. package/src/lib/components/ai-site-starter-fallback.component.d.ts +24 -0
  44. package/src/lib/components/ai-site-starter-fallback.component.js +112 -0
  45. package/src/lib/components/ai-site-starter-fallback.component.js.map +1 -0
  46. package/src/lib/components/assist-jobs-drawer.component.d.ts +18 -20
  47. package/src/lib/components/assist-jobs-drawer.component.js +152 -172
  48. package/src/lib/components/assist-jobs-drawer.component.js.map +1 -1
  49. package/src/lib/components/assist-panel.component.js +62 -6
  50. package/src/lib/components/assist-panel.component.js.map +1 -1
  51. package/src/lib/components/staff-ai-credit-return.component.d.ts +55 -0
  52. package/src/lib/components/staff-ai-credit-return.component.js +346 -0
  53. package/src/lib/components/staff-ai-credit-return.component.js.map +1 -0
  54. package/src/lib/components/staff-org-ai-card.component.js +364 -279
  55. package/src/lib/components/staff-org-ai-card.component.js.map +1 -1
  56. package/src/lib/components/staff-user-ai-usage-card.component.d.ts +4 -0
  57. package/src/lib/components/staff-user-ai-usage-card.component.js +83 -28
  58. package/src/lib/components/staff-user-ai-usage-card.component.js.map +1 -1
  59. package/src/lib/components/use-ai-job-run.js +5 -18
  60. package/src/lib/components/use-ai-job-run.js.map +1 -1
  61. package/src/lib/jobs/ai-job-auto-confirm.d.ts +52 -0
  62. package/src/lib/jobs/ai-job-auto-confirm.js +62 -0
  63. package/src/lib/jobs/ai-job-auto-confirm.js.map +1 -0
  64. package/src/lib/jobs/ai-job-plan-step.d.ts +33 -2
  65. package/src/lib/jobs/ai-job-plan-step.js +161 -17
  66. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  67. package/src/lib/jobs/ai-job-site-step.d.ts +7 -0
  68. package/src/lib/jobs/ai-job-site-step.js +34 -4
  69. package/src/lib/jobs/ai-job-site-step.js.map +1 -1
  70. package/src/lib/jobs/ai-job-text-step.d.ts +7 -0
  71. package/src/lib/jobs/ai-job-text-step.js.map +1 -1
  72. package/src/lib/jobs/ai-jobs-notify.d.ts +37 -0
  73. package/src/lib/jobs/ai-jobs-notify.js +51 -0
  74. package/src/lib/jobs/ai-jobs-notify.js.map +1 -0
  75. package/src/lib/jobs/ai-jobs.d.ts +31 -4
  76. package/src/lib/jobs/ai-jobs.js +196 -32
  77. package/src/lib/jobs/ai-jobs.js.map +1 -1
  78. package/src/lib/model/ai-job-activity.d.ts +112 -0
  79. package/src/lib/model/ai-job-activity.js +144 -0
  80. package/src/lib/model/ai-job-activity.js.map +1 -0
  81. package/src/lib/model/ai-job-failure-copy.d.ts +69 -0
  82. package/src/lib/model/ai-job-failure-copy.js +78 -0
  83. package/src/lib/model/ai-job-failure-copy.js.map +1 -0
  84. package/src/lib/model/ai-job-notice.d.ts +61 -0
  85. package/src/lib/model/ai-job-notice.js +112 -0
  86. package/src/lib/model/ai-job-notice.js.map +1 -0
  87. package/src/lib/model/ai-jobs.types.d.ts +26 -0
  88. package/src/lib/model/ai-jobs.types.js +1 -0
  89. package/src/lib/model/ai-jobs.types.js.map +1 -1
  90. package/src/lib/model/ai-plan-capabilities.d.ts +7 -0
  91. package/src/lib/model/ai-plan-capabilities.js.map +1 -1
  92. package/src/lib/model/ai-site-build-progress.d.ts +45 -0
  93. package/src/lib/model/ai-site-build-progress.js +76 -0
  94. package/src/lib/model/ai-site-build-progress.js.map +1 -0
  95. package/src/lib/model/ai-site-inventory.d.ts +9 -0
  96. package/src/lib/model/ai-site-inventory.js +7 -2
  97. package/src/lib/model/ai-site-inventory.js.map +1 -1
  98. package/src/lib/model/ai-site-job.d.ts +93 -2
  99. package/src/lib/model/ai-site-job.js +88 -7
  100. package/src/lib/model/ai-site-job.js.map +1 -1
  101. package/src/lib/model/ai-site-start.d.ts +3 -1
  102. package/src/lib/model/ai-site-start.js +6 -4
  103. package/src/lib/model/ai-site-start.js.map +1 -1
  104. package/src/lib/model/ai-template-subjects.d.ts +4 -3
  105. package/src/lib/model/ai-template-subjects.js +5 -4
  106. package/src/lib/model/ai-template-subjects.js.map +1 -1
  107. package/src/lib/plan-entitlements.d.ts +3 -3
  108. package/src/lib/plan-entitlements.js +3 -3
  109. package/src/lib/plan-entitlements.js.map +1 -1
  110. package/src/lib/plugin.js +28 -0
  111. package/src/lib/plugin.js.map +1 -1
  112. package/src/lib/runtime/ai-abuse-guards.d.ts +9 -6
  113. package/src/lib/runtime/ai-abuse-guards.js +10 -7
  114. package/src/lib/runtime/ai-abuse-guards.js.map +1 -1
  115. package/src/lib/runtime/ai-doctrine-validators.js +31 -4
  116. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  117. package/src/lib/runtime/ai-gate.d.ts +3 -2
  118. package/src/lib/runtime/ai-gate.js +3 -2
  119. package/src/lib/runtime/ai-gate.js.map +1 -1
  120. package/src/lib/runtime/ai-palette.generated.js +12 -4
  121. package/src/lib/runtime/ai-palette.generated.js.map +1 -1
  122. package/src/lib/runtime/site-inventory.d.ts +16 -1
  123. package/src/lib/runtime/site-inventory.js +43 -3
  124. package/src/lib/runtime/site-inventory.js.map +1 -1
  125. package/src/lib/server/ai-admin-credits.d.ts +21 -0
  126. package/src/lib/server/ai-admin-credits.js +213 -0
  127. package/src/lib/server/ai-admin-credits.js.map +1 -0
  128. package/src/lib/server/ai-admin-org.js +12 -5
  129. package/src/lib/server/ai-admin-org.js.map +1 -1
  130. package/src/lib/server/ai-admin-signals.js +3 -1
  131. package/src/lib/server/ai-admin-signals.js.map +1 -1
  132. package/src/lib/server/ai-admin-user.js +12 -1
  133. package/src/lib/server/ai-admin-user.js.map +1 -1
  134. package/src/lib/server/ai-allotments.js +3 -2
  135. package/src/lib/server/ai-allotments.js.map +1 -1
  136. package/src/lib/server/ai-jobs-route.js +20 -2
  137. package/src/lib/server/ai-jobs-route.js.map +1 -1
  138. package/src/lib/server/assist-chat.js +3 -2
  139. package/src/lib/server/assist-chat.js.map +1 -1
  140. package/src/lib/server/billing-credits.js +4 -3
  141. package/src/lib/server/billing-credits.js.map +1 -1
  142. package/src/lib/server/billing-overage.js +3 -2
  143. package/src/lib/server/billing-overage.js.map +1 -1
  144. package/src/lib/server.js +11 -0
  145. package/src/lib/server.js.map +1 -1
  146. package/src/lib/usage/assist-credit-returns-write.d.ts +75 -0
  147. package/src/lib/usage/assist-credit-returns-write.js +150 -0
  148. package/src/lib/usage/assist-credit-returns-write.js.map +1 -0
  149. package/src/lib/usage/assist-credit-returns.d.ts +68 -0
  150. package/src/lib/usage/assist-credit-returns.js +79 -0
  151. package/src/lib/usage/assist-credit-returns.js.map +1 -0
  152. package/src/lib/usage/assist-free-taste.js +6 -3
  153. package/src/lib/usage/assist-free-taste.js.map +1 -1
  154. package/src/lib/usage/assist-job-refund.d.ts +100 -0
  155. package/src/lib/usage/assist-job-refund.js +109 -0
  156. package/src/lib/usage/assist-job-refund.js.map +1 -0
  157. package/src/lib/usage/assist-usage.js +5 -2
  158. package/src/lib/usage/assist-usage.js.map +1 -1
  159. package/src/lib/usage/staff-org-ai.d.ts +31 -1
  160. package/src/lib/usage/staff-org-ai.js +19 -2
  161. package/src/lib/usage/staff-org-ai.js.map +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-plan-step.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { createHash } from 'node:crypto'\nimport type { AglynOrgBilling } from '@aglyn/aglyn/foundation/definitions/org-billing.types'\nimport {\n AI_BUILD_PLAN_TOOL,\n aiBuildPlanToolFor,\n isAiPlanNewRef,\n type AiBuildPlan,\n} from '../model/ai-build-plan'\nimport type { AiJob, AiJobKind, AiJobPlan, AiJobStatus } from '../model/ai-jobs.types'\nimport {\n AI_TEMPLATE_SUBJECT_DEFINITIONS,\n aiTemplateShownTokens,\n type AiTemplateSubject,\n} from '../model/ai-template-subjects'\nimport { AI_JOB_CREATE_KINDS, AI_JOB_CREATE_NOUNS } from '../model/ai-job-creations'\nimport { AI_PAGE_CREATE_KINDS, aiPagePlanShapeRefusal } from '../model/ai-page-job'\nimport {\n aiPlanCapabilitiesForJob,\n aiPlanCapabilityLines,\n type AiPlanCapabilities,\n type AiPlanJobScope,\n} from '../model/ai-plan-capabilities'\nimport type { AiSiteInventory } from '../model/ai-site-inventory'\nimport { AI_SITE_CREATE_KINDS, aiSitePlanShapeRefusal } from '../model/ai-site-job'\nimport { aiDoctrineSystemBlocks, runValidatedGeneration } from '../runtime/ai-doctrine'\nimport { AI_STEP_TIERS } from '../providers/catalog'\nimport {\n aiPlanEmbedBriefViolations,\n type AiDoctrineViolation,\n} from '../runtime/ai-doctrine-validators'\nimport { AI_ROUTING_TABLE, aiModelForStep } from '../providers/routing'\nimport type { AiTool } from '../providers/contract'\nimport type { AiSystemBlock } from '../runtime/ai-runtime'\nimport { readSiteInventory } from '../runtime/site-inventory'\nimport { aiJobAdmissionRefusal } from './ai-job-admission'\nimport { readAiPlanCapabilities } from './ai-job-drafts'\nimport {\n AI_JOB_BRIEF_MAX_CHARS,\n type AiJobStepOutcome,\n type AiJobStepRunner,\n} from './ai-job-text-step'\nimport { aiJobStepBudget } from './ai-job-budget'\nimport { aiDoctrineReview, aiUnspentOutcome } from './ai-job-generation'\nimport { aiPlanWithDraftIds } from './ai-job-draft-ids'\nimport { AI_JOBS_COLLECTION, registerAiJobPlanStep } from './ai-jobs'\n\n/**\n * The plan step (AGL-2935): the first step of every job that builds site\n * structure. Before a node is generated, the model answers with a typed plan\n * — what the site already has that the job reuses, what it creates and why,\n * and the screens it builds from them — held to the doctrine's plan rules\n * against the site's inventory and re-asked once when it breaks one.\n *\n * A plan that passes is kept on the job, and the job stops for review: the\n * member reads what it will build before a credit is spent building it, and\n * confirms through the resume door. A plan that still breaks a rule on its\n * re-ask stops the job the same way, with the rules named. Either way the\n * step writes nothing itself; the machine records the plan, the spend and\n * the stop, as it records every step.\n *\n * ── Told what it may create, refused what it cannot build (AGL-3030) ─────\n *\n * Before it asks, the step reads what the job may create on its site: the\n * workspace's plan, the site's counts against its limits, and — for a kind\n * that builds only some plans — what the job builds itself. The user turn\n * states it, the plan rules refuse a creation outside it with the one re-ask,\n * and where the workspace keeps no reusable components they accept the page\n * built inline. A plan the confirm door would still refuse — the kind's own\n * admission, handed the plan, as the resume door hands it — is refused HERE,\n * before a member is shown a Confirm: the job fails with the door's sentence,\n * its plan is not kept, and it holds nothing.\n */\n\n/** The plan step's own instructions, cached after the doctrine. */\nexport const AI_JOB_PLAN_INSTRUCTIONS: readonly AiSystemBlock[] = [\n {\n text:\n 'You plan what a website builder job will build, before anything is built. You are given the kind of job and the brief from the person who owns the site. ' +\n 'Answer with submit_build_plan: what the site inventory already has that the job reuses, what it must create and why nothing listed will do, and each page it builds (the screens list) with its layout, template, slug, search title, search description and sections from top to bottom. ' +\n 'Refer to inventory records by id and to what the plan creates as new:<name>. Plan only what the brief asks for: a component, layout, template, form or email job plans no pages unless the brief asks for them, and a page job plans exactly one page.',\n },\n]\n\n/**\n * The plan step's time (AGL-3026, AGL-3036): its two inventory-lookup rounds,\n * its answer and its re-ask at the routing table's ceiling for `job.plan`, on\n * the tier that step kind is served from, with the step's reads and writes,\n * at the rates `ai-job-budget.ts` assumes — fitted to what a beat can give a\n * step, so the served tier asks a little under the routing ceiling.\n */\nexport const AI_JOB_PLAN_STEP_BUDGET = aiJobStepBudget({\n tier: AI_STEP_TIERS['job.plan'],\n maxTokens: AI_ROUTING_TABLE['job.plan'].maxTokens,\n})\n\n/**\n * The least time a plan needs before it starts. Registered with the step, so\n * an inline door never starts a plan. A plan thinks before it answers and\n * routinely runs past an inline door's budget, and a provider call that\n * budget cuts off is still generated and billed upstream while the meter\n * records nothing. The beat starts a plan only with this much of its own\n * budget left, and a spec holds it inside that budget.\n */\nexport const AI_JOB_PLAN_STEP_MINIMUM_MS = AI_JOB_PLAN_STEP_BUDGET.minimumMs\n\n/**\n * The plan's answer ceiling on the model a job runs: the most whose worst\n * case fits the least time the step registered, and never more than the\n * routing table's. A model the catalog does not know is planned at the\n * slowest.\n */\nexport function aiJobPlanMaxTokens(model: string): number {\n return AI_JOB_PLAN_STEP_BUDGET.maxTokens(model)\n}\n\n/** What the member reads while a plan waits for them. */\nexport const AI_JOB_PLAN_REVIEW_COPY =\n 'The plan is ready. Review what the job will reuse and create, then confirm it to build.'\n\n/**\n * The job as the plan step's user turn: its kind, its brief, its scalar\n * inputs and, where the job read them, what it may create on its site\n * (AGL-3030). Per workspace and per site, which is why they ride here and\n * never in a system block the platform's cache entry is keyed on.\n */\nexport function aiJobPlanPrompt(\n job: Pick<AiJob, 'kind' | 'brief' | 'inputs'>,\n capabilities: AiPlanCapabilities | null = null,\n): string {\n const lines = [`Job kind: ${job.kind}`, `Brief: ${job.brief.slice(0, AI_JOB_BRIEF_MAX_CHARS)}`]\n for (const [key, value] of Object.entries(job.inputs ?? {})) {\n if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {\n lines.push(`${key}: ${String(value)}`)\n }\n }\n if (capabilities) lines.push(...aiPlanCapabilityLines(capabilities))\n lines.push(...aiPlanTemplateTokenLines(job))\n return lines.join('\\n')\n}\n\n/**\n * What a TEMPLATE job's plan is told its creation's `fields` are (AGL-3143\n * §11): the subject's binding tokens, listed so the planner promises only\n * what the page can fill and the build can be held to it\n * (`aiPlanTemplateTokenViolations`).\n *\n * ⛔ It rides the job's own turn and NOT the `fields` description in the plan\n * tool, which every kind's request caches. Written there it read better and\n * cost 40 tokens of the shared prefix — 2,980 to 3,020 — which is one credit\n * of the Free page's 300-credit wall, taking the room it keeps for a re-asked\n * section from 45 credits to 44 and breaking `ai-job-free-page.spec.ts`. A\n * page job must not pay for a sentence about templates.\n *\n * The list is the tokens a reader sees, never the whole catalog (AGL-3143\n * §16). Offered a slug and the publish timestamp, a live plan promised them,\n * and its build kept the promise the only way a slug can be kept: it printed\n * \"entry slug: {{entry.slug}}\" on every article.\n */\nexport function aiPlanTemplateTokenLines(job: Pick<AiJob, 'kind' | 'inputs'>): string[] {\n if (job.kind !== 'template') return []\n const subject = (job.inputs ?? {})['subject']\n const definition =\n typeof subject === 'string'\n ? AI_TEMPLATE_SUBJECT_DEFINITIONS[subject as AiTemplateSubject]\n : undefined\n if (!definition) return []\n return [\n `The template's fields are the binding tokens its page shows a reader, each one of: ${aiTemplateShownTokens(\n definition,\n ).join(', ')}. A link or picture token is shown by the link or picture holding it. Promise only what the page fills.`,\n ]\n}\n\n/** A kind that builds only some plans: the creations it makes, and the shapes it refuses. */\nexport interface AiJobPlanScope extends AiPlanJobScope {\n /** Why a job of the kind cannot build a plan of this shape; `null` when it can. Pure. */\n shapeRefusal: (plan: AiBuildPlan) => string | null\n}\n\n/**\n * The kinds whose confirm door builds only some plans (AGL-3030). A page job\n * builds one screen and a scaffold four to eight; each builds only the\n * creations its list names. A template, layout, component, form or email job\n * builds its own record and what that record places (AGL-3143 §15,\n * `AI_JOB_CREATE_KINDS`), and holds no plan to a shape.\n */\nexport const AI_JOB_PLAN_SCOPES: Readonly<Partial<Record<AiJobKind, AiJobPlanScope>>> = {\n page: { noun: 'a page job', creates: AI_PAGE_CREATE_KINDS, shapeRefusal: aiPagePlanShapeRefusal },\n site: { noun: 'a site scaffold', creates: AI_SITE_CREATE_KINDS, shapeRefusal: aiSitePlanShapeRefusal },\n ...Object.fromEntries(\n Object.entries(AI_JOB_CREATE_KINDS).map(([kind, creates]) => [\n kind,\n { noun: AI_JOB_CREATE_NOUNS[kind as AiJobKind] as string, creates, shapeRefusal: () => null },\n ]),\n ),\n}\n\n/**\n * What the plan step holds beyond the doctrine's plan rules, as the\n * violations the plan's one re-ask names: the players the plan lists against\n * the brief it answers (AGL-3433), which only this step reads beside the plan,\n * and a kind's shape refusal.\n */\nfunction planViolations(\n scope: AiJobPlanScope | null,\n brief: string,\n): (plan: AiBuildPlan) => AiDoctrineViolation[] {\n return (plan) => {\n const message = scope?.shapeRefusal(plan) ?? null\n return [\n ...aiPlanEmbedBriefViolations(plan, brief),\n ...(message ? [{ rule: null, code: 'plan-job-shape', message }] : []),\n ]\n }\n}\n\n/**\n * The inventory names of every id the plan references, so the proposal reads\n * \"the Services layout\" rather than an id — kept on the plan because the\n * inventory is not read again when the member opens it.\n */\nexport function aiPlanLabels(\n plan: AiBuildPlan,\n inventory: AiSiteInventory | null,\n): Record<string, string> {\n const names = new Map<string, string>()\n for (const rows of [\n inventory?.components,\n inventory?.layouts,\n inventory?.templates,\n inventory?.forms,\n inventory?.datasets,\n inventory?.collections,\n inventory?.screens,\n ]) {\n for (const row of rows ?? []) names.set(row.id, row.name)\n }\n const refs = [\n ...plan.reuse.map((entry) => entry.id),\n ...plan.create.map((entry) => entry.duplicateOf),\n ...plan.screens.flatMap((screen) => [\n screen.layout,\n screen.template,\n screen.duplicateOf,\n ...screen.sections.flatMap((section) => section.uses),\n ]),\n ]\n const labels: Record<string, string> = {}\n for (const ref of refs) {\n if (!ref || isAiPlanNewRef(ref)) continue\n const name = names.get(ref)\n if (name) labels[ref] = name\n }\n return labels\n}\n\n/* ------------------------------------------------------------------------ *\n * Reusing an identical brief's plan\n * ------------------------------------------------------------------------ */\n\n/**\n * How long a plan may be reused for (AGL-2937).\n *\n * The window is what makes reuse safe rather than merely cheap. The key\n * already covers everything the request said — the brief, the inputs, the\n * model and the prompt as rendered, site inventory included — so a plan is\n * reused only when nothing the model was shown has changed. What the key\n * cannot see is the world outside the request: a page published, a component\n * renamed after the inventory was read, a member who meant something\n * different the second time. Fifteen minutes is short enough that the answer\n * is still the one the site would give, and long enough to cover what reuse\n * is actually for — the same brief run twice in a sitting, a job resubmitted\n * after a refused reservation, two members starting the same work.\n */\nexport const AI_PLAN_REUSE_WINDOW_MS = 15 * 60 * 1_000\n\n/** Jobs one lookup reads. More than one can share a key; the newest plan wins. */\nexport const AI_PLAN_REUSE_CANDIDATES = 5\n\n/** A job whose plan might be reused, as the finder reports it. */\nexport interface AiJobPlanCandidate {\n jobId: string\n status: AiJobStatus\n plan: AiJobPlan\n}\n\n/**\n * The digest a plan is reused by: the whole request, never the answer.\n *\n * It hashes what the model was asked and what it was shown — the job's kind\n * and site, the user turn (which carries the trimmed brief and every scalar\n * input), the model that would answer, every rendered system block including\n * the site inventory, and the tool's schema. Two requests that hash the same\n * would have been sent the same bytes to the same model, so the second can\n * keep the first's answer.\n *\n * The version tag is the escape hatch: anything that changes what a key\n * MEANS, rather than what it covers, bumps it and strands every old key\n * harmlessly, since a key nothing matches simply asks the model.\n */\nexport function aiJobPlanKey(input: {\n job: Pick<AiJob, 'kind' | 'hostId'>\n prompt: string\n model: string\n system: readonly AiSystemBlock[]\n /** The plan tool the request offers (AGL-3433); the plain one when absent. */\n tool?: AiTool\n}): string {\n const digest = createHash('sha256')\n for (const part of [\n 'plan.v1',\n input.job.kind,\n input.job.hostId ?? '',\n input.model,\n input.prompt,\n ...input.system.map((block) => block.text),\n JSON.stringify(input.tool ?? AI_BUILD_PLAN_TOOL),\n ]) {\n // Length-prefixed, so two different splits of the same characters cannot\n // collide by running into one another.\n digest.update(`${part.length}:${part}\\u0000`)\n }\n return digest.digest('hex')\n}\n\n/** A job that could not have produced a reusable plan, whatever its key says. */\nconst UNREUSABLE: readonly AiJobStatus[] = ['canceled', 'failed']\n\nfunction planMillis(plan: AiJobPlan): number | null {\n const at = plan.proposedAt as unknown\n if (at instanceof Date) return at.getTime()\n if (at && typeof (at as { toMillis?: unknown }).toMillis === 'function') {\n return (at as { toMillis(): number }).toMillis()\n }\n return null\n}\n\n/**\n * The newest plan worth reusing out of what the finder returned: another\n * job's, still proposed or already confirmed, on a job that was neither\n * canceled nor failed, proposed inside the window.\n *\n * A canceled or failed job is excluded even though its plan may be perfectly\n * good: those are the jobs a member walked away from, and handing their plan\n * to the next one would make a rejected answer look like a fresh one.\n */\nexport function aiReusablePlan(\n candidates: readonly AiJobPlanCandidate[],\n context: { jobId: string; now: Date },\n): AiJobPlanCandidate | null {\n const floor = context.now.getTime() - AI_PLAN_REUSE_WINDOW_MS\n let best: AiJobPlanCandidate | null = null\n let bestAt = -1\n for (const candidate of candidates) {\n if (candidate.jobId === context.jobId) continue\n if (UNREUSABLE.includes(candidate.status)) continue\n if (candidate.plan.status !== 'proposed' && candidate.plan.status !== 'confirmed') continue\n const at = planMillis(candidate.plan)\n if (at === null || at < floor || at > context.now.getTime()) continue\n if (at > bestAt) {\n best = candidate\n bestAt = at\n }\n }\n return best\n}\n\nexport type AiJobPlanFinder = (\n orgId: string,\n key: string,\n firestore?: FirebaseFirestore.Firestore,\n) => Promise<AiJobPlanCandidate[]>\n\n/**\n * The finder the step uses in production: an equality on `plan.key` inside\n * the org's own jobs.\n *\n * One equality on one field, with no ordering beside it, so Firestore's\n * automatic single-field index answers it and no composite index is deployed.\n * The window and the statuses are applied in memory instead, which is what\n * keeps it that way.\n */\nexport const findAiJobsByPlanKey: AiJobPlanFinder = async (orgId, key, firestore) => {\n if (!firestore) return []\n const snapshot = await firestore\n .collection('orgs')\n .doc(orgId)\n .collection(AI_JOBS_COLLECTION)\n .where('plan.key', '==', key)\n .limit(AI_PLAN_REUSE_CANDIDATES)\n .get()\n return snapshot.docs.flatMap((doc) => {\n const data = doc.data() as { status?: AiJobStatus; plan?: AiJobPlan | null }\n return data.plan\n ? [{ jobId: doc.id, status: data.status ?? 'queued', plan: data.plan }]\n : []\n })\n}\n\n/** What a job may create on its site, read for the plan step; `null` for no site. */\nexport type AiPlanCapabilitiesReader = (input: {\n job: AiJob\n org: Partial<AglynOrgBilling> | null\n firestore: FirebaseFirestore.Firestore\n}) => Promise<AiPlanCapabilities | null>\n\n/** The reader the step uses in production: the draft bands' own arithmetic, for the job's site. */\nexport const readAiJobPlanCapabilities: AiPlanCapabilitiesReader = async ({ job, org, firestore }) =>\n job.hostId ? readAiPlanCapabilities(firestore, { hostId: job.hostId, org }) : null\n\nexport interface AiJobPlanStepDeps {\n /** The inventory reader; specs hand in a fake. */\n readInventory?: typeof readSiteInventory\n /** The reuse lookup; specs hand in a fake, and `null` turns reuse off. */\n findPlansByKey?: AiJobPlanFinder | null\n /** What the job may create on its site (AGL-3030); specs and the eval recorder hand in their own. */\n readCapabilities?: AiPlanCapabilitiesReader\n /** The kind's admission, asked of a plan before it is kept; the registry's otherwise. */\n admissionRefusal?: typeof aiJobAdmissionRefusal\n}\n\nexport function createAiJobPlanStep(deps: AiJobPlanStepDeps = {}): AiJobStepRunner {\n const readInventory = deps.readInventory ?? readSiteInventory\n const findPlansByKey =\n deps.findPlansByKey === undefined ? findAiJobsByPlanKey : deps.findPlansByKey\n const readCapabilities = deps.readCapabilities ?? readAiJobPlanCapabilities\n const admissionRefusal = deps.admissionRefusal ?? aiJobAdmissionRefusal\n return async ({ job, now, signal, firestore, modelFor, org: orgDocument }) => {\n const org = (orgDocument ?? null) as Partial<AglynOrgBilling> | null\n const [inventory, workspace] = await Promise.all([\n job.hostId ? readInventory(job.orgId, job.hostId, { firestore }) : Promise.resolve(null),\n readCapabilities({ job, org, firestore }),\n ])\n const scope = AI_JOB_PLAN_SCOPES[job.kind] ?? null\n const capabilities = workspace ? aiPlanCapabilitiesForJob(workspace, scope) : null\n // The model switch's answer for this job (AGL-2942): the creator's pick\n // where the plan, the org restriction and the allotment allowlists allow\n // it, and Auto held to those same lists otherwise. Without a resolver the\n // doctrine asks the routing table itself.\n const model = modelFor?.('job.plan')\n const route = AI_ROUTING_TABLE['job.plan']\n const prompt = aiJobPlanPrompt(job, capabilities)\n\n /**\n * The confirm door's answer for this plan, asked before the plan is kept\n * (AGL-3030): the kind's own admission, handed the plan as the resume\n * door hands it. A refusal fails the job with the door's sentence; a door\n * that could not answer keeps the plan, since the resume door asks again\n * before anything is built.\n */\n const refusalOnKeep = async (plan: AiJobPlan, outcome: AiJobStepOutcome): Promise<AiJobStepOutcome | null> => {\n let refusal: Awaited<ReturnType<typeof aiJobAdmissionRefusal>> = null\n try {\n refusal = await admissionRefusal(job.kind, {\n firestore,\n orgId: job.orgId,\n hostId: job.hostId ?? null,\n inputs: job.inputs ?? {},\n org,\n plan,\n uid: job.createdBy,\n })\n } catch (error) {\n console.error('ai plan admission failed', { orgId: job.orgId, jobId: job.$id, error })\n }\n return refusal ? { ...outcome, failure: refusal.error } : null\n }\n\n // Reuse before asking (AGL-2937). The key covers the whole request, so a\n // hit is a request that would have been sent the same bytes to the same\n // model; the window covers what the key cannot see. A reused plan spends\n // nothing, so the machine's spent-nothing branch releases the reservation\n // and meters no credit, and the job still stops for the member to confirm\n // — this job's plan is proposed to this member, whatever the last one did\n // with theirs.\n const resolved = model ?? aiModelForStep('job.plan')\n // A brief that asks for a video is offered the plan's list of players\n // (AGL-3433), and a job that may bind a dataset a record template on\n // each screen (AGL-3475).\n const tool = aiBuildPlanToolFor(job.brief, {\n records: Boolean(inventory?.datasets.length) || capabilities?.create.dataset.allowed === true,\n })\n const key = aiJobPlanKey({\n job,\n prompt,\n model: resolved,\n system: aiDoctrineSystemBlocks(inventory, { instructions: AI_JOB_PLAN_INSTRUCTIONS }),\n tool,\n })\n const reused = findPlansByKey\n ? aiReusablePlan(await findPlansByKey(job.orgId, key, firestore), {\n jobId: job.$id,\n now,\n })\n : null\n if (reused) {\n // The reused plan's draft ids name the other job's drafts; this job's are its own.\n const plan: AiJobPlan = aiPlanWithDraftIds(job.kind, {\n ...reused.plan,\n status: 'proposed',\n labels: aiPlanLabels(reused.plan, inventory),\n proposedAt: now as unknown as AiJobPlan['proposedAt'],\n confirmedAt: null,\n confirmedBy: null,\n key,\n reusedFrom: reused.jobId,\n })\n const unspent = aiUnspentOutcome(resolved)\n return (\n (await refusalOnKeep(plan, unspent)) ?? {\n ...unspent,\n plan,\n review: { reason: 'plan', message: AI_JOB_PLAN_REVIEW_COPY, findings: [] },\n }\n )\n }\n\n const result = await runValidatedGeneration('plan', {\n step: 'job.plan',\n ...(model ? { model } : {}),\n instructions: AI_JOB_PLAN_INSTRUCTIONS,\n inventory,\n messages: [{ role: 'user', content: prompt }],\n tool,\n // The routing ceiling, lowered on a tier too slow to look records up,\n // answer and ask again inside the least time the step registered.\n maxTokens: aiJobPlanMaxTokens(resolved),\n ...(route.thinking ? { thinking: route.thinking } : {}),\n ...(route.effort ? { effort: route.effort } : {}),\n ...(signal ? { signal } : {}),\n capabilities,\n extend: planViolations(scope, job.brief),\n })\n const spent: AiJobStepOutcome = {\n outputs: [],\n usage: result.usage,\n estCostUsd: result.estCostUsd,\n model: result.model,\n stopReason: result.stopReason,\n ...(result.effort ? { effort: result.effort } : {}),\n }\n if (result.status === 'refused') return { ...spent, refused: true }\n if (result.status === 'needs_input') return { ...spent, review: aiDoctrineReview(result) }\n // Each draft the plan decides is named as the plan is kept, before anything is built (AGL-3079).\n const plan: AiJobPlan = aiPlanWithDraftIds(job.kind, {\n ...result.value,\n status: 'proposed',\n labels: aiPlanLabels(result.value, inventory),\n // A Date the Admin SDK stores as a timestamp, like every instant the machine writes.\n proposedAt: now as unknown as AiJobPlan['proposedAt'],\n confirmedAt: null,\n confirmedBy: null,\n key,\n })\n return (\n (await refusalOnKeep(plan, spent)) ?? {\n ...spent,\n plan,\n review: { reason: 'plan', message: AI_JOB_PLAN_REVIEW_COPY, findings: [] },\n }\n )\n }\n}\n\nexport const runAiJobPlanStep = createAiJobPlanStep()\n\n/**\n * Registers the plan step every planned kind runs first, with the least time\n * a plan needs; the plugin's console surface calls it.\n */\nexport function registerAiJobPlan(): void {\n registerAiJobPlanStep(runAiJobPlanStep, { minimumMs: AI_JOB_PLAN_STEP_MINIMUM_MS })\n}\n"],"names":["createHash","AI_BUILD_PLAN_TOOL","aiBuildPlanToolFor","isAiPlanNewRef","AI_TEMPLATE_SUBJECT_DEFINITIONS","aiTemplateShownTokens","AI_JOB_CREATE_KINDS","AI_JOB_CREATE_NOUNS","AI_PAGE_CREATE_KINDS","aiPagePlanShapeRefusal","aiPlanCapabilitiesForJob","aiPlanCapabilityLines","AI_SITE_CREATE_KINDS","aiSitePlanShapeRefusal","aiDoctrineSystemBlocks","runValidatedGeneration","AI_STEP_TIERS","aiPlanEmbedBriefViolations","AI_ROUTING_TABLE","aiModelForStep","readSiteInventory","aiJobAdmissionRefusal","readAiPlanCapabilities","AI_JOB_BRIEF_MAX_CHARS","aiJobStepBudget","aiDoctrineReview","aiUnspentOutcome","aiPlanWithDraftIds","AI_JOBS_COLLECTION","registerAiJobPlanStep","AI_JOB_PLAN_INSTRUCTIONS","text","AI_JOB_PLAN_STEP_BUDGET","tier","maxTokens","AI_JOB_PLAN_STEP_MINIMUM_MS","minimumMs","aiJobPlanMaxTokens","model","AI_JOB_PLAN_REVIEW_COPY","aiJobPlanPrompt","job","capabilities","lines","kind","brief","slice","key","value","Object","entries","inputs","push","String","aiPlanTemplateTokenLines","join","subject","definition","undefined","AI_JOB_PLAN_SCOPES","page","noun","creates","shapeRefusal","site","fromEntries","map","planViolations","scope","plan","message","rule","code","aiPlanLabels","inventory","names","Map","rows","components","layouts","templates","forms","datasets","collections","screens","row","set","id","name","refs","reuse","entry","create","duplicateOf","flatMap","screen","layout","template","sections","section","uses","labels","ref","get","AI_PLAN_REUSE_WINDOW_MS","AI_PLAN_REUSE_CANDIDATES","aiJobPlanKey","input","digest","part","hostId","prompt","system","block","JSON","stringify","tool","update","length","UNREUSABLE","planMillis","at","proposedAt","Date","getTime","toMillis","aiReusablePlan","candidates","context","floor","now","best","bestAt","candidate","jobId","includes","status","findAiJobsByPlanKey","orgId","firestore","snapshot","collection","doc","where","limit","docs","data","readAiJobPlanCapabilities","org","createAiJobPlanStep","deps","readInventory","findPlansByKey","readCapabilities","admissionRefusal","signal","modelFor","orgDocument","workspace","Promise","all","resolve","route","refusalOnKeep","outcome","refusal","uid","createdBy","error","console","$id","failure","resolved","records","Boolean","dataset","allowed","instructions","reused","confirmedAt","confirmedBy","reusedFrom","unspent","review","reason","findings","result","step","messages","role","content","thinking","effort","extend","spent","outputs","usage","estCostUsd","stopReason","refused","runAiJobPlanStep","registerAiJobPlan"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-plan-step.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { createHash } from 'node:crypto'\nimport type { AglynOrgBilling } from '@aglyn/aglyn/foundation/definitions/org-billing.types'\nimport {\n AI_BUILD_PLAN_TOOL,\n aiBuildPlanToolFor,\n isAiPlanNewRef,\n type AiBuildPlan,\n} from '../model/ai-build-plan'\nimport type { AiJob, AiJobKind, AiJobPlan, AiJobStatus } from '../model/ai-jobs.types'\nimport {\n AI_TEMPLATE_SUBJECT_DEFINITIONS,\n aiTemplateShownTokens,\n type AiTemplateSubject,\n} from '../model/ai-template-subjects'\nimport { AI_JOB_CREATE_KINDS, AI_JOB_CREATE_NOUNS } from '../model/ai-job-creations'\nimport { AI_PAGE_CREATE_KINDS, aiPagePlanShapeRefusal } from '../model/ai-page-job'\nimport {\n aiPlanCapabilitiesForJob,\n aiPlanCapabilityLines,\n type AiPlanCapabilities,\n type AiPlanJobScope,\n} from '../model/ai-plan-capabilities'\nimport type { AiSiteInventory } from '../model/ai-site-inventory'\nimport {\n AI_FREE_SITE_WORST_CASE_CREDITS,\n AI_SITE_CREATE_KINDS,\n AI_SITE_FREE_PAGES,\n AI_SITE_PLAN_MAX_TOKENS,\n aiFreeSiteSectionsWithin,\n aiSitePlanShapeRefusal,\n} from '../model/ai-site-job'\nimport { aiPlanFailureCopy, aiPlanRetryRefusal } from '../model/ai-job-failure-copy'\nimport { FREE_AI_TASTE_CREDITS_PER_MONTH } from '../plan-entitlements'\nimport { aiDoctrineSystemBlocks, runValidatedGeneration } from '../runtime/ai-doctrine'\nimport { AI_STEP_TIERS, aiCatalogEntry, aiDefaultModelFor } from '../providers/catalog'\nimport {\n AI_FREE_PAGE_WORST_CASE_CREDITS,\n aiPlanEmbedBriefViolations,\n type AiDoctrineViolation,\n} from '../runtime/ai-doctrine-validators'\nimport { assistCreditsFromUsd } from '../usage/assist-credits'\nimport { ASSIST_RETURNED_USD_FIELD, assistSpendAfterReturnsUsd } from '../usage/assist-credit-returns'\nimport { freeAccountUsageRef, freeAssistAccount, type AssistMeteredOrg } from '../usage/assist-free-taste'\nimport { AI_ROUTING_TABLE, aiModelForStep } from '../providers/routing'\nimport type { AiTool } from '../providers/contract'\nimport type { AiSystemBlock } from '../runtime/ai-runtime'\nimport { readSiteInventory } from '../runtime/site-inventory'\nimport { aiJobAdmissionRefusal } from './ai-job-admission'\nimport { readAiPlanCapabilities } from './ai-job-drafts'\nimport {\n AI_JOB_BRIEF_MAX_CHARS,\n type AiJobStepOutcome,\n type AiJobStepRunner,\n} from './ai-job-text-step'\nimport { aiJobStepBudget } from './ai-job-budget'\nimport { aiDoctrineReview, aiUnspentOutcome } from './ai-job-generation'\nimport { aiPlanWithDraftIds } from './ai-job-draft-ids'\nimport { AI_JOBS_COLLECTION, registerAiJobPlanStep } from './ai-jobs'\n\n/**\n * The plan step (AGL-2935): the first step of every job that builds site\n * structure. Before a node is generated, the model answers with a typed plan\n * — what the site already has that the job reuses, what it creates and why,\n * and the screens it builds from them — held to the doctrine's plan rules\n * against the site's inventory and re-asked once when it breaks one.\n *\n * A plan that passes is kept on the job, and the job stops for review: the\n * member reads what it will build before a credit is spent building it, and\n * confirms through the resume door. A plan that still breaks a rule on its\n * re-ask stops the job the same way, with the rules named. Either way the\n * step writes nothing itself; the machine records the plan, the spend and\n * the stop, as it records every step.\n *\n * ── Told what it may create, refused what it cannot build (AGL-3030) ─────\n *\n * Before it asks, the step reads what the job may create on its site: the\n * workspace's plan, the site's counts against its limits, and — for a kind\n * that builds only some plans — what the job builds itself. The user turn\n * states it, the plan rules refuse a creation outside it with the one re-ask,\n * and where the workspace keeps no reusable components they accept the page\n * built inline. A plan the confirm door would still refuse — the kind's own\n * admission, handed the plan, as the resume door hands it — is refused HERE,\n * before a member is shown a Confirm: the job fails with the door's sentence,\n * its plan is not kept, and it holds nothing.\n */\n\n/** The plan step's own instructions, cached after the doctrine. */\nexport const AI_JOB_PLAN_INSTRUCTIONS: readonly AiSystemBlock[] = [\n {\n text:\n 'You plan what a website builder job will build, before anything is built. You are given the kind of job and the brief from the person who owns the site. ' +\n 'Answer with submit_build_plan: what the site inventory already has that the job reuses, what it must create and why nothing listed will do, and each page it builds (the screens list) with its layout, template, slug, search title, search description and sections from top to bottom. ' +\n 'Refer to inventory records by id and to what the plan creates as new:<name>. Plan only what the brief asks for: a component, layout, template, form or email job plans no pages unless the brief asks for them, and a page job plans exactly one page.',\n },\n]\n\n/**\n * The plan step's time (AGL-3026, AGL-3036): its two inventory-lookup rounds,\n * its answer and its re-ask at the routing table's ceiling for `job.plan`, on\n * the tier that step kind is served from, with the step's reads and writes,\n * at the rates `ai-job-budget.ts` assumes — fitted to what a beat can give a\n * step, so the served tier asks a little under the routing ceiling.\n */\nexport const AI_JOB_PLAN_STEP_BUDGET = aiJobStepBudget({\n tier: AI_STEP_TIERS['job.plan'],\n maxTokens: AI_ROUTING_TABLE['job.plan'].maxTokens,\n})\n\n/**\n * The least time a plan needs before it starts. Registered with the step, so\n * an inline door never starts a plan. A plan thinks before it answers and\n * routinely runs past an inline door's budget, and a provider call that\n * budget cuts off is still generated and billed upstream while the meter\n * records nothing. The beat starts a plan only with this much of its own\n * budget left, and a spec holds it inside that budget.\n */\nexport const AI_JOB_PLAN_STEP_MINIMUM_MS = AI_JOB_PLAN_STEP_BUDGET.minimumMs\n\n/**\n * The plan's answer ceiling on the model a job runs: the most whose worst\n * case fits the least time the step registered, and never more than the\n * routing table's. A model the catalog does not know is planned at the\n * slowest.\n */\nexport function aiJobPlanMaxTokens(model: string): number {\n return AI_JOB_PLAN_STEP_BUDGET.maxTokens(model)\n}\n\n/** What the member reads while a plan waits for them. */\nexport const AI_JOB_PLAN_REVIEW_COPY =\n 'The plan is ready. Review what the job will reuse and create, then confirm it to build.'\n\n/**\n * The job as the plan step's user turn: its kind, its brief, its scalar\n * inputs and, where the job read them, what it may create on its site\n * (AGL-3030). Per workspace and per site, which is why they ride here and\n * never in a system block the platform's cache entry is keyed on.\n */\nexport function aiJobPlanPrompt(\n job: Pick<AiJob, 'kind' | 'brief' | 'inputs'>,\n capabilities: AiPlanCapabilities | null = null,\n inventory: AiSiteInventory | null = null,\n): string {\n const lines = [`Job kind: ${job.kind}`, `Brief: ${job.brief.slice(0, AI_JOB_BRIEF_MAX_CHARS)}`]\n for (const [key, value] of Object.entries(job.inputs ?? {})) {\n if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {\n lines.push(`${key}: ${String(value)}`)\n }\n }\n if (capabilities) lines.push(...aiPlanCapabilityLines(capabilities))\n lines.push(...aiPlanTemplateTokenLines(job))\n lines.push(...aiPlanSiteLines(job, inventory, capabilities))\n return lines.join('\\n')\n}\n\n/**\n * What a SITE job's plan is told beside the inventory (AGL-3594): the pages\n * the site already has and that their addresses are taken, which one — the\n * untouched starter home — a planned home page at `/` replaces, the Free page\n * cap and its guidance, and that a plan is an outline.\n *\n * A site job only. A new site is born with a published home page at `/`\n * (AGL-3497), and a plan that did not know it could replace it planned its own\n * home there and was refused for the clash on every new site. Kept off a page\n * job's turn, whose length the Free page's wall is proven at.\n */\nexport function aiPlanSiteLines(\n job: Pick<AiJob, 'kind'>,\n inventory: AiSiteInventory | null,\n capabilities: AiPlanCapabilities | null,\n): string[] {\n if (job.kind !== 'site') return []\n const lines: string[] = []\n const pages = (inventory?.screens ?? []).filter((screen) => !screen.template)\n const starter = pages.find((screen) => screen.replaceable)\n if (pages.length) {\n lines.push('Pages the site already has; their addresses are taken unless a line says otherwise:')\n for (const page of pages) {\n lines.push(\n page.replaceable\n ? `- ${page.name} at ${page.slug}: the starter home page the site was created with. Plan this site's home page at / and it replaces this one.`\n : `- ${page.name} at ${page.slug}`,\n )\n }\n if (!starter && pages.some((page) => page.slug.trim() === '/' || page.slug.trim() === '')) {\n lines.push(\"The home page at / is the owner's own and stays: give every page you plan another address.\")\n }\n }\n const cap = capabilities?.freeSitePages\n if (cap !== undefined) {\n const sections = aiFreeSiteSectionsWithin(\n { layouts: (inventory?.layouts.length ?? 0) ? 0 : 1, pages: cap },\n FREE_AI_TASTE_CREDITS_PER_MONTH,\n )\n lines.push(\n `This is a Free workspace: plan at most ${cap} ${cap === 1 ? 'page' : 'pages'} — the home page at / and the one page the brief most needs, such as services, booking or contact — with at most ${sections} sections across them. Put the contact form on one of them.`,\n )\n }\n lines.push(\n \"Keep the plan an outline: each page's title, address, a short search title and description, and its sections named in a few words. The build writes the copy.\",\n )\n return lines\n}\n\n/**\n * What a TEMPLATE job's plan is told its creation's `fields` are (AGL-3143\n * §11): the subject's binding tokens, listed so the planner promises only\n * what the page can fill and the build can be held to it\n * (`aiPlanTemplateTokenViolations`).\n *\n * ⛔ It rides the job's own turn and NOT the `fields` description in the plan\n * tool, which every kind's request caches. Written there it read better and\n * cost 40 tokens of the shared prefix — 2,980 to 3,020 — which is one credit\n * of the Free page's 300-credit wall, taking the room it keeps for a re-asked\n * section from 45 credits to 44 and breaking `ai-job-free-page.spec.ts`. A\n * page job must not pay for a sentence about templates.\n *\n * The list is the tokens a reader sees, never the whole catalog (AGL-3143\n * §16). Offered a slug and the publish timestamp, a live plan promised them,\n * and its build kept the promise the only way a slug can be kept: it printed\n * \"entry slug: {{entry.slug}}\" on every article.\n */\nexport function aiPlanTemplateTokenLines(job: Pick<AiJob, 'kind' | 'inputs'>): string[] {\n if (job.kind !== 'template') return []\n const subject = (job.inputs ?? {})['subject']\n const definition =\n typeof subject === 'string'\n ? AI_TEMPLATE_SUBJECT_DEFINITIONS[subject as AiTemplateSubject]\n : undefined\n if (!definition) return []\n return [\n `The template's fields are the binding tokens its page shows a reader, each one of: ${aiTemplateShownTokens(\n definition,\n ).join(', ')}. A link or picture token is shown by the link or picture holding it. Promise only what the page fills.`,\n ]\n}\n\n/** A kind that builds only some plans: the creations it makes, and the shapes it refuses. */\nexport interface AiJobPlanScope extends AiPlanJobScope {\n /** Why a job of the kind cannot build a plan of this shape; `null` when it can. Pure. */\n shapeRefusal: (plan: AiBuildPlan, options?: { freeTaste?: boolean }) => string | null\n}\n\n/**\n * The kinds whose confirm door builds only some plans (AGL-3030). A page job\n * builds one screen and a scaffold four to eight; each builds only the\n * creations its list names. A template, layout, component, form or email job\n * builds its own record and what that record places (AGL-3143 §15,\n * `AI_JOB_CREATE_KINDS`), and holds no plan to a shape.\n */\nexport const AI_JOB_PLAN_SCOPES: Readonly<Partial<Record<AiJobKind, AiJobPlanScope>>> = {\n page: { noun: 'a page job', creates: AI_PAGE_CREATE_KINDS, shapeRefusal: aiPagePlanShapeRefusal },\n site: { noun: 'a site scaffold', creates: AI_SITE_CREATE_KINDS, shapeRefusal: aiSitePlanShapeRefusal },\n ...Object.fromEntries(\n Object.entries(AI_JOB_CREATE_KINDS).map(([kind, creates]) => [\n kind,\n { noun: AI_JOB_CREATE_NOUNS[kind as AiJobKind] as string, creates, shapeRefusal: () => null },\n ]),\n ),\n}\n\n/**\n * What the plan step holds beyond the doctrine's plan rules, as the\n * violations the plan's one re-ask names: the players the plan lists against\n * the brief it answers (AGL-3433), which only this step reads beside the plan,\n * and a kind's shape refusal.\n */\nfunction planViolations(\n scope: AiJobPlanScope | null,\n brief: string,\n freeTaste = false,\n): (plan: AiBuildPlan) => AiDoctrineViolation[] {\n return (plan) => {\n const message = scope?.shapeRefusal(plan, { freeTaste }) ?? null\n return [\n ...aiPlanEmbedBriefViolations(plan, brief),\n ...(message ? [{ rule: null, code: 'plan-job-shape', message }] : []),\n ]\n }\n}\n\n/**\n * The inventory names of every id the plan references, so the proposal reads\n * \"the Services layout\" rather than an id — kept on the plan because the\n * inventory is not read again when the member opens it.\n */\nexport function aiPlanLabels(\n plan: AiBuildPlan,\n inventory: AiSiteInventory | null,\n): Record<string, string> {\n const names = new Map<string, string>()\n for (const rows of [\n inventory?.components,\n inventory?.layouts,\n inventory?.templates,\n inventory?.forms,\n inventory?.datasets,\n inventory?.collections,\n inventory?.screens,\n ]) {\n for (const row of rows ?? []) names.set(row.id, row.name)\n }\n const refs = [\n ...plan.reuse.map((entry) => entry.id),\n ...plan.create.map((entry) => entry.duplicateOf),\n ...plan.screens.flatMap((screen) => [\n screen.layout,\n screen.template,\n screen.duplicateOf,\n ...screen.sections.flatMap((section) => section.uses),\n ]),\n ]\n const labels: Record<string, string> = {}\n for (const ref of refs) {\n if (!ref || isAiPlanNewRef(ref)) continue\n const name = names.get(ref)\n if (name) labels[ref] = name\n }\n return labels\n}\n\n/* ------------------------------------------------------------------------ *\n * Reusing an identical brief's plan\n * ------------------------------------------------------------------------ */\n\n/**\n * How long a plan may be reused for (AGL-2937).\n *\n * The window is what makes reuse safe rather than merely cheap. The key\n * already covers everything the request said — the brief, the inputs, the\n * model and the prompt as rendered, site inventory included — so a plan is\n * reused only when nothing the model was shown has changed. What the key\n * cannot see is the world outside the request: a page published, a component\n * renamed after the inventory was read, a member who meant something\n * different the second time. Fifteen minutes is short enough that the answer\n * is still the one the site would give, and long enough to cover what reuse\n * is actually for — the same brief run twice in a sitting, a job resubmitted\n * after a refused reservation, two members starting the same work.\n */\nexport const AI_PLAN_REUSE_WINDOW_MS = 15 * 60 * 1_000\n\n/** Jobs one lookup reads. More than one can share a key; the newest plan wins. */\nexport const AI_PLAN_REUSE_CANDIDATES = 5\n\n/** A job whose plan might be reused, as the finder reports it. */\nexport interface AiJobPlanCandidate {\n jobId: string\n status: AiJobStatus\n plan: AiJobPlan\n}\n\n/**\n * The digest a plan is reused by: the whole request, never the answer.\n *\n * It hashes what the model was asked and what it was shown — the job's kind\n * and site, the user turn (which carries the trimmed brief and every scalar\n * input), the model that would answer, every rendered system block including\n * the site inventory, and the tool's schema. Two requests that hash the same\n * would have been sent the same bytes to the same model, so the second can\n * keep the first's answer.\n *\n * The version tag is the escape hatch: anything that changes what a key\n * MEANS, rather than what it covers, bumps it and strands every old key\n * harmlessly, since a key nothing matches simply asks the model.\n */\nexport function aiJobPlanKey(input: {\n job: Pick<AiJob, 'kind' | 'hostId'>\n prompt: string\n model: string\n system: readonly AiSystemBlock[]\n /** The plan tool the request offers (AGL-3433); the plain one when absent. */\n tool?: AiTool\n}): string {\n const digest = createHash('sha256')\n for (const part of [\n 'plan.v1',\n input.job.kind,\n input.job.hostId ?? '',\n input.model,\n input.prompt,\n ...input.system.map((block) => block.text),\n JSON.stringify(input.tool ?? AI_BUILD_PLAN_TOOL),\n ]) {\n // Length-prefixed, so two different splits of the same characters cannot\n // collide by running into one another.\n digest.update(`${part.length}:${part}\\u0000`)\n }\n return digest.digest('hex')\n}\n\n/** A job that could not have produced a reusable plan, whatever its key says. */\nconst UNREUSABLE: readonly AiJobStatus[] = ['canceled', 'failed']\n\nfunction planMillis(plan: AiJobPlan): number | null {\n const at = plan.proposedAt as unknown\n if (at instanceof Date) return at.getTime()\n if (at && typeof (at as { toMillis?: unknown }).toMillis === 'function') {\n return (at as { toMillis(): number }).toMillis()\n }\n return null\n}\n\n/**\n * The newest plan worth reusing out of what the finder returned: another\n * job's, still proposed or already confirmed, on a job that was neither\n * canceled nor failed, proposed inside the window.\n *\n * A canceled or failed job is excluded even though its plan may be perfectly\n * good: those are the jobs a member walked away from, and handing their plan\n * to the next one would make a rejected answer look like a fresh one.\n */\nexport function aiReusablePlan(\n candidates: readonly AiJobPlanCandidate[],\n context: { jobId: string; now: Date },\n): AiJobPlanCandidate | null {\n const floor = context.now.getTime() - AI_PLAN_REUSE_WINDOW_MS\n let best: AiJobPlanCandidate | null = null\n let bestAt = -1\n for (const candidate of candidates) {\n if (candidate.jobId === context.jobId) continue\n if (UNREUSABLE.includes(candidate.status)) continue\n if (candidate.plan.status !== 'proposed' && candidate.plan.status !== 'confirmed') continue\n const at = planMillis(candidate.plan)\n if (at === null || at < floor || at > context.now.getTime()) continue\n if (at > bestAt) {\n best = candidate\n bestAt = at\n }\n }\n return best\n}\n\nexport type AiJobPlanFinder = (\n orgId: string,\n key: string,\n firestore?: FirebaseFirestore.Firestore,\n) => Promise<AiJobPlanCandidate[]>\n\n/**\n * The finder the step uses in production: an equality on `plan.key` inside\n * the org's own jobs.\n *\n * One equality on one field, with no ordering beside it, so Firestore's\n * automatic single-field index answers it and no composite index is deployed.\n * The window and the statuses are applied in memory instead, which is what\n * keeps it that way.\n */\nexport const findAiJobsByPlanKey: AiJobPlanFinder = async (orgId, key, firestore) => {\n if (!firestore) return []\n const snapshot = await firestore\n .collection('orgs')\n .doc(orgId)\n .collection(AI_JOBS_COLLECTION)\n .where('plan.key', '==', key)\n .limit(AI_PLAN_REUSE_CANDIDATES)\n .get()\n return snapshot.docs.flatMap((doc) => {\n const data = doc.data() as { status?: AiJobStatus; plan?: AiJobPlan | null }\n return data.plan\n ? [{ jobId: doc.id, status: data.status ?? 'queued', plan: data.plan }]\n : []\n })\n}\n\n/** What a job may create on its site, read for the plan step; `null` for no site. */\nexport type AiPlanCapabilitiesReader = (input: {\n job: AiJob\n org: Partial<AglynOrgBilling> | null\n firestore: FirebaseFirestore.Firestore\n}) => Promise<AiPlanCapabilities | null>\n\n/** The reader the step uses in production: the draft bands' own arithmetic, for the job's site. */\nexport const readAiJobPlanCapabilities: AiPlanCapabilitiesReader = async ({ job, org, firestore }) =>\n job.hostId ? readAiPlanCapabilities(firestore, { hostId: job.hostId, org }) : null\n\nexport interface AiJobPlanStepDeps {\n /** The inventory reader; specs hand in a fake. */\n readInventory?: typeof readSiteInventory\n /** The reuse lookup; specs hand in a fake, and `null` turns reuse off. */\n findPlansByKey?: AiJobPlanFinder | null\n /** What the job may create on its site (AGL-3030); specs and the eval recorder hand in their own. */\n readCapabilities?: AiPlanCapabilitiesReader\n /** The kind's admission, asked of a plan before it is kept; the registry's otherwise. */\n admissionRefusal?: typeof aiJobAdmissionRefusal\n}\n\nexport function createAiJobPlanStep(deps: AiJobPlanStepDeps = {}): AiJobStepRunner {\n const readInventory = deps.readInventory ?? readSiteInventory\n const findPlansByKey =\n deps.findPlansByKey === undefined ? findAiJobsByPlanKey : deps.findPlansByKey\n const readCapabilities = deps.readCapabilities ?? readAiJobPlanCapabilities\n const admissionRefusal = deps.admissionRefusal ?? aiJobAdmissionRefusal\n return async ({ job, now, signal, firestore, modelFor, org: orgDocument }) => {\n const org = (orgDocument ?? null) as Partial<AglynOrgBilling> | null\n const [inventory, workspace] = await Promise.all([\n job.hostId ? readInventory(job.orgId, job.hostId, { firestore }) : Promise.resolve(null),\n readCapabilities({ job, org, firestore }),\n ])\n const scope = AI_JOB_PLAN_SCOPES[job.kind] ?? null\n const capabilities = aiSitePlanCapabilities(\n job,\n workspace ? aiPlanCapabilitiesForJob(workspace, scope) : null,\n )\n const freeTaste = capabilities?.freeTaste === true\n const site = job.kind === 'site'\n // The model switch's answer for this job (AGL-2942): the creator's pick\n // where the plan, the org restriction and the allotment allowlists allow\n // it, and Auto held to those same lists otherwise. Without a resolver the\n // doctrine asks the routing table itself. A Free site plans on its\n // provider's fast tier (AGL-3594), unless the creator picked a model.\n const model = aiSitePlanModel(job, modelFor?.('job.plan'), freeTaste)\n const route = AI_ROUTING_TABLE['job.plan']\n const prompt = aiJobPlanPrompt(job, capabilities, inventory)\n\n /**\n * The confirm door's answer for this plan, asked before the plan is kept\n * (AGL-3030): the kind's own admission, handed the plan as the resume\n * door hands it. A refusal fails the job with the door's sentence; a door\n * that could not answer keeps the plan, since the resume door asks again\n * before anything is built.\n */\n const refusalOnKeep = async (plan: AiJobPlan, outcome: AiJobStepOutcome): Promise<AiJobStepOutcome | null> => {\n let refusal: Awaited<ReturnType<typeof aiJobAdmissionRefusal>> = null\n try {\n refusal = await admissionRefusal(job.kind, {\n firestore,\n orgId: job.orgId,\n hostId: job.hostId ?? null,\n inputs: job.inputs ?? {},\n org,\n plan,\n uid: job.createdBy,\n })\n } catch (error) {\n console.error('ai plan admission failed', { orgId: job.orgId, jobId: job.$id, error })\n }\n return refusal ? { ...outcome, failure: refusal.error } : null\n }\n\n // Reuse before asking (AGL-2937). The key covers the whole request, so a\n // hit is a request that would have been sent the same bytes to the same\n // model; the window covers what the key cannot see. A reused plan spends\n // nothing, so the machine's spent-nothing branch releases the reservation\n // and meters no credit, and the job still stops for the member to confirm\n // — this job's plan is proposed to this member, whatever the last one did\n // with theirs.\n const resolved = model ?? aiModelForStep('job.plan')\n // A brief that asks for a video is offered the plan's list of players\n // (AGL-3433), and a job that may bind a dataset a record template on\n // each screen (AGL-3475).\n const tool = aiBuildPlanToolFor(job.brief, {\n records: Boolean(inventory?.datasets.length) || capabilities?.create.dataset.allowed === true,\n })\n const key = aiJobPlanKey({\n job,\n prompt,\n model: resolved,\n system: aiDoctrineSystemBlocks(inventory, { instructions: AI_JOB_PLAN_INSTRUCTIONS }),\n tool,\n })\n const reused = findPlansByKey\n ? aiReusablePlan(await findPlansByKey(job.orgId, key, firestore), {\n jobId: job.$id,\n now,\n })\n : null\n if (reused) {\n // The reused plan's draft ids name the other job's drafts; this job's are its own.\n const plan: AiJobPlan = aiPlanWithDraftIds(job.kind, {\n ...reused.plan,\n status: 'proposed',\n labels: aiPlanLabels(reused.plan, inventory),\n proposedAt: now as unknown as AiJobPlan['proposedAt'],\n confirmedAt: null,\n confirmedBy: null,\n key,\n reusedFrom: reused.jobId,\n })\n const unspent = aiUnspentOutcome(resolved)\n return (\n (await refusalOnKeep(plan, unspent)) ?? {\n ...unspent,\n plan,\n review: { reason: 'plan', message: AI_JOB_PLAN_REVIEW_COPY, findings: [] },\n }\n )\n }\n\n const result = await runValidatedGeneration('plan', {\n step: 'job.plan',\n ...(model ? { model } : {}),\n instructions: AI_JOB_PLAN_INSTRUCTIONS,\n inventory,\n messages: [{ role: 'user', content: prompt }],\n tool,\n // The routing ceiling, lowered on a tier too slow to look records up,\n // answer and ask again inside the least time the step registered; a\n // scaffold's outline is held to its own, far lower ceiling (AGL-3594).\n maxTokens: aiSitePlanMaxTokens(job, resolved, freeTaste),\n // A scaffold plans an outline without thinking (AGL-3594): the thinking\n // was nearly all of the 13,491 tokens a Free site's plan spent.\n ...(site ? { thinking: 'off' as const } : route.thinking ? { thinking: route.thinking } : {}),\n ...(route.effort ? { effort: route.effort } : {}),\n ...(signal ? { signal } : {}),\n capabilities,\n extend: planViolations(scope, job.brief, freeTaste),\n })\n const spent: AiJobStepOutcome = {\n outputs: [],\n usage: result.usage,\n estCostUsd: result.estCostUsd,\n model: result.model,\n stopReason: result.stopReason,\n ...(result.effort ? { effort: result.effort } : {}),\n }\n if (result.status === 'refused') return { ...spent, refused: true }\n if (result.status === 'needs_input') {\n // Refused by our own checks, after the one re-ask (AGL-3594): the member\n // reads a sentence and an action, staff the rules; the machine gives the\n // credits back (`uncredited`), and on the Free taste Try again says when\n // it cannot work. Whether the give-back was made is the job's to say\n // (`refundedCredits`), so this sentence does not promise it.\n const review = aiDoctrineReview(result)\n const retryRefusal = freeTaste\n ? await aiFreePlanRetryRefusal({ job, org, firestore, now, site })\n : null\n return {\n ...spent,\n uncredited: true,\n review: {\n ...review,\n message: aiPlanFailureCopy({\n kind: job.kind,\n codes: result.violations.map((violation) => violation.code),\n refunded: false,\n }),\n detail: review.message,\n ...(retryRefusal ? { retryRefusal } : {}),\n },\n }\n }\n // Each draft the plan decides is named as the plan is kept, before anything is built (AGL-3079).\n const plan: AiJobPlan = aiPlanWithDraftIds(job.kind, {\n ...result.value,\n status: 'proposed',\n labels: aiPlanLabels(result.value, inventory),\n // A Date the Admin SDK stores as a timestamp, like every instant the machine writes.\n proposedAt: now as unknown as AiJobPlan['proposedAt'],\n confirmedAt: null,\n confirmedBy: null,\n key,\n })\n return (\n (await refusalOnKeep(plan, spent)) ?? {\n ...spent,\n plan,\n review: { reason: 'plan', message: AI_JOB_PLAN_REVIEW_COPY, findings: [] },\n }\n )\n }\n}\n\n/**\n * A site job's capabilities on the Free taste (AGL-3594): its plan holds at\n * most the pages the member asked for within the Free band, which the Free\n * wall then holds it to, and it changes no theme — the site was born with\n * one (AGL-3497), and a palette pass is credits the two pages need. Every\n * other job's capabilities pass through.\n */\nexport function aiSitePlanCapabilities(\n job: Pick<AiJob, 'kind' | 'inputs'>,\n capabilities: AiPlanCapabilities | null,\n): AiPlanCapabilities | null {\n if (job.kind !== 'site' || !capabilities?.freeTaste) return capabilities\n const asked = Number((job.inputs ?? {})['pages'])\n const pages =\n Number.isInteger(asked) && asked >= AI_SITE_FREE_PAGES.min\n ? Math.min(asked, AI_SITE_FREE_PAGES.max)\n : AI_SITE_FREE_PAGES.max\n return {\n ...capabilities,\n freeSitePages: pages,\n create: {\n ...capabilities.create,\n 'theme-change': {\n allowed: false,\n left: null,\n reason: \"a Free workspace's site start keeps the theme the site was created with\",\n },\n },\n }\n}\n\n/**\n * The model a plan runs on (AGL-3594): the resolved one, except a Free site\n * plans on its provider's fast tier — an outline the plan rules hold and\n * re-ask, at a fifth of the balanced tier's output rate. A model the creator\n * picked by name stays theirs.\n */\nexport function aiSitePlanModel(\n job: Pick<AiJob, 'kind' | 'model'>,\n resolved: string | undefined,\n freeTaste: boolean,\n): string | undefined {\n if (job.kind !== 'site' || !freeTaste) return resolved\n const picked = typeof job.model === 'string' ? job.model.trim() : ''\n if (picked && picked !== 'auto') return resolved\n const entry = aiCatalogEntry(resolved ?? aiModelForStep('job.plan'))\n return (entry ? aiDefaultModelFor(entry.provider, 'fast') : undefined) ?? resolved\n}\n\n/** The plan's answer ceiling: the step's own, and a scaffold's outline ceiling under it (AGL-3594). */\nexport function aiSitePlanMaxTokens(\n job: Pick<AiJob, 'kind'>,\n model: string,\n freeTaste: boolean,\n): number {\n const ceiling = aiJobPlanMaxTokens(model)\n if (job.kind !== 'site') return ceiling\n return Math.min(ceiling, freeTaste ? AI_SITE_PLAN_MAX_TOKENS.free : AI_SITE_PLAN_MAX_TOKENS.paid)\n}\n\n/**\n * Why trying a Free plan again cannot work this month, or `null` (AGL-3594):\n * the owner's Free allowance left, read off the account month the meter\n * writes net of give-backs, against what a plan of this kind costs at its\n * worst. Read before this step is metered, and the machine gives a refused\n * plan's credits back, so the figure read is the figure left. A read that fails\n * leaves the button on: the reservation still refuses at the wall.\n */\nasync function aiFreePlanRetryRefusal(input: {\n job: AiJob\n org: Partial<AglynOrgBilling> | null\n firestore: FirebaseFirestore.Firestore\n now: Date\n site: boolean\n}): Promise<string | null> {\n const account = freeAssistAccount(input.org as AssistMeteredOrg | null)\n if (!account?.accountUid) return null\n try {\n // The meter's month key, `YYYY-MM` in UTC (`assistUsageMonth`).\n const month = input.now.toISOString().slice(0, 7)\n const snapshot = await freeAccountUsageRef(input.firestore, account.accountUid, month).get()\n const spent = assistCreditsFromUsd(\n assistSpendAfterReturnsUsd(snapshot.get('estCostUsd'), snapshot.get(ASSIST_RETURNED_USD_FIELD)),\n )\n return aiPlanRetryRefusal({\n creditsLeft: FREE_AI_TASTE_CREDITS_PER_MONTH - spent,\n planCredits: input.site ? AI_FREE_SITE_WORST_CASE_CREDITS.plan : AI_FREE_PAGE_WORST_CASE_CREDITS.plan,\n })\n } catch (error) {\n console.error('ai plan retry allowance read failed', { orgId: input.job.orgId, jobId: input.job.$id, error })\n return null\n }\n}\n\nexport const runAiJobPlanStep = createAiJobPlanStep()\n\n/**\n * Registers the plan step every planned kind runs first, with the least time\n * a plan needs; the plugin's console surface calls it.\n */\nexport function registerAiJobPlan(): void {\n registerAiJobPlanStep(runAiJobPlanStep, { minimumMs: AI_JOB_PLAN_STEP_MINIMUM_MS })\n}\n"],"names":["createHash","AI_BUILD_PLAN_TOOL","aiBuildPlanToolFor","isAiPlanNewRef","AI_TEMPLATE_SUBJECT_DEFINITIONS","aiTemplateShownTokens","AI_JOB_CREATE_KINDS","AI_JOB_CREATE_NOUNS","AI_PAGE_CREATE_KINDS","aiPagePlanShapeRefusal","aiPlanCapabilitiesForJob","aiPlanCapabilityLines","AI_FREE_SITE_WORST_CASE_CREDITS","AI_SITE_CREATE_KINDS","AI_SITE_FREE_PAGES","AI_SITE_PLAN_MAX_TOKENS","aiFreeSiteSectionsWithin","aiSitePlanShapeRefusal","aiPlanFailureCopy","aiPlanRetryRefusal","FREE_AI_TASTE_CREDITS_PER_MONTH","aiDoctrineSystemBlocks","runValidatedGeneration","AI_STEP_TIERS","aiCatalogEntry","aiDefaultModelFor","AI_FREE_PAGE_WORST_CASE_CREDITS","aiPlanEmbedBriefViolations","assistCreditsFromUsd","ASSIST_RETURNED_USD_FIELD","assistSpendAfterReturnsUsd","freeAccountUsageRef","freeAssistAccount","AI_ROUTING_TABLE","aiModelForStep","readSiteInventory","aiJobAdmissionRefusal","readAiPlanCapabilities","AI_JOB_BRIEF_MAX_CHARS","aiJobStepBudget","aiDoctrineReview","aiUnspentOutcome","aiPlanWithDraftIds","AI_JOBS_COLLECTION","registerAiJobPlanStep","AI_JOB_PLAN_INSTRUCTIONS","text","AI_JOB_PLAN_STEP_BUDGET","tier","maxTokens","AI_JOB_PLAN_STEP_MINIMUM_MS","minimumMs","aiJobPlanMaxTokens","model","AI_JOB_PLAN_REVIEW_COPY","aiJobPlanPrompt","job","capabilities","inventory","lines","kind","brief","slice","key","value","Object","entries","inputs","push","String","aiPlanTemplateTokenLines","aiPlanSiteLines","join","pages","screens","filter","screen","template","starter","find","replaceable","length","page","name","slug","some","trim","cap","freeSitePages","undefined","sections","layouts","subject","definition","AI_JOB_PLAN_SCOPES","noun","creates","shapeRefusal","site","fromEntries","map","planViolations","scope","freeTaste","plan","message","rule","code","aiPlanLabels","names","Map","rows","components","templates","forms","datasets","collections","row","set","id","refs","reuse","entry","create","duplicateOf","flatMap","layout","section","uses","labels","ref","get","AI_PLAN_REUSE_WINDOW_MS","AI_PLAN_REUSE_CANDIDATES","aiJobPlanKey","input","digest","part","hostId","prompt","system","block","JSON","stringify","tool","update","UNREUSABLE","planMillis","at","proposedAt","Date","getTime","toMillis","aiReusablePlan","candidates","context","floor","now","best","bestAt","candidate","jobId","includes","status","findAiJobsByPlanKey","orgId","firestore","snapshot","collection","doc","where","limit","docs","data","readAiJobPlanCapabilities","org","createAiJobPlanStep","deps","readInventory","findPlansByKey","readCapabilities","admissionRefusal","signal","modelFor","orgDocument","workspace","Promise","all","resolve","aiSitePlanCapabilities","aiSitePlanModel","route","refusalOnKeep","outcome","refusal","uid","createdBy","error","console","$id","failure","resolved","records","Boolean","dataset","allowed","instructions","reused","confirmedAt","confirmedBy","reusedFrom","unspent","review","reason","findings","result","step","messages","role","content","aiSitePlanMaxTokens","thinking","effort","extend","spent","outputs","usage","estCostUsd","stopReason","refused","retryRefusal","aiFreePlanRetryRefusal","uncredited","codes","violations","violation","refunded","detail","asked","Number","isInteger","min","Math","max","left","picked","provider","ceiling","free","paid","account","accountUid","month","toISOString","creditsLeft","planCredits","runAiJobPlanStep","registerAiJobPlan"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,UAAU,QAAQ,cAAa;AAExC,SACEC,kBAAkB,EAClBC,kBAAkB,EAClBC,cAAc,QAET,4BAAwB;AAE/B,SACEC,+BAA+B,EAC/BC,qBAAqB,QAEhB,mCAA+B;AACtC,SAASC,mBAAmB,EAAEC,mBAAmB,QAAQ,+BAA2B;AACpF,SAASC,oBAAoB,EAAEC,sBAAsB,QAAQ,0BAAsB;AACnF,SACEC,wBAAwB,EACxBC,qBAAqB,QAGhB,mCAA+B;AAEtC,SACEC,+BAA+B,EAC/BC,oBAAoB,EACpBC,kBAAkB,EAClBC,uBAAuB,EACvBC,wBAAwB,EACxBC,sBAAsB,QACjB,0BAAsB;AAC7B,SAASC,iBAAiB,EAAEC,kBAAkB,QAAQ,kCAA8B;AACpF,SAASC,+BAA+B,QAAQ,0BAAsB;AACtE,SAASC,sBAAsB,EAAEC,sBAAsB,QAAQ,4BAAwB;AACvF,SAASC,aAAa,EAAEC,cAAc,EAAEC,iBAAiB,QAAQ,0BAAsB;AACvF,SACEC,+BAA+B,EAC/BC,0BAA0B,QAErB,uCAAmC;AAC1C,SAASC,oBAAoB,QAAQ,6BAAyB;AAC9D,SAASC,yBAAyB,EAAEC,0BAA0B,QAAQ,oCAAgC;AACtG,SAASC,mBAAmB,EAAEC,iBAAiB,QAA+B,gCAA4B;AAC1G,SAASC,gBAAgB,EAAEC,cAAc,QAAQ,0BAAsB;AAGvE,SAASC,iBAAiB,QAAQ,+BAA2B;AAC7D,SAASC,qBAAqB,QAAQ,wBAAoB;AAC1D,SAASC,sBAAsB,QAAQ,qBAAiB;AACxD,SACEC,sBAAsB,QAGjB,wBAAoB;AAC3B,SAASC,eAAe,QAAQ,qBAAiB;AACjD,SAASC,gBAAgB,EAAEC,gBAAgB,QAAQ,yBAAqB;AACxE,SAASC,kBAAkB,QAAQ,wBAAoB;AACvD,SAASC,kBAAkB,EAAEC,qBAAqB,QAAQ,eAAW;AAErE;;;;;;;;;;;;;;;;;;;;;;;;;CAyBC,GAED,iEAAiE,GACjE,OAAO,MAAMC,2BAAqD;IAChE;QACEC,MACE,8JACA,+RACA;IACJ;CACD,CAAA;AAED;;;;;;CAMC,GACD,OAAO,MAAMC,0BAA0BR,gBAAgB;IACrDS,MAAMzB,aAAa,CAAC,WAAW;IAC/B0B,WAAWhB,gBAAgB,CAAC,WAAW,CAACgB,SAAS;AACnD,GAAE;AAEF;;;;;;;CAOC,GACD,OAAO,MAAMC,8BAA8BH,wBAAwBI,SAAS,CAAA;AAE5E;;;;;CAKC,GACD,OAAO,SAASC,mBAAmBC,KAAa;IAC9C,OAAON,wBAAwBE,SAAS,CAACI;AAC3C;AAEA,uDAAuD,GACvD,OAAO,MAAMC,0BACX,0FAAyF;AAE3F;;;;;CAKC,GACD,OAAO,SAASC,gBACdC,GAA6C,EAC7CC,eAA0C,IAAI,EAC9CC,YAAoC,IAAI;QAGEF;IAD1C,MAAMG,QAAQ;QAAC,CAAC,UAAU,EAAEH,IAAII,IAAI,EAAE;QAAE,CAAC,OAAO,EAAEJ,IAAIK,KAAK,CAACC,KAAK,CAAC,GAAGxB,yBAAyB;KAAC;IAC/F,KAAK,MAAM,CAACyB,KAAKC,MAAM,IAAIC,OAAOC,OAAO,EAACV,cAAAA,IAAIW,MAAM,YAAVX,cAAc,CAAC,GAAI;QAC3D,IAAI,OAAOQ,UAAU,YAAY,OAAOA,UAAU,YAAY,OAAOA,UAAU,WAAW;YACxFL,MAAMS,IAAI,CAAC,GAAGL,IAAI,EAAE,EAAEM,OAAOL,QAAQ;QACvC;IACF;IACA,IAAIP,cAAcE,MAAMS,IAAI,IAAIzD,sBAAsB8C;IACtDE,MAAMS,IAAI,IAAIE,yBAAyBd;IACvCG,MAAMS,IAAI,IAAIG,gBAAgBf,KAAKE,WAAWD;IAC9C,OAAOE,MAAMa,IAAI,CAAC;AACpB;AAEA;;;;;;;;;;CAUC,GACD,OAAO,SAASD,gBACdf,GAAwB,EACxBE,SAAiC,EACjCD,YAAuC;;IAEvC,IAAID,IAAII,IAAI,KAAK,QAAQ,OAAO,EAAE;IAClC,MAAMD,QAAkB,EAAE;IAC1B,MAAMc,QAAQ,SAACf,6BAAAA,UAAWgB,OAAO,mBAAI,EAAE,EAAEC,MAAM,CAAC,CAACC,SAAW,CAACA,OAAOC,QAAQ;IAC5E,MAAMC,UAAUL,MAAMM,IAAI,CAAC,CAACH,SAAWA,OAAOI,WAAW;IACzD,IAAIP,MAAMQ,MAAM,EAAE;QAChBtB,MAAMS,IAAI,CAAC;QACX,KAAK,MAAMc,QAAQT,MAAO;YACxBd,MAAMS,IAAI,CACRc,KAAKF,WAAW,GACZ,CAAC,EAAE,EAAEE,KAAKC,IAAI,CAAC,IAAI,EAAED,KAAKE,IAAI,CAAC,4GAA4G,CAAC,GAC5I,CAAC,EAAE,EAAEF,KAAKC,IAAI,CAAC,IAAI,EAAED,KAAKE,IAAI,EAAE;QAExC;QACA,IAAI,CAACN,WAAWL,MAAMY,IAAI,CAAC,CAACH,OAASA,KAAKE,IAAI,CAACE,IAAI,OAAO,OAAOJ,KAAKE,IAAI,CAACE,IAAI,OAAO,KAAK;YACzF3B,MAAMS,IAAI,CAAC;QACb;IACF;IACA,MAAMmB,MAAM9B,gCAAAA,aAAc+B,aAAa;IACvC,IAAID,QAAQE,WAAW;;QACrB,MAAMC,WAAW1E,yBACf;YAAE2E,SAAS,UAACjC,6BAAAA,UAAWiC,OAAO,CAACV,MAAM,oBAAI,KAAK,IAAI;YAAGR,OAAOc;QAAI,GAChEnE;QAEFuC,MAAMS,IAAI,CACR,CAAC,uCAAuC,EAAEmB,IAAI,CAAC,EAAEA,QAAQ,IAAI,SAAS,QAAQ,iHAAiH,EAAEG,SAAS,2DAA2D,CAAC;IAE1Q;IACA/B,MAAMS,IAAI,CACR;IAEF,OAAOT;AACT;AAEA;;;;;;;;;;;;;;;;;CAiBC,GACD,OAAO,SAASW,yBAAyBd,GAAmC;QAEzDA;IADjB,IAAIA,IAAII,IAAI,KAAK,YAAY,OAAO,EAAE;IACtC,MAAMgC,UAAU,EAACpC,cAAAA,IAAIW,MAAM,YAAVX,cAAc,CAAC,EAAE,CAAC,UAAU;IAC7C,MAAMqC,aACJ,OAAOD,YAAY,WACfxF,+BAA+B,CAACwF,QAA6B,GAC7DH;IACN,IAAI,CAACI,YAAY,OAAO,EAAE;IAC1B,OAAO;QACL,CAAC,mFAAmF,EAAExF,sBACpFwF,YACArB,IAAI,CAAC,MAAM,uGAAuG,CAAC;KACtH;AACH;AAQA;;;;;;CAMC,GACD,OAAO,MAAMsB,qBAA2E;IACtFZ,MAAM;QAAEa,MAAM;QAAcC,SAASxF;QAAsByF,cAAcxF;IAAuB;IAChGyF,MAAM;QAAEH,MAAM;QAAmBC,SAASnF;QAAsBoF,cAAchF;IAAuB;GAClGgD,OAAOkC,WAAW,CACnBlC,OAAOC,OAAO,CAAC5D,qBAAqB8F,GAAG,CAAC,CAAC,CAACxC,MAAMoC,QAAQ,GAAK;QAC3DpC;QACA;YAAEmC,MAAMxF,mBAAmB,CAACqD,KAAkB;YAAYoC;YAASC,cAAc,IAAM;QAAK;KAC7F,IAEJ;AAED;;;;;CAKC,GACD,SAASI,eACPC,KAA4B,EAC5BzC,KAAa,EACb0C,YAAY,KAAK;IAEjB,OAAO,CAACC;;QACN,MAAMC,kBAAUH,yBAAAA,MAAOL,YAAY,CAACO,MAAM;YAAED;QAAU,qBAAM;QAC5D,OAAO;eACF5E,2BAA2B6E,MAAM3C;eAChC4C,UAAU;gBAAC;oBAAEC,MAAM;oBAAMC,MAAM;oBAAkBF;gBAAQ;aAAE,GAAG,EAAE;SACrE;IACH;AACF;AAEA;;;;CAIC,GACD,OAAO,SAASG,aACdJ,IAAiB,EACjB9C,SAAiC;IAEjC,MAAMmD,QAAQ,IAAIC;IAClB,KAAK,MAAMC,QAAQ;QACjBrD,6BAAAA,UAAWsD,UAAU;QACrBtD,6BAAAA,UAAWiC,OAAO;QAClBjC,6BAAAA,UAAWuD,SAAS;QACpBvD,6BAAAA,UAAWwD,KAAK;QAChBxD,6BAAAA,UAAWyD,QAAQ;QACnBzD,6BAAAA,UAAW0D,WAAW;QACtB1D,6BAAAA,UAAWgB,OAAO;KACnB,CAAE;QACD,KAAK,MAAM2C,OAAON,eAAAA,OAAQ,EAAE,CAAEF,MAAMS,GAAG,CAACD,IAAIE,EAAE,EAAEF,IAAIlC,IAAI;IAC1D;IACA,MAAMqC,OAAO;WACRhB,KAAKiB,KAAK,CAACrB,GAAG,CAAC,CAACsB,QAAUA,MAAMH,EAAE;WAClCf,KAAKmB,MAAM,CAACvB,GAAG,CAAC,CAACsB,QAAUA,MAAME,WAAW;WAC5CpB,KAAK9B,OAAO,CAACmD,OAAO,CAAC,CAACjD,SAAW;gBAClCA,OAAOkD,MAAM;gBACblD,OAAOC,QAAQ;gBACfD,OAAOgD,WAAW;mBACfhD,OAAOc,QAAQ,CAACmC,OAAO,CAAC,CAACE,UAAYA,QAAQC,IAAI;aACrD;KACF;IACD,MAAMC,SAAiC,CAAC;IACxC,KAAK,MAAMC,OAAOV,KAAM;QACtB,IAAI,CAACU,OAAO/H,eAAe+H,MAAM;QACjC,MAAM/C,OAAO0B,MAAMsB,GAAG,CAACD;QACvB,IAAI/C,MAAM8C,MAAM,CAACC,IAAI,GAAG/C;IAC1B;IACA,OAAO8C;AACT;AAEA;;4EAE4E,GAE5E;;;;;;;;;;;;;CAaC,GACD,OAAO,MAAMG,0BAA0B,KAAK,KAAK,KAAK;AAEtD,gFAAgF,GAChF,OAAO,MAAMC,2BAA2B,EAAC;AASzC;;;;;;;;;;;;;CAaC,GACD,OAAO,SAASC,aAAaC,KAO5B;QAKGA,mBAIeA;IARjB,MAAMC,SAASxI,WAAW;IAC1B,KAAK,MAAMyI,QAAQ;QACjB;QACAF,MAAM/E,GAAG,CAACI,IAAI;SACd2E,oBAAAA,MAAM/E,GAAG,CAACkF,MAAM,YAAhBH,oBAAoB;QACpBA,MAAMlF,KAAK;QACXkF,MAAMI,MAAM;WACTJ,MAAMK,MAAM,CAACxC,GAAG,CAAC,CAACyC,QAAUA,MAAM/F,IAAI;QACzCgG,KAAKC,SAAS,EAACR,cAAAA,MAAMS,IAAI,YAAVT,cAActI;KAC9B,CAAE;QACD,yEAAyE;QACzE,uCAAuC;QACvCuI,OAAOS,MAAM,CAAC,GAAGR,KAAKxD,MAAM,CAAC,CAAC,EAAEwD,KAAK,MAAM,CAAC;IAC9C;IACA,OAAOD,OAAOA,MAAM,CAAC;AACvB;AAEA,+EAA+E,GAC/E,MAAMU,aAAqC;IAAC;IAAY;CAAS;AAEjE,SAASC,WAAW3C,IAAe;IACjC,MAAM4C,KAAK5C,KAAK6C,UAAU;IAC1B,IAAID,cAAcE,MAAM,OAAOF,GAAGG,OAAO;IACzC,IAAIH,MAAM,OAAO,AAACA,GAA8BI,QAAQ,KAAK,YAAY;QACvE,OAAO,AAACJ,GAA8BI,QAAQ;IAChD;IACA,OAAO;AACT;AAEA;;;;;;;;CAQC,GACD,OAAO,SAASC,eACdC,UAAyC,EACzCC,OAAqC;IAErC,MAAMC,QAAQD,QAAQE,GAAG,CAACN,OAAO,KAAKnB;IACtC,IAAI0B,OAAkC;IACtC,IAAIC,SAAS,CAAC;IACd,KAAK,MAAMC,aAAaN,WAAY;QAClC,IAAIM,UAAUC,KAAK,KAAKN,QAAQM,KAAK,EAAE;QACvC,IAAIf,WAAWgB,QAAQ,CAACF,UAAUG,MAAM,GAAG;QAC3C,IAAIH,UAAUxD,IAAI,CAAC2D,MAAM,KAAK,cAAcH,UAAUxD,IAAI,CAAC2D,MAAM,KAAK,aAAa;QACnF,MAAMf,KAAKD,WAAWa,UAAUxD,IAAI;QACpC,IAAI4C,OAAO,QAAQA,KAAKQ,SAASR,KAAKO,QAAQE,GAAG,CAACN,OAAO,IAAI;QAC7D,IAAIH,KAAKW,QAAQ;YACfD,OAAOE;YACPD,SAASX;QACX;IACF;IACA,OAAOU;AACT;AAQA;;;;;;;;CAQC,GACD,OAAO,MAAMM,sBAAuC,OAAOC,OAAOtG,KAAKuG;IACrE,IAAI,CAACA,WAAW,OAAO,EAAE;IACzB,MAAMC,WAAW,MAAMD,UACpBE,UAAU,CAAC,QACXC,GAAG,CAACJ,OACJG,UAAU,CAAC7H,oBACX+H,KAAK,CAAC,YAAY,MAAM3G,KACxB4G,KAAK,CAACtC,0BACNF,GAAG;IACN,OAAOoC,SAASK,IAAI,CAAC/C,OAAO,CAAC,CAAC4C;YAGEI;QAF9B,MAAMA,OAAOJ,IAAII,IAAI;QACrB,OAAOA,KAAKrE,IAAI,GACZ;YAAC;gBAAEyD,OAAOQ,IAAIlD,EAAE;gBAAE4C,MAAM,GAAEU,eAAAA,KAAKV,MAAM,YAAXU,eAAe;gBAAUrE,MAAMqE,KAAKrE,IAAI;YAAC;SAAE,GACrE,EAAE;IACR;AACF,EAAC;AASD,iGAAiG,GACjG,OAAO,MAAMsE,4BAAsD,OAAO,EAAEtH,GAAG,EAAEuH,GAAG,EAAET,SAAS,EAAE,GAC/F9G,IAAIkF,MAAM,GAAGrG,uBAAuBiI,WAAW;QAAE5B,QAAQlF,IAAIkF,MAAM;QAAEqC;IAAI,KAAK,KAAI;AAapF,OAAO,SAASC,oBAAoBC,OAA0B,CAAC,CAAC;QACxCA,qBAGGA,wBACAA;IAJzB,MAAMC,iBAAgBD,sBAAAA,KAAKC,aAAa,YAAlBD,sBAAsB9I;IAC5C,MAAMgJ,iBACJF,KAAKE,cAAc,KAAK1F,YAAY2E,sBAAsBa,KAAKE,cAAc;IAC/E,MAAMC,oBAAmBH,yBAAAA,KAAKG,gBAAgB,YAArBH,yBAAyBH;IAClD,MAAMO,oBAAmBJ,yBAAAA,KAAKI,gBAAgB,YAArBJ,yBAAyB7I;IAClD,OAAO,OAAO,EAAEoB,GAAG,EAAEqG,GAAG,EAAEyB,MAAM,EAAEhB,SAAS,EAAEiB,QAAQ,EAAER,KAAKS,WAAW,EAAE;YAMzD1F,8BA2JX;QAhKH,MAAMiF,MAAOS,sBAAAA,cAAe;QAC5B,MAAM,CAAC9H,WAAW+H,UAAU,GAAG,MAAMC,QAAQC,GAAG,CAAC;YAC/CnI,IAAIkF,MAAM,GAAGwC,cAAc1H,IAAI6G,KAAK,EAAE7G,IAAIkF,MAAM,EAAE;gBAAE4B;YAAU,KAAKoB,QAAQE,OAAO,CAAC;YACnFR,iBAAiB;gBAAE5H;gBAAKuH;gBAAKT;YAAU;SACxC;QACD,MAAMhE,SAAQR,+BAAAA,kBAAkB,CAACtC,IAAII,IAAI,CAAC,YAA5BkC,+BAAgC;QAC9C,MAAMrC,eAAeoI,uBACnBrI,KACAiI,YAAY/K,yBAAyB+K,WAAWnF,SAAS;QAE3D,MAAMC,YAAY9C,CAAAA,gCAAAA,aAAc8C,SAAS,MAAK;QAC9C,MAAML,OAAO1C,IAAII,IAAI,KAAK;QAC1B,wEAAwE;QACxE,yEAAyE;QACzE,0EAA0E;QAC1E,mEAAmE;QACnE,sEAAsE;QACtE,MAAMP,QAAQyI,gBAAgBtI,KAAK+H,4BAAAA,SAAW,aAAahF;QAC3D,MAAMwF,QAAQ9J,gBAAgB,CAAC,WAAW;QAC1C,MAAM0G,SAASpF,gBAAgBC,KAAKC,cAAcC;QAElD;;;;;;KAMC,GACD,MAAMsI,gBAAgB,OAAOxF,MAAiByF;YAC5C,IAAIC,UAA6D;YACjE,IAAI;oBAIQ1I,aACAA;gBAJV0I,UAAU,MAAMb,iBAAiB7H,IAAII,IAAI,EAAE;oBACzC0G;oBACAD,OAAO7G,IAAI6G,KAAK;oBAChB3B,MAAM,GAAElF,cAAAA,IAAIkF,MAAM,YAAVlF,cAAc;oBACtBW,MAAM,GAAEX,cAAAA,IAAIW,MAAM,YAAVX,cAAc,CAAC;oBACvBuH;oBACAvE;oBACA2F,KAAK3I,IAAI4I,SAAS;gBACpB;YACF,EAAE,OAAOC,OAAO;gBACdC,QAAQD,KAAK,CAAC,4BAA4B;oBAAEhC,OAAO7G,IAAI6G,KAAK;oBAAEJ,OAAOzG,IAAI+I,GAAG;oBAAEF;gBAAM;YACtF;YACA,OAAOH,UAAU,aAAKD;gBAASO,SAASN,QAAQG,KAAK;iBAAK;QAC5D;QAEA,yEAAyE;QACzE,wEAAwE;QACxE,yEAAyE;QACzE,0EAA0E;QAC1E,0EAA0E;QAC1E,0EAA0E;QAC1E,eAAe;QACf,MAAMI,WAAWpJ,gBAAAA,QAASnB,eAAe;QACzC,sEAAsE;QACtE,qEAAqE;QACrE,0BAA0B;QAC1B,MAAM8G,OAAO9I,mBAAmBsD,IAAIK,KAAK,EAAE;YACzC6I,SAASC,QAAQjJ,6BAAAA,UAAWyD,QAAQ,CAAClC,MAAM,KAAKxB,CAAAA,gCAAAA,aAAckE,MAAM,CAACiF,OAAO,CAACC,OAAO,MAAK;QAC3F;QACA,MAAM9I,MAAMuE,aAAa;YACvB9E;YACAmF;YACAtF,OAAOoJ;YACP7D,QAAQvH,uBAAuBqC,WAAW;gBAAEoJ,cAAcjK;YAAyB;YACnFmG;QACF;QACA,MAAM+D,SAAS5B,iBACX1B,eAAe,MAAM0B,eAAe3H,IAAI6G,KAAK,EAAEtG,KAAKuG,YAAY;YAC9DL,OAAOzG,IAAI+I,GAAG;YACd1C;QACF,KACA;QACJ,IAAIkD,QAAQ;gBAcP;YAbH,mFAAmF;YACnF,MAAMvG,OAAkB9D,mBAAmBc,IAAII,IAAI,EAAE,aAChDmJ,OAAOvG,IAAI;gBACd2D,QAAQ;gBACRlC,QAAQrB,aAAamG,OAAOvG,IAAI,EAAE9C;gBAClC2F,YAAYQ;gBACZmD,aAAa;gBACbC,aAAa;gBACblJ;gBACAmJ,YAAYH,OAAO9C,KAAK;;YAE1B,MAAMkD,UAAU1K,iBAAiBgK;YACjC,QACG,QAAA,MAAMT,cAAcxF,MAAM2G,oBAA1B,QAAuC,aACnCA;gBACH3G;gBACA4G,QAAQ;oBAAEC,QAAQ;oBAAQ5G,SAASnD;oBAAyBgK,UAAU,EAAE;gBAAC;;QAG/E;QAEA,MAAMC,SAAS,MAAMjM,uBAAuB,QAAQ;YAClDkM,MAAM;WACFnK,QAAQ;YAAEA;QAAM,IAAI,CAAC;YACzByJ,cAAcjK;YACda;YACA+J,UAAU;gBAAC;oBAAEC,MAAM;oBAAQC,SAAShF;gBAAO;aAAE;YAC7CK;YACA,sEAAsE;YACtE,oEAAoE;YACpE,uEAAuE;YACvE/F,WAAW2K,oBAAoBpK,KAAKiJ,UAAUlG;WAG1CL,OAAO;YAAE2H,UAAU;QAAe,IAAI9B,MAAM8B,QAAQ,GAAG;YAAEA,UAAU9B,MAAM8B,QAAQ;QAAC,IAAI,CAAC,GACvF9B,MAAM+B,MAAM,GAAG;YAAEA,QAAQ/B,MAAM+B,MAAM;QAAC,IAAI,CAAC,GAC3CxC,SAAS;YAAEA;QAAO,IAAI,CAAC;YAC3B7H;YACAsK,QAAQ1H,eAAeC,OAAO9C,IAAIK,KAAK,EAAE0C;;QAE3C,MAAMyH,QAA0B;YAC9BC,SAAS,EAAE;YACXC,OAAOX,OAAOW,KAAK;YACnBC,YAAYZ,OAAOY,UAAU;YAC7B9K,OAAOkK,OAAOlK,KAAK;YACnB+K,YAAYb,OAAOa,UAAU;WACzBb,OAAOO,MAAM,GAAG;YAAEA,QAAQP,OAAOO,MAAM;QAAC,IAAI,CAAC;QAEnD,IAAIP,OAAOpD,MAAM,KAAK,WAAW,OAAO,aAAK6D;YAAOK,SAAS;;QAC7D,IAAId,OAAOpD,MAAM,KAAK,eAAe;YACnC,yEAAyE;YACzE,yEAAyE;YACzE,yEAAyE;YACzE,qEAAqE;YACrE,6DAA6D;YAC7D,MAAMiD,SAAS5K,iBAAiB+K;YAChC,MAAMe,eAAe/H,YACjB,MAAMgI,uBAAuB;gBAAE/K;gBAAKuH;gBAAKT;gBAAWT;gBAAK3D;YAAK,KAC9D;YACJ,OAAO,aACF8H;gBACHQ,YAAY;gBACZpB,QAAQ,aACHA;oBACH3G,SAASvF,kBAAkB;wBACzB0C,MAAMJ,IAAII,IAAI;wBACd6K,OAAOlB,OAAOmB,UAAU,CAACtI,GAAG,CAAC,CAACuI,YAAcA,UAAUhI,IAAI;wBAC1DiI,UAAU;oBACZ;oBACAC,QAAQzB,OAAO3G,OAAO;mBAClB6H,eAAe;oBAAEA;gBAAa,IAAI,CAAC;;QAG7C;QACA,iGAAiG;QACjG,MAAM9H,OAAkB9D,mBAAmBc,IAAII,IAAI,EAAE,aAChD2J,OAAOvJ,KAAK;YACfmG,QAAQ;YACRlC,QAAQrB,aAAa2G,OAAOvJ,KAAK,EAAEN;YACnC,qFAAqF;YACrF2F,YAAYQ;YACZmD,aAAa;YACbC,aAAa;YACblJ;;QAEF,QACG,OAAA,MAAMiI,cAAcxF,MAAMwH,kBAA1B,OAAqC,aACjCA;YACHxH;YACA4G,QAAQ;gBAAEC,QAAQ;gBAAQ5G,SAASnD;gBAAyBgK,UAAU,EAAE;YAAC;;IAG/E;AACF;AAEA;;;;;;CAMC,GACD,OAAO,SAASzB,uBACdrI,GAAmC,EACnCC,YAAuC;QAGjBD;IADtB,IAAIA,IAAII,IAAI,KAAK,UAAU,EAACH,gCAAAA,aAAc8C,SAAS,GAAE,OAAO9C;IAC5D,MAAMqL,QAAQC,OAAO,EAACvL,cAAAA,IAAIW,MAAM,YAAVX,cAAc,CAAC,EAAE,CAAC,QAAQ;IAChD,MAAMiB,QACJsK,OAAOC,SAAS,CAACF,UAAUA,SAAShO,mBAAmBmO,GAAG,GACtDC,KAAKD,GAAG,CAACH,OAAOhO,mBAAmBqO,GAAG,IACtCrO,mBAAmBqO,GAAG;IAC5B,OAAO,aACF1L;QACH+B,eAAef;QACfkD,QAAQ,aACHlE,aAAakE,MAAM;YACtB,gBAAgB;gBACdkF,SAAS;gBACTuC,MAAM;gBACN/B,QAAQ;YACV;;;AAGN;AAEA;;;;;CAKC,GACD,OAAO,SAASvB,gBACdtI,GAAkC,EAClCiJ,QAA4B,EAC5BlG,SAAkB;QAMVmB;IAJR,IAAIlE,IAAII,IAAI,KAAK,UAAU,CAAC2C,WAAW,OAAOkG;IAC9C,MAAM4C,SAAS,OAAO7L,IAAIH,KAAK,KAAK,WAAWG,IAAIH,KAAK,CAACiC,IAAI,KAAK;IAClE,IAAI+J,UAAUA,WAAW,QAAQ,OAAO5C;IACxC,MAAM/E,QAAQlG,eAAeiL,mBAAAA,WAAYvK,eAAe;IACxD,QAAQwF,OAAAA,QAAQjG,kBAAkBiG,MAAM4H,QAAQ,EAAE,UAAU7J,qBAApDiC,OAAkE+E;AAC5E;AAEA,qGAAqG,GACrG,OAAO,SAASmB,oBACdpK,GAAwB,EACxBH,KAAa,EACbkD,SAAkB;IAElB,MAAMgJ,UAAUnM,mBAAmBC;IACnC,IAAIG,IAAII,IAAI,KAAK,QAAQ,OAAO2L;IAChC,OAAOL,KAAKD,GAAG,CAACM,SAAShJ,YAAYxF,wBAAwByO,IAAI,GAAGzO,wBAAwB0O,IAAI;AAClG;AAEA;;;;;;;CAOC,GACD,eAAelB,uBAAuBhG,KAMrC;IACC,MAAMmH,UAAU1N,kBAAkBuG,MAAMwC,GAAG;IAC3C,IAAI,EAAC2E,2BAAAA,QAASC,UAAU,GAAE,OAAO;IACjC,IAAI;QACF,gEAAgE;QAChE,MAAMC,QAAQrH,MAAMsB,GAAG,CAACgG,WAAW,GAAG/L,KAAK,CAAC,GAAG;QAC/C,MAAMyG,WAAW,MAAMxI,oBAAoBwG,MAAM+B,SAAS,EAAEoF,QAAQC,UAAU,EAAEC,OAAOzH,GAAG;QAC1F,MAAM6F,QAAQpM,qBACZE,2BAA2ByI,SAASpC,GAAG,CAAC,eAAeoC,SAASpC,GAAG,CAACtG;QAEtE,OAAOV,mBAAmB;YACxB2O,aAAa1O,kCAAkC4M;YAC/C+B,aAAaxH,MAAMrC,IAAI,GAAGtF,gCAAgC4F,IAAI,GAAG9E,gCAAgC8E,IAAI;QACvG;IACF,EAAE,OAAO6F,OAAO;QACdC,QAAQD,KAAK,CAAC,uCAAuC;YAAEhC,OAAO9B,MAAM/E,GAAG,CAAC6G,KAAK;YAAEJ,OAAO1B,MAAM/E,GAAG,CAAC+I,GAAG;YAAEF;QAAM;QAC3G,OAAO;IACT;AACF;AAEA,OAAO,MAAM2D,mBAAmBhF,sBAAqB;AAErD;;;CAGC,GACD,OAAO,SAASiF;IACdrN,sBAAsBoN,kBAAkB;QAAE7M,WAAWD;IAA4B;AACnF"}
@@ -207,6 +207,13 @@ export declare function aiSiteUnitJob(job: AiJob, unit: AiSiteUnit, built: Built
207
207
  * a scaffold whose pages nothing can build is not a scaffold.
208
208
  */
209
209
  export declare const aiSiteJobAdmission: AiJobAdmission;
210
+ /** Whether a scaffold's workspace spends the Free taste (AGL-3594); a missing org reads as paid, as the band has always been. */
211
+ export declare function aiSiteFreeTaste(org: object | null | undefined): boolean;
212
+ /**
213
+ * Whether a scaffold drafts its welcome email (AGL-3594): where it was asked
214
+ * for, and never on the Free taste, whose credits its pages need.
215
+ */
216
+ export declare function aiSiteWelcomeEmail(inputs: AiSiteJobInputs, freeTaste: boolean): boolean;
210
217
  /** What one delegated pass came to: the unit's spend as the job records it, and whether the unit is built. */
211
218
  export interface AiJobUnitPass {
212
219
  outcome: AiJobStepOutcome;
@@ -14,9 +14,10 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
14
14
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
15
  * See the License for the specific language governing permissions and
16
16
  * limitations under the License.
17
- */ import { resolveOrgIdForHost } from "@aglyn/tenant-data-admin/server/organizations";
17
+ */ import { resolveEffectivePlan } from "@aglyn/aglyn/app-utils/plan-entitlements";
18
+ import { resolveOrgIdForHost } from "@aglyn/tenant-data-admin/server/organizations";
18
19
  import { isAiPlanNewRef } from "../model/ai-build-plan.js";
19
- import { AI_SITE_EMAIL_TYPE, AI_SITE_MAX_SECTIONS, AI_SITE_PAGES, aiSitePlanRefusal, aiSiteSubmissions, aiSiteWords, parseAiSiteJobInputs } from "../model/ai-site-job.js";
20
+ import { AI_SITE_EMAIL_TYPE, AI_SITE_MAX_SECTIONS, AI_SITE_PAGES, aiSitePagesRefusal, aiSitePlanRefusal, aiSiteSubmissions, aiSiteWords, parseAiSiteJobInputs } from "../model/ai-site-job.js";
20
21
  import { AI_SITE_SEO_OUTPUT_ID, aiSiteSeoProposalForInputs } from "../model/ai-site-start-seo.js";
21
22
  import { aiModelForStep } from "../providers/routing.js";
22
23
  import { registerAiJobAdmission } from "./ai-job-admission.js";
@@ -379,6 +380,23 @@ export const AI_SITE_NO_PLAN_COPY = 'This site job has no confirmed plan to buil
379
380
  status: 400,
380
381
  error: inputs
381
382
  };
383
+ // The workspace's own page band (AGL-3594): one or two pages on the Free
384
+ // taste, four to eight on a paid plan.
385
+ const freeTaste = aiSiteFreeTaste(context.org);
386
+ const pages = aiSitePagesRefusal(inputs.pages, freeTaste);
387
+ if (pages) return {
388
+ status: 400,
389
+ error: pages
390
+ };
391
+ if (context.plan) {
392
+ const shape = aiSitePlanRefusal(context.plan, {
393
+ freeTaste
394
+ });
395
+ if (shape) return {
396
+ status: 400,
397
+ error: shape
398
+ };
399
+ }
382
400
  if (!context.hostId) {
383
401
  return {
384
402
  status: 400,
@@ -398,6 +416,15 @@ export const AI_SITE_NO_PLAN_COPY = 'This site job has no confirmed plan to buil
398
416
  }
399
417
  return null;
400
418
  };
419
+ /** Whether a scaffold's workspace spends the Free taste (AGL-3594); a missing org reads as paid, as the band has always been. */ export function aiSiteFreeTaste(org) {
420
+ return Boolean(org) && resolveEffectivePlan(org) === 'free';
421
+ }
422
+ /**
423
+ * Whether a scaffold drafts its welcome email (AGL-3594): where it was asked
424
+ * for, and never on the Free taste, whose credits its pages need.
425
+ */ export function aiSiteWelcomeEmail(inputs, freeTaste) {
426
+ return inputs.welcomeEmail && !freeTaste;
427
+ }
401
428
  /**
402
429
  * One delegated pass (AGL-2911, AGL-3031): the unit handed to the runner of
403
430
  * the kind that owns it, under the job derived for it, and its outcome as the
@@ -466,7 +493,10 @@ export function createAiJobSiteStep(deps = {}) {
466
493
  });
467
494
  // The resume door refuses such a plan at confirmation once it can read
468
495
  // one; a plan confirmed before a site changed still stops here, unspent.
469
- const refusal = aiSitePlanRefusal(plan);
496
+ const freeTaste = aiSiteFreeTaste(context.org);
497
+ const refusal = aiSitePlanRefusal(plan, {
498
+ freeTaste
499
+ });
470
500
  if (refusal) return _extends({}, aiUnspentOutcome(model), {
471
501
  failure: refusal
472
502
  });
@@ -477,7 +507,7 @@ export function createAiJobSiteStep(deps = {}) {
477
507
  // A kind this deployment has not loaded is not among the units at all, so
478
508
  // a scaffold owes only what something can build.
479
509
  const units = aiSiteJobUnits(plan, {
480
- welcomeEmail: inputs.welcomeEmail
510
+ welcomeEmail: aiSiteWelcomeEmail(inputs, freeTaste)
481
511
  }).filter((unit)=>runnerFor(unit.jobKind));
482
512
  const outputs = (_job_outputs = job.outputs) != null ? _job_outputs : [];
483
513
  const pending = aiSitePendingUnits(units, outputs);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-site-step.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { resolveOrgIdForHost } from '@aglyn/tenant-data-admin/server/organizations'\nimport {\n isAiPlanNewRef,\n type AiBuildPlanCreate,\n type AiBuildPlanCreateKind,\n type AiBuildPlanScreen,\n} from '../model/ai-build-plan'\nimport type {\n AiJob,\n AiJobKind,\n AiJobOutput,\n AiJobOutputResource,\n AiJobPlan,\n} from '../model/ai-jobs.types'\nimport {\n AI_SITE_EMAIL_TYPE,\n AI_SITE_MAX_SECTIONS,\n AI_SITE_PAGES,\n aiSitePlanRefusal,\n aiSiteSubmissions,\n aiSiteWords,\n parseAiSiteJobInputs,\n type AiSiteJobInputs,\n} from '../model/ai-site-job'\nimport {\n AI_SITE_SEO_OUTPUT_ID,\n aiSiteSeoProposalForInputs,\n} from '../model/ai-site-start-seo'\nimport { aiModelForStep } from '../providers/routing'\nimport { registerAiJobAdmission, type AiJobAdmission } from './ai-job-admission'\nimport { aiRecordedJobDraftId } from './ai-job-draft-ids'\nimport { aiConfirmedPlan, aiUnspentOutcome } from './ai-job-generation'\nimport {\n AI_JOB_BRIEF_MAX_CHARS,\n type AiJobStepContext,\n type AiJobStepOutcome,\n type AiJobStepRunner,\n} from './ai-job-text-step'\nimport { AI_JOB_PAGE_STEP_MINIMUM_MS } from './ai-job-page-budget'\nimport {\n aiJobStepRunMinimumMs,\n aiJobStepRunnerFor,\n registerAiJobStep,\n registerAiJobStepPasses,\n} from './ai-jobs'\n\n/**\n * The site scaffold (AGL-2911): the generation step of a `site` job, run once\n * a member confirmed the plan their brief produced.\n *\n * ── It builds nothing itself ─────────────────────────────────────────────\n *\n * A scaffold is a whole site: a palette suggestion, the layout its pages\n * render inside, the contact form they place, four to eight pages with their\n * search listings, and a welcome email. Every one of those is already a job\n * kind with a step of its own, so this step generates nothing and asks no\n * model anything. It works through the confirmed plan one UNIT at a time and\n * hands each unit to the runner registered for the kind that owns it — the\n * theme step for the palette, the page step for a page — under a job of that\n * kind derived from this one.\n *\n * That is why the scaffold cannot drift from the thing it scaffolds: a page\n * it builds is a page job's page, held to the same doctrine, written by the\n * same draft writer, left unpublished by the same rule. A kind whose step\n * this deployment has not loaded is simply not among the units, so nothing\n * here knows which issues have landed.\n *\n * ── One unit a pass ──────────────────────────────────────────────────────\n *\n * The step runs ONE delegated pass and asks the machine to continue, so every\n * pass is one reservation, one provider exchange and one recorded spend, and\n * the org's monthly ceiling binds a scaffold exactly as it binds a chat turn.\n * Credits running out mid-scaffold is therefore the machine's own park: the\n * job waits as `needs_input` with the meter's words, and the beat resumes it\n * where it stopped once the workspace's standing has changed. A pass needs the\n * time its unit's own step needs (`aiSiteJobRunMinimumMs`), so the beat starts\n * a palette change only with a palette change's time left.\n *\n * Where it stopped is read from the job's OUTPUTS rather than kept anywhere:\n * each unit reports exactly one output when it completes, so the units\n * already represented there are the ones already built. A pass interrupted\n * between its work and its record repeats a unit that then finds its own\n * draft and reports it, which is what every step's draft id already gives.\n *\n * ── Never published ──────────────────────────────────────────────────────\n *\n * Nothing here publishes, routes or sends: each delegated step writes the\n * unpublished draft it already wrote for a job of its own kind, and the\n * palette and navigation travel as proposals a member applies.\n *\n * ── The page job builds its creations the same way (AGL-3031) ────────────\n *\n * A page job whose plan creates a layout, forms or components builds them\n * before its page through this same machinery: the units a creation implies\n * (`aiCreationUnit`), where the job stands read from its outputs\n * (`aiSitePendingUnits`), one delegated pass a unit (`aiRunJobUnit`), and the\n * `new:<name>` references resolved to what was built (`aiSiteBuiltRefs`,\n * `aiSiteUnitJob`). There is one loop, and it is the machine's.\n */\n\n/** What a scaffold's units are, in the order it builds them; a component is a page job's. */\nexport type AiSiteUnitKind = 'theme' | 'layout' | 'form' | 'component' | 'page' | 'email'\n\nexport interface AiSiteUnit {\n kind: AiSiteUnitKind\n /** The job kind whose registered runner builds it. */\n jobKind: AiJobKind\n /** The resource the unit reports when it completes. */\n resource: AiJobOutputResource\n /** Stable within the job: what the unit's derived job and draft are addressed by. */\n slot: string\n /** The plan screen a page unit builds. */\n screen?: AiBuildPlanScreen\n /** The creation a theme, layout or form unit builds. */\n creation?: AiBuildPlanCreate\n /** What the unit is, for the derived job's brief. */\n label: string\n}\n\n/** The job kind and output resource each unit is built through. */\nconst UNIT_KINDS: Record<\n AiSiteUnitKind,\n { jobKind: AiJobKind; resource: AiJobOutputResource }\n> = {\n theme: { jobKind: 'theme', resource: 'theme' },\n layout: { jobKind: 'layout', resource: 'layout' },\n form: { jobKind: 'form', resource: 'form' },\n component: { jobKind: 'component', resource: 'reusableComponent' },\n page: { jobKind: 'page', resource: 'screen' },\n email: { jobKind: 'email', resource: 'emailScreen' },\n}\n\n/** The unit kind a creation is built as, where a unit builds it. */\nconst CREATION_UNIT_KINDS: Partial<Record<AiBuildPlanCreateKind, AiSiteUnitKind>> = {\n 'theme-change': 'theme',\n layout: 'layout',\n form: 'form',\n component: 'component',\n}\n\n/**\n * The unit one creation is built as, under the slot its derived job and draft\n * are addressed by; `null` for a creation no unit builds.\n */\nexport function aiCreationUnit(creation: AiBuildPlanCreate, slot: string): AiSiteUnit | null {\n const kind = CREATION_UNIT_KINDS[creation.kind]\n if (!kind) return null\n return { kind, ...UNIT_KINDS[kind], slot, creation, label: creation.name }\n}\n\n/** The creation kinds a unit is built from, by unit kind. */\nconst CREATION_UNITS: Array<{\n unit: AiSiteUnitKind\n create: AiBuildPlanCreateKind\n}> = [\n { unit: 'theme', create: 'theme-change' },\n { unit: 'layout', create: 'layout' },\n { unit: 'form', create: 'form' },\n]\n\nexport const AI_SITE_NO_PLAN_COPY =\n 'This site job has no confirmed plan to build.'\n\n/** What a scaffold answers where the step that builds a page is not loaded. */\nexport const AI_SITE_NO_PAGE_STEP_COPY =\n 'Building a whole site is not available yet.'\n\n/** A unit that finished without reporting what it built; the scaffold cannot go on. */\nexport const AI_SITE_UNIT_EMPTY_COPY =\n 'Part of this site could not be built. Try the site brief again.'\n\n/**\n * The most passes a scaffold's step may take: the largest plan it admits —\n * eight pages of eight sections, its three creations and a welcome email —\n * with nothing to spare, so a runner that never finishes is still bounded\n * while a real site is not.\n */\nexport const AI_SITE_MAX_PASSES =\n AI_SITE_PAGES.max * (AI_SITE_MAX_SECTIONS + 1) + CREATION_UNITS.length + 1\n\n/**\n * The units a plan implies, in build order: the palette first, because a\n * member reads it while the pages are still building; then the layout every\n * page renders inside and the form they place, because a page binds both by\n * id and so needs them to exist; then the pages; then the welcome email,\n * which is about the site rather than part of it.\n */\nexport function aiSiteJobUnits(\n plan: Pick<AiJobPlan, 'create' | 'screens'>,\n options: { welcomeEmail?: boolean } = {},\n): AiSiteUnit[] {\n const units: AiSiteUnit[] = []\n for (const { unit, create } of CREATION_UNITS) {\n const creation = plan.create.find((entry) => entry.kind === create)\n if (!creation) continue\n units.push({\n kind: unit,\n ...UNIT_KINDS[unit],\n slot: unit[0],\n creation,\n label: creation.name,\n })\n }\n plan.screens.forEach((screen, index) => {\n units.push({\n kind: 'page',\n ...UNIT_KINDS.page,\n slot: `p${index}`,\n screen,\n label: screen.title,\n })\n })\n if (options.welcomeEmail) {\n units.push({\n kind: 'email',\n ...UNIT_KINDS.email,\n slot: 'e',\n label: 'Welcome email',\n })\n }\n return units\n}\n\n/**\n * The units a scaffold still owes after the outputs recorded so far. Each\n * unit reports one output of its own resource when it completes, so the\n * outputs already recorded are consumed against the units in build order.\n */\nexport function aiSitePendingUnits(\n units: readonly AiSiteUnit[],\n outputs: readonly Pick<AiJobOutput, 'resource'>[],\n): AiSiteUnit[] {\n const done = new Map<AiJobOutputResource, number>()\n for (const output of outputs) {\n done.set(output.resource, (done.get(output.resource) ?? 0) + 1)\n }\n return units.filter((unit) => {\n const left = done.get(unit.resource) ?? 0\n if (left <= 0) return true\n done.set(unit.resource, left - 1)\n return false\n })\n}\n\n/**\n * The site's own search listing as the scaffold reports it (AGL-2918), or\n * nothing when the job already reported one or its inputs describe no site.\n *\n * It costs nothing and asks no model: the values are arithmetic on the\n * answers (`aiSiteSeoProposal`), so they ride out on the first pass that\n * builds anything rather than waiting for a pass of their own. Its resource\n * is `seo`, which no unit reports, so it cannot be mistaken for a unit's\n * output by `aiSitePendingUnits` and cannot move where the scaffold thinks\n * it is.\n */\nexport function aiSiteSeoOutputs(job: AiJob): AiJobOutput[] {\n const outputs = job.outputs ?? []\n if (outputs.some((output) => output.resource === 'seo' && output.id === AI_SITE_SEO_OUTPUT_ID)) {\n return []\n }\n const proposal = aiSiteSeoProposalForInputs(job.inputs)\n if (!proposal) return []\n return [\n {\n resource: 'seo',\n id: AI_SITE_SEO_OUTPUT_ID,\n hostId: job.hostId ?? null,\n label: 'The site’s search title and description',\n proposal: proposal as unknown as Record<string, unknown>,\n },\n ]\n}\n\n/** A record an earlier unit created, as a later one references it. */\nexport interface AiSiteBuiltRef {\n id: string\n label: string\n /** Only what becomes a record a page can name; a palette change is not one. */\n kind: 'layout' | 'form' | 'component'\n}\n\n/**\n * What the scaffold has already created, as the plan's `new:<name>`\n * references resolve to: the id of the record each completed creation wrote.\n * A page unit is told about the layout and the form by id, exactly as a page\n * job is told about records the site already had.\n */\nexport function aiSiteBuiltRefs(\n units: readonly AiSiteUnit[],\n outputs: readonly Pick<AiJobOutput, 'resource' | 'id' | 'label'>[],\n): Map<string, AiSiteBuiltRef> {\n const byResource = new Map<\n AiJobOutputResource,\n Array<Pick<AiJobOutput, 'id' | 'label'>>\n >()\n for (const output of outputs) {\n const rows = byResource.get(output.resource) ?? []\n rows.push(output)\n byResource.set(output.resource, rows)\n }\n const built = new Map<string, AiSiteBuiltRef>()\n for (const unit of units) {\n const rows = byResource.get(unit.resource)\n const row = rows?.shift()\n if (!row || !unit.creation) continue\n // A theme change is a proposal with no record to reference; a layout, a\n // form and a component become ids a page can name.\n if (\n unit.creation.kind !== 'layout' &&\n unit.creation.kind !== 'form' &&\n unit.creation.kind !== 'component'\n ) {\n continue\n }\n built.set(unit.creation.name.toLowerCase(), {\n id: row.id,\n label: row.label || unit.creation.name,\n kind: unit.creation.kind,\n })\n }\n return built\n}\n\ntype BuiltRefs = ReturnType<typeof aiSiteBuiltRefs>\n\n/** A plan reference as the unit's own job reads it: an id the site now has, or nothing. */\nexport function aiSiteResolvedRef(ref: string | null, built: BuiltRefs): string | null {\n if (!ref) return null\n if (!isAiPlanNewRef(ref)) return ref\n return (\n built.get(\n ref\n .slice(ref.indexOf(':') + 1)\n .trim()\n .toLowerCase(),\n )?.id ?? null\n )\n}\n\n/**\n * The site's own words, as every unit's brief carries them.\n *\n * The audience is here rather than left to the brief's prose (AGL-2918). A\n * guided start writes it into the brief itself, but an agency batch's brief\n * is a member's own sentence and may never mention it, and a theme, a layout\n * or a form asked to serve nobody in particular serves nobody in particular.\n */\nexport function aiSiteBriefLines(\n brief: string,\n inputs: AiSiteJobInputs,\n): string[] {\n const lines = [brief.trim()]\n const site = [\n inputs.businessName ? `name: ${inputs.businessName}` : '',\n `business: ${inputs.businessType}`,\n inputs.audience ? `for: ${inputs.audience}` : '',\n inputs.city ? `city: ${inputs.city}` : '',\n inputs.brand ? `brand: ${inputs.brand}` : '',\n ].filter(Boolean)\n lines.push(`Site — ${site.join('; ')}.`)\n return lines\n}\n\n/**\n * What the welcome email is told (AGL-2918), beyond that it is a welcome\n * email.\n *\n * The scaffold's email unit used to carry one sentence, and a yes/no toggle\n * decided whether it ran — so the draft was a welcome email for a business\n * in general, to nobody in particular, about nothing that had happened. The\n * two answers it is missing are who wrote in and what now becomes of what\n * they wrote, and the second of those is the person's own routing answer, so\n * the email and the form it acknowledges cannot say different things.\n *\n * ⛔ A DRAFT either way. Nothing here sends, schedules or enrolls anybody:\n * the email step writes an unpublished email design and stops.\n */\nexport function aiSiteEmailBriefLines(\n inputs: Readonly<Record<string, unknown>> | null | undefined,\n): string[] {\n const lines = [\n 'Write the welcome email this site sends someone who gets in touch.',\n ]\n const { audience } = aiSiteWords(inputs)\n if (audience) lines.push(`Write it to ${audience}.`)\n const submissions = aiSiteSubmissions(inputs)\n if (submissions === 'lead') {\n lines.push('Their message is a sales lead, so say that somebody will be in touch about it.')\n } else if (submissions === 'inbox') {\n lines.push('Their message has been read, so say it arrived and that a reply is coming.')\n }\n return lines\n}\n\n/**\n * The id a unit's job is named by, and so the id its draft is written under\n * (AGL-3079): the id the plan entry it builds recorded, or for the welcome\n * email the id recorded on this job's step. A unit that writes no draft — a\n * palette change — and a unit whose plan recorded no id are named by this\n * job's id and the unit's slot, the name every draft of such a plan has.\n */\nexport function aiSiteUnitJobId(job: Pick<AiJob, '$id' | 'steps'>, unit: AiSiteUnit): string {\n const recorded =\n unit.creation?.id ?? unit.screen?.id ?? (unit.kind === 'email' ? aiRecordedJobDraftId(job, 'email') : null)\n return recorded || `${job.$id}-${unit.slot}`\n}\n\n/**\n * The job a unit is built under: this job's org, site, creator and model,\n * the kind that owns the unit, and a plan narrowed to the unit alone.\n *\n * The derived job carries NO steps and NO outputs of the scaffold's: a page\n * job's step reads its own draft and its own plan, and what the other units\n * produced is not its business. Its `$id` is the id the unit's draft is\n * written under (`aiSiteUnitJobId`), which is what a step with no id of its\n * own recorded names its draft by, so a unit re-run after its write finds its\n * own draft rather than writing a second.\n *\n * A creation unit reuses what the plan reuses less what the plan's screens\n * place themselves (AGL-3031): a component a page's section places is the\n * page's to place, and a layout or a card told to place it would be refused\n * for leaving it out.\n */\nexport function aiSiteUnitJob(\n job: AiJob,\n unit: AiSiteUnit,\n built: BuiltRefs,\n): AiJob {\n const plan = job.plan as AiJobPlan\n const labels = { ...(plan.labels ?? {}) }\n for (const entry of built.values()) labels[entry.id] = entry.label\n const inputs = parseAiSiteJobInputs(job.inputs)\n const brief =\n typeof inputs === 'string'\n ? [job.brief]\n : aiSiteBriefLines(job.brief, inputs)\n const unitPlan: AiJobPlan = {\n ...plan,\n labels,\n reuse: [...plan.reuse],\n create: [],\n screens: [],\n }\n if (unit.creation) {\n const placedByScreens = new Set(\n plan.screens.flatMap((screen) => screen.sections.flatMap((section) => section.uses)),\n )\n unitPlan.reuse = plan.reuse.filter((entry) => !placedByScreens.has(entry.id))\n unitPlan.create = [\n {\n ...unit.creation,\n duplicateOf: aiSiteResolvedRef(unit.creation.duplicateOf, built),\n },\n ]\n brief.push(\n `Build the ${unit.creation.kind} “${unit.creation.name}”: ${unit.creation.why}`,\n )\n }\n if (unit.screen) {\n const screen = unit.screen\n // Everything the scaffold has already built is a record the site has, so\n // the page is planned against it the way a page job is planned against\n // what the site always had.\n for (const entry of built.values()) {\n unitPlan.reuse.push({\n kind: entry.kind,\n id: entry.id,\n purpose: `the ${entry.kind} this site’s pages are built on`,\n })\n }\n unitPlan.screens = [\n {\n ...screen,\n layout: aiSiteResolvedRef(screen.layout, built),\n template: aiSiteResolvedRef(screen.template, built),\n duplicateOf: aiSiteResolvedRef(screen.duplicateOf, built),\n // A dataset the plan creates and the site has not built yet keeps its\n // name, which the page's draft still binds by (AGL-3475).\n record: screen.record\n ? {\n ...screen.record,\n dataset: aiSiteResolvedRef(screen.record.dataset, built) ?? screen.record.dataset,\n }\n : null,\n sections: screen.sections.map((section) => ({\n ...section,\n uses: section.uses\n .map((ref) => aiSiteResolvedRef(ref, built))\n .filter((ref): ref is string => !!ref),\n })),\n },\n ]\n brief.push(\n `Build the page “${screen.title}” of this site, at ${screen.slug}.`,\n )\n }\n if (unit.kind === 'email') {\n brief.push(...aiSiteEmailBriefLines(job.inputs))\n }\n return {\n ...job,\n $id: aiSiteUnitJobId(job, unit),\n kind: unit.jobKind,\n steps: [],\n outputs: [],\n plan: unitPlan,\n // A theme change on a site with no theme of its own is a new palette.\n ...(unit.kind === 'theme'\n ? { inputs: { ...job.inputs, mode: 'create' } }\n : {}),\n // The email step reads the kind off the inputs it is handed, so a job the\n // scaffold composed says which kind it is exactly as a member's own does.\n ...(unit.kind === 'email'\n ? { inputs: { ...job.inputs, emailType: AI_SITE_EMAIL_TYPE } }\n : {}),\n brief: brief.join('\\n').slice(0, AI_JOB_BRIEF_MAX_CHARS),\n }\n}\n\n/**\n * A scaffold is admitted with inputs that read, for a site of the job's own\n * org, and only where this deployment has loaded the step that builds a page:\n * a scaffold whose pages nothing can build is not a scaffold.\n */\nexport const aiSiteJobAdmission: AiJobAdmission = async (context) => {\n const inputs = parseAiSiteJobInputs(context.inputs)\n if (typeof inputs === 'string') return { status: 400, error: inputs }\n if (!context.hostId) {\n return {\n status: 400,\n error: 'Open the site the scaffold is for before starting the job',\n }\n }\n const owner = await resolveOrgIdForHost(context.hostId)\n if (!owner || owner !== context.orgId)\n return { status: 404, error: 'Unknown site' }\n if (!aiJobStepRunnerFor('page')) {\n return { status: 400, error: AI_SITE_NO_PAGE_STEP_COPY }\n }\n return null\n}\n\n/** What one delegated pass came to: the unit's spend as the job records it, and whether the unit is built. */\nexport interface AiJobUnitPass {\n outcome: AiJobStepOutcome\n /** The unit reported what it built and asked for no pass of its own. */\n built: boolean\n}\n\n/**\n * One delegated pass (AGL-2911, AGL-3031): the unit handed to the runner of\n * the kind that owns it, under the job derived for it, and its outcome as the\n * delegating job records it. A delegate proposes a plan for its own job,\n * which does not exist, so no plan is carried back. A unit that stopped for a\n * person, refused or failed is the delegating job's stop; a unit that asked\n * for another pass keeps its turn; and a unit that finished without reporting\n * what it built fails with `emptyCopy`, since another pass would ask the same\n * thing again. The tokens it spent are on the bill either way.\n */\nexport async function aiRunJobUnit(\n context: AiJobStepContext,\n input: {\n unit: AiSiteUnit\n units: readonly AiSiteUnit[]\n runner: AiJobStepRunner\n emptyCopy: string\n },\n): Promise<AiJobUnitPass> {\n const { job } = context\n const outputs = job.outputs ?? []\n const outcome = await input.runner({\n ...context,\n job: aiSiteUnitJob(job, input.unit, aiSiteBuiltRefs(input.units, outputs)),\n })\n const spent: AiJobStepOutcome = {\n outputs: outcome.outputs,\n usage: outcome.usage,\n estCostUsd: outcome.estCostUsd,\n model: outcome.model,\n stopReason: outcome.stopReason,\n ...(outcome.effort ? { effort: outcome.effort } : {}),\n ...(outcome.refused ? { refused: true } : {}),\n ...(outcome.failure ? { failure: outcome.failure } : {}),\n ...(outcome.review ? { review: outcome.review } : {}),\n }\n if (spent.review || spent.refused || spent.failure) return { outcome: spent, built: false }\n if (outcome.continue) return { outcome: { ...spent, continue: true }, built: false }\n if (!outcome.outputs.length) return { outcome: { ...spent, failure: input.emptyCopy }, built: false }\n return { outcome: spent, built: true }\n}\n\nexport interface AiJobSiteStepDeps {\n /** The runner registry; specs hand in a fake for a kind they drive. */\n runnerFor?: typeof aiJobStepRunnerFor\n}\n\nexport function createAiJobSiteStep(\n deps: AiJobSiteStepDeps = {},\n): AiJobStepRunner {\n const runnerFor = deps.runnerFor ?? aiJobStepRunnerFor\n return async (context): Promise<AiJobStepOutcome> => {\n const { job } = context\n // The scaffold asks no model of its own. What it names where it spends\n // nothing is the model its plan ran on, which is the one exchange this\n // job has already had of its own.\n const model = context.modelFor?.('job.plan') ?? aiModelForStep('job.plan')\n const plan = aiConfirmedPlan(job)\n if (!plan)\n return { ...aiUnspentOutcome(model), failure: AI_SITE_NO_PLAN_COPY }\n // The resume door refuses such a plan at confirmation once it can read\n // one; a plan confirmed before a site changed still stops here, unspent.\n const refusal = aiSitePlanRefusal(plan)\n if (refusal) return { ...aiUnspentOutcome(model), failure: refusal }\n const inputs = parseAiSiteJobInputs(job.inputs)\n if (typeof inputs === 'string')\n return { ...aiUnspentOutcome(model), failure: inputs }\n\n // A kind this deployment has not loaded is not among the units at all, so\n // a scaffold owes only what something can build.\n const units = aiSiteJobUnits(plan, {\n welcomeEmail: inputs.welcomeEmail,\n }).filter((unit) => runnerFor(unit.jobKind))\n const outputs = job.outputs ?? []\n const pending = aiSitePendingUnits(units, outputs)\n if (!pending.length) return aiUnspentOutcome(model)\n const unit = pending[0]\n const runner = runnerFor(unit.jobKind)\n if (!runner) return aiUnspentOutcome(model)\n\n const pass = await aiRunJobUnit(context, {\n unit,\n units,\n runner,\n emptyCopy: AI_SITE_UNIT_EMPTY_COPY,\n })\n // The site's own listing rides out beside the first unit's output, once\n // (AGL-2918): it is derived from the answers rather than generated, so it\n // is ready before anything is built and costs the pass nothing.\n const outcome: AiJobStepOutcome = {\n ...pass.outcome,\n outputs: [...aiSiteSeoOutputs(job), ...pass.outcome.outputs],\n }\n // A built unit continues the scaffold while units remain after it.\n return pass.built && pending.length > 1 ? { ...outcome, continue: true } : outcome\n }\n}\n\nexport const runAiJobSiteStep = createAiJobSiteStep()\n\n/**\n * The least time a scaffold's next pass needs (AGL-3035). The scaffold asks no\n * model itself: each pass hands one unit to the step registered for its kind,\n * under the job derived for it, so it needs what that step registers for that\n * job — a palette change's or a layout's time for those units, a page pass's\n * for a page. A scaffold with nothing left to build, or with no plan it can\n * build, spends nothing, and a page pass's time covers it.\n */\nexport function aiSiteJobRunMinimumMs(job: AiJob): number {\n const plan = aiConfirmedPlan(job)\n const inputs = parseAiSiteJobInputs(job.inputs)\n if (!plan || typeof inputs === 'string') return AI_JOB_PAGE_STEP_MINIMUM_MS\n const units = aiSiteJobUnits(plan, { welcomeEmail: inputs.welcomeEmail }).filter((unit) =>\n aiJobStepRunnerFor(unit.jobKind),\n )\n const outputs = job.outputs ?? []\n const [unit] = aiSitePendingUnits(units, outputs)\n if (!unit) return AI_JOB_PAGE_STEP_MINIMUM_MS\n return aiJobStepRunMinimumMs(aiSiteUnitJob(job, unit, aiSiteBuiltRefs(units, outputs)))\n}\n\n/**\n * Registers the site scaffold with the least time a pass needs — every\n * scaffold builds pages, and a page pass needs the least of any unit — and the\n * time its next pass needs, the passes a whole site's plan may take, and the\n * check a site job passes before it is created or resumed.\n */\nexport function registerAiSiteJob(): void {\n registerAiJobStep('site', runAiJobSiteStep, {\n minimumMs: AI_JOB_PAGE_STEP_MINIMUM_MS,\n minimumMsFor: aiSiteJobRunMinimumMs,\n })\n registerAiJobStepPasses('site', AI_SITE_MAX_PASSES)\n registerAiJobAdmission('site', aiSiteJobAdmission)\n}\n"],"names":["resolveOrgIdForHost","isAiPlanNewRef","AI_SITE_EMAIL_TYPE","AI_SITE_MAX_SECTIONS","AI_SITE_PAGES","aiSitePlanRefusal","aiSiteSubmissions","aiSiteWords","parseAiSiteJobInputs","AI_SITE_SEO_OUTPUT_ID","aiSiteSeoProposalForInputs","aiModelForStep","registerAiJobAdmission","aiRecordedJobDraftId","aiConfirmedPlan","aiUnspentOutcome","AI_JOB_BRIEF_MAX_CHARS","AI_JOB_PAGE_STEP_MINIMUM_MS","aiJobStepRunMinimumMs","aiJobStepRunnerFor","registerAiJobStep","registerAiJobStepPasses","UNIT_KINDS","theme","jobKind","resource","layout","form","component","page","email","CREATION_UNIT_KINDS","aiCreationUnit","creation","slot","kind","label","name","CREATION_UNITS","unit","create","AI_SITE_NO_PLAN_COPY","AI_SITE_NO_PAGE_STEP_COPY","AI_SITE_UNIT_EMPTY_COPY","AI_SITE_MAX_PASSES","max","length","aiSiteJobUnits","plan","options","units","find","entry","push","screens","forEach","screen","index","title","welcomeEmail","aiSitePendingUnits","outputs","done","Map","output","set","get","filter","left","aiSiteSeoOutputs","job","some","id","proposal","inputs","hostId","aiSiteBuiltRefs","byResource","rows","built","row","shift","toLowerCase","aiSiteResolvedRef","ref","slice","indexOf","trim","aiSiteBriefLines","brief","lines","site","businessName","businessType","audience","city","brand","Boolean","join","aiSiteEmailBriefLines","submissions","aiSiteUnitJobId","recorded","$id","aiSiteUnitJob","labels","values","unitPlan","reuse","placedByScreens","Set","flatMap","sections","section","uses","has","duplicateOf","why","purpose","template","record","dataset","map","slug","steps","mode","emailType","aiSiteJobAdmission","context","status","error","owner","orgId","aiRunJobUnit","input","outcome","runner","spent","usage","estCostUsd","model","stopReason","effort","refused","failure","review","continue","emptyCopy","createAiJobSiteStep","deps","runnerFor","modelFor","refusal","pending","pass","runAiJobSiteStep","aiSiteJobRunMinimumMs","registerAiSiteJob","minimumMs","minimumMsFor"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,mBAAmB,QAAQ,gDAA+C;AACnF,SACEC,cAAc,QAIT,4BAAwB;AAQ/B,SACEC,kBAAkB,EAClBC,oBAAoB,EACpBC,aAAa,EACbC,iBAAiB,EACjBC,iBAAiB,EACjBC,WAAW,EACXC,oBAAoB,QAEf,0BAAsB;AAC7B,SACEC,qBAAqB,EACrBC,0BAA0B,QACrB,gCAA4B;AACnC,SAASC,cAAc,QAAQ,0BAAsB;AACrD,SAASC,sBAAsB,QAA6B,wBAAoB;AAChF,SAASC,oBAAoB,QAAQ,wBAAoB;AACzD,SAASC,eAAe,EAAEC,gBAAgB,QAAQ,yBAAqB;AACvE,SACEC,sBAAsB,QAIjB,wBAAoB;AAC3B,SAASC,2BAA2B,QAAQ,0BAAsB;AAClE,SACEC,qBAAqB,EACrBC,kBAAkB,EAClBC,iBAAiB,EACjBC,uBAAuB,QAClB,eAAW;AA2ElB,iEAAiE,GACjE,MAAMC,aAGF;IACFC,OAAO;QAAEC,SAAS;QAASC,UAAU;IAAQ;IAC7CC,QAAQ;QAAEF,SAAS;QAAUC,UAAU;IAAS;IAChDE,MAAM;QAAEH,SAAS;QAAQC,UAAU;IAAO;IAC1CG,WAAW;QAAEJ,SAAS;QAAaC,UAAU;IAAoB;IACjEI,MAAM;QAAEL,SAAS;QAAQC,UAAU;IAAS;IAC5CK,OAAO;QAAEN,SAAS;QAASC,UAAU;IAAc;AACrD;AAEA,kEAAkE,GAClE,MAAMM,sBAA8E;IAClF,gBAAgB;IAChBL,QAAQ;IACRC,MAAM;IACNC,WAAW;AACb;AAEA;;;CAGC,GACD,OAAO,SAASI,eAAeC,QAA2B,EAAEC,IAAY;IACtE,MAAMC,OAAOJ,mBAAmB,CAACE,SAASE,IAAI,CAAC;IAC/C,IAAI,CAACA,MAAM,OAAO;IAClB,OAAO;QAAEA;OAASb,UAAU,CAACa,KAAK;QAAED;QAAMD;QAAUG,OAAOH,SAASI,IAAI;;AAC1E;AAEA,2DAA2D,GAC3D,MAAMC,iBAGD;IACH;QAAEC,MAAM;QAASC,QAAQ;IAAe;IACxC;QAAED,MAAM;QAAUC,QAAQ;IAAS;IACnC;QAAED,MAAM;QAAQC,QAAQ;IAAO;CAChC;AAED,OAAO,MAAMC,uBACX,gDAA+C;AAEjD,6EAA6E,GAC7E,OAAO,MAAMC,4BACX,8CAA6C;AAE/C,qFAAqF,GACrF,OAAO,MAAMC,0BACX,kEAAiE;AAEnE;;;;;CAKC,GACD,OAAO,MAAMC,qBACXxC,cAAcyC,GAAG,GAAI1C,CAAAA,uBAAuB,CAAA,IAAKmC,eAAeQ,MAAM,GAAG,EAAC;AAE5E;;;;;;CAMC,GACD,OAAO,SAASC,eACdC,IAA2C,EAC3CC,UAAsC,CAAC,CAAC;IAExC,MAAMC,QAAsB,EAAE;IAC9B,KAAK,MAAM,EAAEX,IAAI,EAAEC,MAAM,EAAE,IAAIF,eAAgB;QAC7C,MAAML,WAAWe,KAAKR,MAAM,CAACW,IAAI,CAAC,CAACC,QAAUA,MAAMjB,IAAI,KAAKK;QAC5D,IAAI,CAACP,UAAU;QACfiB,MAAMG,IAAI,CAAC;YACTlB,MAAMI;WACHjB,UAAU,CAACiB,KAAK;YACnBL,MAAMK,IAAI,CAAC,EAAE;YACbN;YACAG,OAAOH,SAASI,IAAI;;IAExB;IACAW,KAAKM,OAAO,CAACC,OAAO,CAAC,CAACC,QAAQC;QAC5BP,MAAMG,IAAI,CAAC;YACTlB,MAAM;WACHb,WAAWO,IAAI;YAClBK,MAAM,CAAC,CAAC,EAAEuB,OAAO;YACjBD;YACApB,OAAOoB,OAAOE,KAAK;;IAEvB;IACA,IAAIT,QAAQU,YAAY,EAAE;QACxBT,MAAMG,IAAI,CAAC;YACTlB,MAAM;WACHb,WAAWQ,KAAK;YACnBI,MAAM;YACNE,OAAO;;IAEX;IACA,OAAOc;AACT;AAEA;;;;CAIC,GACD,OAAO,SAASU,mBACdV,KAA4B,EAC5BW,OAAiD;IAEjD,MAAMC,OAAO,IAAIC;IACjB,KAAK,MAAMC,UAAUH,QAAS;YACDC;QAA3BA,KAAKG,GAAG,CAACD,OAAOvC,QAAQ,EAAE,EAACqC,YAAAA,KAAKI,GAAG,CAACF,OAAOvC,QAAQ,aAAxBqC,YAA6B,KAAK;IAC/D;IACA,OAAOZ,MAAMiB,MAAM,CAAC,CAAC5B;YACNuB;QAAb,MAAMM,QAAON,YAAAA,KAAKI,GAAG,CAAC3B,KAAKd,QAAQ,aAAtBqC,YAA2B;QACxC,IAAIM,QAAQ,GAAG,OAAO;QACtBN,KAAKG,GAAG,CAAC1B,KAAKd,QAAQ,EAAE2C,OAAO;QAC/B,OAAO;IACT;AACF;AAEA;;;;;;;;;;CAUC,GACD,OAAO,SAASC,iBAAiBC,GAAU;QACzBA,cAUJA;IAVZ,MAAMT,WAAUS,eAAAA,IAAIT,OAAO,YAAXS,eAAe,EAAE;IACjC,IAAIT,QAAQU,IAAI,CAAC,CAACP,SAAWA,OAAOvC,QAAQ,KAAK,SAASuC,OAAOQ,EAAE,KAAK/D,wBAAwB;QAC9F,OAAO,EAAE;IACX;IACA,MAAMgE,WAAW/D,2BAA2B4D,IAAII,MAAM;IACtD,IAAI,CAACD,UAAU,OAAO,EAAE;IACxB,OAAO;QACL;YACEhD,UAAU;YACV+C,IAAI/D;YACJkE,MAAM,GAAEL,cAAAA,IAAIK,MAAM,YAAVL,cAAc;YACtBlC,OAAO;YACPqC,UAAUA;QACZ;KACD;AACH;AAUA;;;;;CAKC,GACD,OAAO,SAASG,gBACd1B,KAA4B,EAC5BW,OAAkE;IAElE,MAAMgB,aAAa,IAAId;IAIvB,KAAK,MAAMC,UAAUH,QAAS;YACfgB;QAAb,MAAMC,QAAOD,kBAAAA,WAAWX,GAAG,CAACF,OAAOvC,QAAQ,aAA9BoD,kBAAmC,EAAE;QAClDC,KAAKzB,IAAI,CAACW;QACVa,WAAWZ,GAAG,CAACD,OAAOvC,QAAQ,EAAEqD;IAClC;IACA,MAAMC,QAAQ,IAAIhB;IAClB,KAAK,MAAMxB,QAAQW,MAAO;QACxB,MAAM4B,OAAOD,WAAWX,GAAG,CAAC3B,KAAKd,QAAQ;QACzC,MAAMuD,MAAMF,wBAAAA,KAAMG,KAAK;QACvB,IAAI,CAACD,OAAO,CAACzC,KAAKN,QAAQ,EAAE;QAC5B,wEAAwE;QACxE,mDAAmD;QACnD,IACEM,KAAKN,QAAQ,CAACE,IAAI,KAAK,YACvBI,KAAKN,QAAQ,CAACE,IAAI,KAAK,UACvBI,KAAKN,QAAQ,CAACE,IAAI,KAAK,aACvB;YACA;QACF;QACA4C,MAAMd,GAAG,CAAC1B,KAAKN,QAAQ,CAACI,IAAI,CAAC6C,WAAW,IAAI;YAC1CV,IAAIQ,IAAIR,EAAE;YACVpC,OAAO4C,IAAI5C,KAAK,IAAIG,KAAKN,QAAQ,CAACI,IAAI;YACtCF,MAAMI,KAAKN,QAAQ,CAACE,IAAI;QAC1B;IACF;IACA,OAAO4C;AACT;AAIA,yFAAyF,GACzF,OAAO,SAASI,kBAAkBC,GAAkB,EAAEL,KAAgB;;QAIlEA;IAHF,IAAI,CAACK,KAAK,OAAO;IACjB,IAAI,CAACnF,eAAemF,MAAM,OAAOA;IACjC,gBACEL,aAAAA,MAAMb,GAAG,CACPkB,IACGC,KAAK,CAACD,IAAIE,OAAO,CAAC,OAAO,GACzBC,IAAI,GACJL,WAAW,wBAJhBH,WAKGP,EAAE,mBAAI;AAEb;AAEA;;;;;;;CAOC,GACD,OAAO,SAASgB,iBACdC,KAAa,EACbf,MAAuB;IAEvB,MAAMgB,QAAQ;QAACD,MAAMF,IAAI;KAAG;IAC5B,MAAMI,OAAO;QACXjB,OAAOkB,YAAY,GAAG,CAAC,MAAM,EAAElB,OAAOkB,YAAY,EAAE,GAAG;QACvD,CAAC,UAAU,EAAElB,OAAOmB,YAAY,EAAE;QAClCnB,OAAOoB,QAAQ,GAAG,CAAC,KAAK,EAAEpB,OAAOoB,QAAQ,EAAE,GAAG;QAC9CpB,OAAOqB,IAAI,GAAG,CAAC,MAAM,EAAErB,OAAOqB,IAAI,EAAE,GAAG;QACvCrB,OAAOsB,KAAK,GAAG,CAAC,OAAO,EAAEtB,OAAOsB,KAAK,EAAE,GAAG;KAC3C,CAAC7B,MAAM,CAAC8B;IACTP,MAAMrC,IAAI,CAAC,CAAC,OAAO,EAAEsC,KAAKO,IAAI,CAAC,MAAM,CAAC,CAAC;IACvC,OAAOR;AACT;AAEA;;;;;;;;;;;;;CAaC,GACD,OAAO,SAASS,sBACdzB,MAA4D;IAE5D,MAAMgB,QAAQ;QACZ;KACD;IACD,MAAM,EAAEI,QAAQ,EAAE,GAAGvF,YAAYmE;IACjC,IAAIoB,UAAUJ,MAAMrC,IAAI,CAAC,CAAC,YAAY,EAAEyC,SAAS,CAAC,CAAC;IACnD,MAAMM,cAAc9F,kBAAkBoE;IACtC,IAAI0B,gBAAgB,QAAQ;QAC1BV,MAAMrC,IAAI,CAAC;IACb,OAAO,IAAI+C,gBAAgB,SAAS;QAClCV,MAAMrC,IAAI,CAAC;IACb;IACA,OAAOqC;AACT;AAEA;;;;;;CAMC,GACD,OAAO,SAASW,gBAAgB/B,GAAiC,EAAE/B,IAAgB;QAE/EA;QAAAA,gBAAqBA;IADvB,MAAM+D,YACJ/D,iBAAAA,iBAAAA,KAAKN,QAAQ,qBAAbM,eAAeiC,EAAE,qBAAIjC,eAAAA,KAAKiB,MAAM,qBAAXjB,aAAaiC,EAAE,YAApCjC,OAAyCA,KAAKJ,IAAI,KAAK,UAAUtB,qBAAqByD,KAAK,WAAW;IACxG,OAAOgC,YAAY,GAAGhC,IAAIiC,GAAG,CAAC,CAAC,EAAEhE,KAAKL,IAAI,EAAE;AAC9C;AAEA;;;;;;;;;;;;;;;CAeC,GACD,OAAO,SAASsE,cACdlC,GAAU,EACV/B,IAAgB,EAChBwC,KAAgB;QAGK/B;IADrB,MAAMA,OAAOsB,IAAItB,IAAI;IACrB,MAAMyD,SAAS,cAAMzD,eAAAA,KAAKyD,MAAM,YAAXzD,eAAe,CAAC;IACrC,KAAK,MAAMI,SAAS2B,MAAM2B,MAAM,GAAID,MAAM,CAACrD,MAAMoB,EAAE,CAAC,GAAGpB,MAAMhB,KAAK;IAClE,MAAMsC,SAASlE,qBAAqB8D,IAAII,MAAM;IAC9C,MAAMe,QACJ,OAAOf,WAAW,WACd;QAACJ,IAAImB,KAAK;KAAC,GACXD,iBAAiBlB,IAAImB,KAAK,EAAEf;IAClC,MAAMiC,WAAsB,aACvB3D;QACHyD;QACAG,OAAO;eAAI5D,KAAK4D,KAAK;SAAC;QACtBpE,QAAQ,EAAE;QACVc,SAAS,EAAE;;IAEb,IAAIf,KAAKN,QAAQ,EAAE;QACjB,MAAM4E,kBAAkB,IAAIC,IAC1B9D,KAAKM,OAAO,CAACyD,OAAO,CAAC,CAACvD,SAAWA,OAAOwD,QAAQ,CAACD,OAAO,CAAC,CAACE,UAAYA,QAAQC,IAAI;QAEpFP,SAASC,KAAK,GAAG5D,KAAK4D,KAAK,CAACzC,MAAM,CAAC,CAACf,QAAU,CAACyD,gBAAgBM,GAAG,CAAC/D,MAAMoB,EAAE;QAC3EmC,SAASnE,MAAM,GAAG;YAChB,aACKD,KAAKN,QAAQ;gBAChBmF,aAAajC,kBAAkB5C,KAAKN,QAAQ,CAACmF,WAAW,EAAErC;;SAE7D;QACDU,MAAMpC,IAAI,CACR,CAAC,UAAU,EAAEd,KAAKN,QAAQ,CAACE,IAAI,CAAC,EAAE,EAAEI,KAAKN,QAAQ,CAACI,IAAI,CAAC,GAAG,EAAEE,KAAKN,QAAQ,CAACoF,GAAG,EAAE;IAEnF;IACA,IAAI9E,KAAKiB,MAAM,EAAE;YAuBI2B;QAtBnB,MAAM3B,SAASjB,KAAKiB,MAAM;QAC1B,yEAAyE;QACzE,uEAAuE;QACvE,4BAA4B;QAC5B,KAAK,MAAMJ,SAAS2B,MAAM2B,MAAM,GAAI;YAClCC,SAASC,KAAK,CAACvD,IAAI,CAAC;gBAClBlB,MAAMiB,MAAMjB,IAAI;gBAChBqC,IAAIpB,MAAMoB,EAAE;gBACZ8C,SAAS,CAAC,IAAI,EAAElE,MAAMjB,IAAI,CAAC,+BAA+B,CAAC;YAC7D;QACF;QACAwE,SAASrD,OAAO,GAAG;YACjB,aACKE;gBACH9B,QAAQyD,kBAAkB3B,OAAO9B,MAAM,EAAEqD;gBACzCwC,UAAUpC,kBAAkB3B,OAAO+D,QAAQ,EAAExC;gBAC7CqC,aAAajC,kBAAkB3B,OAAO4D,WAAW,EAAErC;gBACnD,sEAAsE;gBACtE,0DAA0D;gBAC1DyC,QAAQhE,OAAOgE,MAAM,GACjB,aACKhE,OAAOgE,MAAM;oBAChBC,OAAO,GAAEtC,qBAAAA,kBAAkB3B,OAAOgE,MAAM,CAACC,OAAO,EAAE1C,kBAAzCI,qBAAmD3B,OAAOgE,MAAM,CAACC,OAAO;qBAEnF;gBACJT,UAAUxD,OAAOwD,QAAQ,CAACU,GAAG,CAAC,CAACT,UAAa,aACvCA;wBACHC,MAAMD,QAAQC,IAAI,CACfQ,GAAG,CAAC,CAACtC,MAAQD,kBAAkBC,KAAKL,QACpCZ,MAAM,CAAC,CAACiB,MAAuB,CAAC,CAACA;;;SAGzC;QACDK,MAAMpC,IAAI,CACR,CAAC,gBAAgB,EAAEG,OAAOE,KAAK,CAAC,mBAAmB,EAAEF,OAAOmE,IAAI,CAAC,CAAC,CAAC;IAEvE;IACA,IAAIpF,KAAKJ,IAAI,KAAK,SAAS;QACzBsD,MAAMpC,IAAI,IAAI8C,sBAAsB7B,IAAII,MAAM;IAChD;IACA,OAAO,aACFJ;QACHiC,KAAKF,gBAAgB/B,KAAK/B;QAC1BJ,MAAMI,KAAKf,OAAO;QAClBoG,OAAO,EAAE;QACT/D,SAAS,EAAE;QACXb,MAAM2D;OAEFpE,KAAKJ,IAAI,KAAK,UACd;QAAEuC,QAAQ,aAAKJ,IAAII,MAAM;YAAEmD,MAAM;;IAAW,IAC5C,CAAC,GAGDtF,KAAKJ,IAAI,KAAK,UACd;QAAEuC,QAAQ,aAAKJ,IAAII,MAAM;YAAEoD,WAAW5H;;IAAqB,IAC3D,CAAC;QACLuF,OAAOA,MAAMS,IAAI,CAAC,MAAMb,KAAK,CAAC,GAAGrE;;AAErC;AAEA;;;;CAIC,GACD,OAAO,MAAM+G,qBAAqC,OAAOC;IACvD,MAAMtD,SAASlE,qBAAqBwH,QAAQtD,MAAM;IAClD,IAAI,OAAOA,WAAW,UAAU,OAAO;QAAEuD,QAAQ;QAAKC,OAAOxD;IAAO;IACpE,IAAI,CAACsD,QAAQrD,MAAM,EAAE;QACnB,OAAO;YACLsD,QAAQ;YACRC,OAAO;QACT;IACF;IACA,MAAMC,QAAQ,MAAMnI,oBAAoBgI,QAAQrD,MAAM;IACtD,IAAI,CAACwD,SAASA,UAAUH,QAAQI,KAAK,EACnC,OAAO;QAAEH,QAAQ;QAAKC,OAAO;IAAe;IAC9C,IAAI,CAAC/G,mBAAmB,SAAS;QAC/B,OAAO;YAAE8G,QAAQ;YAAKC,OAAOxF;QAA0B;IACzD;IACA,OAAO;AACT,EAAC;AASD;;;;;;;;;CASC,GACD,OAAO,eAAe2F,aACpBL,OAAyB,EACzBM,KAKC;QAGehE;IADhB,MAAM,EAAEA,GAAG,EAAE,GAAG0D;IAChB,MAAMnE,WAAUS,eAAAA,IAAIT,OAAO,YAAXS,eAAe,EAAE;IACjC,MAAMiE,UAAU,MAAMD,MAAME,MAAM,CAAC,aAC9BR;QACH1D,KAAKkC,cAAclC,KAAKgE,MAAM/F,IAAI,EAAEqC,gBAAgB0D,MAAMpF,KAAK,EAAEW;;IAEnE,MAAM4E,QAA0B;QAC9B5E,SAAS0E,QAAQ1E,OAAO;QACxB6E,OAAOH,QAAQG,KAAK;QACpBC,YAAYJ,QAAQI,UAAU;QAC9BC,OAAOL,QAAQK,KAAK;QACpBC,YAAYN,QAAQM,UAAU;OAC1BN,QAAQO,MAAM,GAAG;QAAEA,QAAQP,QAAQO,MAAM;IAAC,IAAI,CAAC,GAC/CP,QAAQQ,OAAO,GAAG;QAAEA,SAAS;IAAK,IAAI,CAAC,GACvCR,QAAQS,OAAO,GAAG;QAAEA,SAAST,QAAQS,OAAO;IAAC,IAAI,CAAC,GAClDT,QAAQU,MAAM,GAAG;QAAEA,QAAQV,QAAQU,MAAM;IAAC,IAAI,CAAC;IAErD,IAAIR,MAAMQ,MAAM,IAAIR,MAAMM,OAAO,IAAIN,MAAMO,OAAO,EAAE,OAAO;QAAET,SAASE;QAAO1D,OAAO;IAAM;IAC1F,IAAIwD,QAAQW,QAAQ,EAAE,OAAO;QAAEX,SAAS,aAAKE;YAAOS,UAAU;;QAAQnE,OAAO;IAAM;IACnF,IAAI,CAACwD,QAAQ1E,OAAO,CAACf,MAAM,EAAE,OAAO;QAAEyF,SAAS,aAAKE;YAAOO,SAASV,MAAMa,SAAS;;QAAIpE,OAAO;IAAM;IACpG,OAAO;QAAEwD,SAASE;QAAO1D,OAAO;IAAK;AACvC;AAOA,OAAO,SAASqE,oBACdC,OAA0B,CAAC,CAAC;QAEVA;IAAlB,MAAMC,aAAYD,kBAAAA,KAAKC,SAAS,YAAdD,kBAAkBlI;IACpC,OAAO,OAAO6G;kBAsBI1D;QArBhB,MAAM,EAAEA,GAAG,EAAE,GAAG0D;QAChB,uEAAuE;QACvE,uEAAuE;QACvE,kCAAkC;QAClC,MAAMY,gBAAQZ,QAAQuB,QAAQ,oBAAhBvB,QAAQuB,QAAQ,MAAhBvB,SAAmB,8BAAerH,eAAe;QAC/D,MAAMqC,OAAOlC,gBAAgBwD;QAC7B,IAAI,CAACtB,MACH,OAAO,aAAKjC,iBAAiB6H;YAAQI,SAASvG;;QAChD,uEAAuE;QACvE,yEAAyE;QACzE,MAAM+G,UAAUnJ,kBAAkB2C;QAClC,IAAIwG,SAAS,OAAO,aAAKzI,iBAAiB6H;YAAQI,SAASQ;;QAC3D,MAAM9E,SAASlE,qBAAqB8D,IAAII,MAAM;QAC9C,IAAI,OAAOA,WAAW,UACpB,OAAO,aAAK3D,iBAAiB6H;YAAQI,SAAStE;;QAEhD,0EAA0E;QAC1E,iDAAiD;QACjD,MAAMxB,QAAQH,eAAeC,MAAM;YACjCW,cAAce,OAAOf,YAAY;QACnC,GAAGQ,MAAM,CAAC,CAAC5B,OAAS+G,UAAU/G,KAAKf,OAAO;QAC1C,MAAMqC,WAAUS,eAAAA,IAAIT,OAAO,YAAXS,eAAe,EAAE;QACjC,MAAMmF,UAAU7F,mBAAmBV,OAAOW;QAC1C,IAAI,CAAC4F,QAAQ3G,MAAM,EAAE,OAAO/B,iBAAiB6H;QAC7C,MAAMrG,OAAOkH,OAAO,CAAC,EAAE;QACvB,MAAMjB,SAASc,UAAU/G,KAAKf,OAAO;QACrC,IAAI,CAACgH,QAAQ,OAAOzH,iBAAiB6H;QAErC,MAAMc,OAAO,MAAMrB,aAAaL,SAAS;YACvCzF;YACAW;YACAsF;YACAW,WAAWxG;QACb;QACA,wEAAwE;QACxE,0EAA0E;QAC1E,gEAAgE;QAChE,MAAM4F,UAA4B,aAC7BmB,KAAKnB,OAAO;YACf1E,SAAS;mBAAIQ,iBAAiBC;mBAASoF,KAAKnB,OAAO,CAAC1E,OAAO;aAAC;;QAE9D,mEAAmE;QACnE,OAAO6F,KAAK3E,KAAK,IAAI0E,QAAQ3G,MAAM,GAAG,IAAI,aAAKyF;YAASW,UAAU;aAASX;IAC7E;AACF;AAEA,OAAO,MAAMoB,mBAAmBP,sBAAqB;AAErD;;;;;;;CAOC,GACD,OAAO,SAASQ,sBAAsBtF,GAAU;QAO9BA;IANhB,MAAMtB,OAAOlC,gBAAgBwD;IAC7B,MAAMI,SAASlE,qBAAqB8D,IAAII,MAAM;IAC9C,IAAI,CAAC1B,QAAQ,OAAO0B,WAAW,UAAU,OAAOzD;IAChD,MAAMiC,QAAQH,eAAeC,MAAM;QAAEW,cAAce,OAAOf,YAAY;IAAC,GAAGQ,MAAM,CAAC,CAAC5B,OAChFpB,mBAAmBoB,KAAKf,OAAO;IAEjC,MAAMqC,WAAUS,eAAAA,IAAIT,OAAO,YAAXS,eAAe,EAAE;IACjC,MAAM,CAAC/B,KAAK,GAAGqB,mBAAmBV,OAAOW;IACzC,IAAI,CAACtB,MAAM,OAAOtB;IAClB,OAAOC,sBAAsBsF,cAAclC,KAAK/B,MAAMqC,gBAAgB1B,OAAOW;AAC/E;AAEA;;;;;CAKC,GACD,OAAO,SAASgG;IACdzI,kBAAkB,QAAQuI,kBAAkB;QAC1CG,WAAW7I;QACX8I,cAAcH;IAChB;IACAvI,wBAAwB,QAAQuB;IAChChC,uBAAuB,QAAQmH;AACjC"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-site-step.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { resolveEffectivePlan } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport { resolveOrgIdForHost } from '@aglyn/tenant-data-admin/server/organizations'\nimport {\n isAiPlanNewRef,\n type AiBuildPlanCreate,\n type AiBuildPlanCreateKind,\n type AiBuildPlanScreen,\n} from '../model/ai-build-plan'\nimport type {\n AiJob,\n AiJobKind,\n AiJobOutput,\n AiJobOutputResource,\n AiJobPlan,\n} from '../model/ai-jobs.types'\nimport {\n AI_SITE_EMAIL_TYPE,\n AI_SITE_MAX_SECTIONS,\n AI_SITE_PAGES,\n aiSitePagesRefusal,\n aiSitePlanRefusal,\n aiSiteSubmissions,\n aiSiteWords,\n parseAiSiteJobInputs,\n type AiSiteJobInputs,\n} from '../model/ai-site-job'\nimport {\n AI_SITE_SEO_OUTPUT_ID,\n aiSiteSeoProposalForInputs,\n} from '../model/ai-site-start-seo'\nimport { aiModelForStep } from '../providers/routing'\nimport { registerAiJobAdmission, type AiJobAdmission } from './ai-job-admission'\nimport { aiRecordedJobDraftId } from './ai-job-draft-ids'\nimport { aiConfirmedPlan, aiUnspentOutcome } from './ai-job-generation'\nimport {\n AI_JOB_BRIEF_MAX_CHARS,\n type AiJobStepContext,\n type AiJobStepOutcome,\n type AiJobStepRunner,\n} from './ai-job-text-step'\nimport { AI_JOB_PAGE_STEP_MINIMUM_MS } from './ai-job-page-budget'\nimport {\n aiJobStepRunMinimumMs,\n aiJobStepRunnerFor,\n registerAiJobStep,\n registerAiJobStepPasses,\n} from './ai-jobs'\n\n/**\n * The site scaffold (AGL-2911): the generation step of a `site` job, run once\n * a member confirmed the plan their brief produced.\n *\n * ── It builds nothing itself ─────────────────────────────────────────────\n *\n * A scaffold is a whole site: a palette suggestion, the layout its pages\n * render inside, the contact form they place, four to eight pages with their\n * search listings, and a welcome email. Every one of those is already a job\n * kind with a step of its own, so this step generates nothing and asks no\n * model anything. It works through the confirmed plan one UNIT at a time and\n * hands each unit to the runner registered for the kind that owns it — the\n * theme step for the palette, the page step for a page — under a job of that\n * kind derived from this one.\n *\n * That is why the scaffold cannot drift from the thing it scaffolds: a page\n * it builds is a page job's page, held to the same doctrine, written by the\n * same draft writer, left unpublished by the same rule. A kind whose step\n * this deployment has not loaded is simply not among the units, so nothing\n * here knows which issues have landed.\n *\n * ── One unit a pass ──────────────────────────────────────────────────────\n *\n * The step runs ONE delegated pass and asks the machine to continue, so every\n * pass is one reservation, one provider exchange and one recorded spend, and\n * the org's monthly ceiling binds a scaffold exactly as it binds a chat turn.\n * Credits running out mid-scaffold is therefore the machine's own park: the\n * job waits as `needs_input` with the meter's words, and the beat resumes it\n * where it stopped once the workspace's standing has changed. A pass needs the\n * time its unit's own step needs (`aiSiteJobRunMinimumMs`), so the beat starts\n * a palette change only with a palette change's time left.\n *\n * Where it stopped is read from the job's OUTPUTS rather than kept anywhere:\n * each unit reports exactly one output when it completes, so the units\n * already represented there are the ones already built. A pass interrupted\n * between its work and its record repeats a unit that then finds its own\n * draft and reports it, which is what every step's draft id already gives.\n *\n * ── Never published ──────────────────────────────────────────────────────\n *\n * Nothing here publishes, routes or sends: each delegated step writes the\n * unpublished draft it already wrote for a job of its own kind, and the\n * palette and navigation travel as proposals a member applies.\n *\n * ── The page job builds its creations the same way (AGL-3031) ────────────\n *\n * A page job whose plan creates a layout, forms or components builds them\n * before its page through this same machinery: the units a creation implies\n * (`aiCreationUnit`), where the job stands read from its outputs\n * (`aiSitePendingUnits`), one delegated pass a unit (`aiRunJobUnit`), and the\n * `new:<name>` references resolved to what was built (`aiSiteBuiltRefs`,\n * `aiSiteUnitJob`). There is one loop, and it is the machine's.\n */\n\n/** What a scaffold's units are, in the order it builds them; a component is a page job's. */\nexport type AiSiteUnitKind = 'theme' | 'layout' | 'form' | 'component' | 'page' | 'email'\n\nexport interface AiSiteUnit {\n kind: AiSiteUnitKind\n /** The job kind whose registered runner builds it. */\n jobKind: AiJobKind\n /** The resource the unit reports when it completes. */\n resource: AiJobOutputResource\n /** Stable within the job: what the unit's derived job and draft are addressed by. */\n slot: string\n /** The plan screen a page unit builds. */\n screen?: AiBuildPlanScreen\n /** The creation a theme, layout or form unit builds. */\n creation?: AiBuildPlanCreate\n /** What the unit is, for the derived job's brief. */\n label: string\n}\n\n/** The job kind and output resource each unit is built through. */\nconst UNIT_KINDS: Record<\n AiSiteUnitKind,\n { jobKind: AiJobKind; resource: AiJobOutputResource }\n> = {\n theme: { jobKind: 'theme', resource: 'theme' },\n layout: { jobKind: 'layout', resource: 'layout' },\n form: { jobKind: 'form', resource: 'form' },\n component: { jobKind: 'component', resource: 'reusableComponent' },\n page: { jobKind: 'page', resource: 'screen' },\n email: { jobKind: 'email', resource: 'emailScreen' },\n}\n\n/** The unit kind a creation is built as, where a unit builds it. */\nconst CREATION_UNIT_KINDS: Partial<Record<AiBuildPlanCreateKind, AiSiteUnitKind>> = {\n 'theme-change': 'theme',\n layout: 'layout',\n form: 'form',\n component: 'component',\n}\n\n/**\n * The unit one creation is built as, under the slot its derived job and draft\n * are addressed by; `null` for a creation no unit builds.\n */\nexport function aiCreationUnit(creation: AiBuildPlanCreate, slot: string): AiSiteUnit | null {\n const kind = CREATION_UNIT_KINDS[creation.kind]\n if (!kind) return null\n return { kind, ...UNIT_KINDS[kind], slot, creation, label: creation.name }\n}\n\n/** The creation kinds a unit is built from, by unit kind. */\nconst CREATION_UNITS: Array<{\n unit: AiSiteUnitKind\n create: AiBuildPlanCreateKind\n}> = [\n { unit: 'theme', create: 'theme-change' },\n { unit: 'layout', create: 'layout' },\n { unit: 'form', create: 'form' },\n]\n\nexport const AI_SITE_NO_PLAN_COPY =\n 'This site job has no confirmed plan to build.'\n\n/** What a scaffold answers where the step that builds a page is not loaded. */\nexport const AI_SITE_NO_PAGE_STEP_COPY =\n 'Building a whole site is not available yet.'\n\n/** A unit that finished without reporting what it built; the scaffold cannot go on. */\nexport const AI_SITE_UNIT_EMPTY_COPY =\n 'Part of this site could not be built. Try the site brief again.'\n\n/**\n * The most passes a scaffold's step may take: the largest plan it admits —\n * eight pages of eight sections, its three creations and a welcome email —\n * with nothing to spare, so a runner that never finishes is still bounded\n * while a real site is not.\n */\nexport const AI_SITE_MAX_PASSES =\n AI_SITE_PAGES.max * (AI_SITE_MAX_SECTIONS + 1) + CREATION_UNITS.length + 1\n\n/**\n * The units a plan implies, in build order: the palette first, because a\n * member reads it while the pages are still building; then the layout every\n * page renders inside and the form they place, because a page binds both by\n * id and so needs them to exist; then the pages; then the welcome email,\n * which is about the site rather than part of it.\n */\nexport function aiSiteJobUnits(\n plan: Pick<AiJobPlan, 'create' | 'screens'>,\n options: { welcomeEmail?: boolean } = {},\n): AiSiteUnit[] {\n const units: AiSiteUnit[] = []\n for (const { unit, create } of CREATION_UNITS) {\n const creation = plan.create.find((entry) => entry.kind === create)\n if (!creation) continue\n units.push({\n kind: unit,\n ...UNIT_KINDS[unit],\n slot: unit[0],\n creation,\n label: creation.name,\n })\n }\n plan.screens.forEach((screen, index) => {\n units.push({\n kind: 'page',\n ...UNIT_KINDS.page,\n slot: `p${index}`,\n screen,\n label: screen.title,\n })\n })\n if (options.welcomeEmail) {\n units.push({\n kind: 'email',\n ...UNIT_KINDS.email,\n slot: 'e',\n label: 'Welcome email',\n })\n }\n return units\n}\n\n/**\n * The units a scaffold still owes after the outputs recorded so far. Each\n * unit reports one output of its own resource when it completes, so the\n * outputs already recorded are consumed against the units in build order.\n */\nexport function aiSitePendingUnits(\n units: readonly AiSiteUnit[],\n outputs: readonly Pick<AiJobOutput, 'resource'>[],\n): AiSiteUnit[] {\n const done = new Map<AiJobOutputResource, number>()\n for (const output of outputs) {\n done.set(output.resource, (done.get(output.resource) ?? 0) + 1)\n }\n return units.filter((unit) => {\n const left = done.get(unit.resource) ?? 0\n if (left <= 0) return true\n done.set(unit.resource, left - 1)\n return false\n })\n}\n\n/**\n * The site's own search listing as the scaffold reports it (AGL-2918), or\n * nothing when the job already reported one or its inputs describe no site.\n *\n * It costs nothing and asks no model: the values are arithmetic on the\n * answers (`aiSiteSeoProposal`), so they ride out on the first pass that\n * builds anything rather than waiting for a pass of their own. Its resource\n * is `seo`, which no unit reports, so it cannot be mistaken for a unit's\n * output by `aiSitePendingUnits` and cannot move where the scaffold thinks\n * it is.\n */\nexport function aiSiteSeoOutputs(job: AiJob): AiJobOutput[] {\n const outputs = job.outputs ?? []\n if (outputs.some((output) => output.resource === 'seo' && output.id === AI_SITE_SEO_OUTPUT_ID)) {\n return []\n }\n const proposal = aiSiteSeoProposalForInputs(job.inputs)\n if (!proposal) return []\n return [\n {\n resource: 'seo',\n id: AI_SITE_SEO_OUTPUT_ID,\n hostId: job.hostId ?? null,\n label: 'The site’s search title and description',\n proposal: proposal as unknown as Record<string, unknown>,\n },\n ]\n}\n\n/** A record an earlier unit created, as a later one references it. */\nexport interface AiSiteBuiltRef {\n id: string\n label: string\n /** Only what becomes a record a page can name; a palette change is not one. */\n kind: 'layout' | 'form' | 'component'\n}\n\n/**\n * What the scaffold has already created, as the plan's `new:<name>`\n * references resolve to: the id of the record each completed creation wrote.\n * A page unit is told about the layout and the form by id, exactly as a page\n * job is told about records the site already had.\n */\nexport function aiSiteBuiltRefs(\n units: readonly AiSiteUnit[],\n outputs: readonly Pick<AiJobOutput, 'resource' | 'id' | 'label'>[],\n): Map<string, AiSiteBuiltRef> {\n const byResource = new Map<\n AiJobOutputResource,\n Array<Pick<AiJobOutput, 'id' | 'label'>>\n >()\n for (const output of outputs) {\n const rows = byResource.get(output.resource) ?? []\n rows.push(output)\n byResource.set(output.resource, rows)\n }\n const built = new Map<string, AiSiteBuiltRef>()\n for (const unit of units) {\n const rows = byResource.get(unit.resource)\n const row = rows?.shift()\n if (!row || !unit.creation) continue\n // A theme change is a proposal with no record to reference; a layout, a\n // form and a component become ids a page can name.\n if (\n unit.creation.kind !== 'layout' &&\n unit.creation.kind !== 'form' &&\n unit.creation.kind !== 'component'\n ) {\n continue\n }\n built.set(unit.creation.name.toLowerCase(), {\n id: row.id,\n label: row.label || unit.creation.name,\n kind: unit.creation.kind,\n })\n }\n return built\n}\n\ntype BuiltRefs = ReturnType<typeof aiSiteBuiltRefs>\n\n/** A plan reference as the unit's own job reads it: an id the site now has, or nothing. */\nexport function aiSiteResolvedRef(ref: string | null, built: BuiltRefs): string | null {\n if (!ref) return null\n if (!isAiPlanNewRef(ref)) return ref\n return (\n built.get(\n ref\n .slice(ref.indexOf(':') + 1)\n .trim()\n .toLowerCase(),\n )?.id ?? null\n )\n}\n\n/**\n * The site's own words, as every unit's brief carries them.\n *\n * The audience is here rather than left to the brief's prose (AGL-2918). A\n * guided start writes it into the brief itself, but an agency batch's brief\n * is a member's own sentence and may never mention it, and a theme, a layout\n * or a form asked to serve nobody in particular serves nobody in particular.\n */\nexport function aiSiteBriefLines(\n brief: string,\n inputs: AiSiteJobInputs,\n): string[] {\n const lines = [brief.trim()]\n const site = [\n inputs.businessName ? `name: ${inputs.businessName}` : '',\n `business: ${inputs.businessType}`,\n inputs.audience ? `for: ${inputs.audience}` : '',\n inputs.city ? `city: ${inputs.city}` : '',\n inputs.brand ? `brand: ${inputs.brand}` : '',\n ].filter(Boolean)\n lines.push(`Site — ${site.join('; ')}.`)\n return lines\n}\n\n/**\n * What the welcome email is told (AGL-2918), beyond that it is a welcome\n * email.\n *\n * The scaffold's email unit used to carry one sentence, and a yes/no toggle\n * decided whether it ran — so the draft was a welcome email for a business\n * in general, to nobody in particular, about nothing that had happened. The\n * two answers it is missing are who wrote in and what now becomes of what\n * they wrote, and the second of those is the person's own routing answer, so\n * the email and the form it acknowledges cannot say different things.\n *\n * ⛔ A DRAFT either way. Nothing here sends, schedules or enrolls anybody:\n * the email step writes an unpublished email design and stops.\n */\nexport function aiSiteEmailBriefLines(\n inputs: Readonly<Record<string, unknown>> | null | undefined,\n): string[] {\n const lines = [\n 'Write the welcome email this site sends someone who gets in touch.',\n ]\n const { audience } = aiSiteWords(inputs)\n if (audience) lines.push(`Write it to ${audience}.`)\n const submissions = aiSiteSubmissions(inputs)\n if (submissions === 'lead') {\n lines.push('Their message is a sales lead, so say that somebody will be in touch about it.')\n } else if (submissions === 'inbox') {\n lines.push('Their message has been read, so say it arrived and that a reply is coming.')\n }\n return lines\n}\n\n/**\n * The id a unit's job is named by, and so the id its draft is written under\n * (AGL-3079): the id the plan entry it builds recorded, or for the welcome\n * email the id recorded on this job's step. A unit that writes no draft — a\n * palette change — and a unit whose plan recorded no id are named by this\n * job's id and the unit's slot, the name every draft of such a plan has.\n */\nexport function aiSiteUnitJobId(job: Pick<AiJob, '$id' | 'steps'>, unit: AiSiteUnit): string {\n const recorded =\n unit.creation?.id ?? unit.screen?.id ?? (unit.kind === 'email' ? aiRecordedJobDraftId(job, 'email') : null)\n return recorded || `${job.$id}-${unit.slot}`\n}\n\n/**\n * The job a unit is built under: this job's org, site, creator and model,\n * the kind that owns the unit, and a plan narrowed to the unit alone.\n *\n * The derived job carries NO steps and NO outputs of the scaffold's: a page\n * job's step reads its own draft and its own plan, and what the other units\n * produced is not its business. Its `$id` is the id the unit's draft is\n * written under (`aiSiteUnitJobId`), which is what a step with no id of its\n * own recorded names its draft by, so a unit re-run after its write finds its\n * own draft rather than writing a second.\n *\n * A creation unit reuses what the plan reuses less what the plan's screens\n * place themselves (AGL-3031): a component a page's section places is the\n * page's to place, and a layout or a card told to place it would be refused\n * for leaving it out.\n */\nexport function aiSiteUnitJob(\n job: AiJob,\n unit: AiSiteUnit,\n built: BuiltRefs,\n): AiJob {\n const plan = job.plan as AiJobPlan\n const labels = { ...(plan.labels ?? {}) }\n for (const entry of built.values()) labels[entry.id] = entry.label\n const inputs = parseAiSiteJobInputs(job.inputs)\n const brief =\n typeof inputs === 'string'\n ? [job.brief]\n : aiSiteBriefLines(job.brief, inputs)\n const unitPlan: AiJobPlan = {\n ...plan,\n labels,\n reuse: [...plan.reuse],\n create: [],\n screens: [],\n }\n if (unit.creation) {\n const placedByScreens = new Set(\n plan.screens.flatMap((screen) => screen.sections.flatMap((section) => section.uses)),\n )\n unitPlan.reuse = plan.reuse.filter((entry) => !placedByScreens.has(entry.id))\n unitPlan.create = [\n {\n ...unit.creation,\n duplicateOf: aiSiteResolvedRef(unit.creation.duplicateOf, built),\n },\n ]\n brief.push(\n `Build the ${unit.creation.kind} “${unit.creation.name}”: ${unit.creation.why}`,\n )\n }\n if (unit.screen) {\n const screen = unit.screen\n // Everything the scaffold has already built is a record the site has, so\n // the page is planned against it the way a page job is planned against\n // what the site always had.\n for (const entry of built.values()) {\n unitPlan.reuse.push({\n kind: entry.kind,\n id: entry.id,\n purpose: `the ${entry.kind} this site’s pages are built on`,\n })\n }\n unitPlan.screens = [\n {\n ...screen,\n layout: aiSiteResolvedRef(screen.layout, built),\n template: aiSiteResolvedRef(screen.template, built),\n duplicateOf: aiSiteResolvedRef(screen.duplicateOf, built),\n // A dataset the plan creates and the site has not built yet keeps its\n // name, which the page's draft still binds by (AGL-3475).\n record: screen.record\n ? {\n ...screen.record,\n dataset: aiSiteResolvedRef(screen.record.dataset, built) ?? screen.record.dataset,\n }\n : null,\n sections: screen.sections.map((section) => ({\n ...section,\n uses: section.uses\n .map((ref) => aiSiteResolvedRef(ref, built))\n .filter((ref): ref is string => !!ref),\n })),\n },\n ]\n brief.push(\n `Build the page “${screen.title}” of this site, at ${screen.slug}.`,\n )\n }\n if (unit.kind === 'email') {\n brief.push(...aiSiteEmailBriefLines(job.inputs))\n }\n return {\n ...job,\n $id: aiSiteUnitJobId(job, unit),\n kind: unit.jobKind,\n steps: [],\n outputs: [],\n plan: unitPlan,\n // A theme change on a site with no theme of its own is a new palette.\n ...(unit.kind === 'theme'\n ? { inputs: { ...job.inputs, mode: 'create' } }\n : {}),\n // The email step reads the kind off the inputs it is handed, so a job the\n // scaffold composed says which kind it is exactly as a member's own does.\n ...(unit.kind === 'email'\n ? { inputs: { ...job.inputs, emailType: AI_SITE_EMAIL_TYPE } }\n : {}),\n brief: brief.join('\\n').slice(0, AI_JOB_BRIEF_MAX_CHARS),\n }\n}\n\n/**\n * A scaffold is admitted with inputs that read, for a site of the job's own\n * org, and only where this deployment has loaded the step that builds a page:\n * a scaffold whose pages nothing can build is not a scaffold.\n */\nexport const aiSiteJobAdmission: AiJobAdmission = async (context) => {\n const inputs = parseAiSiteJobInputs(context.inputs)\n if (typeof inputs === 'string') return { status: 400, error: inputs }\n // The workspace's own page band (AGL-3594): one or two pages on the Free\n // taste, four to eight on a paid plan.\n const freeTaste = aiSiteFreeTaste(context.org)\n const pages = aiSitePagesRefusal(inputs.pages, freeTaste)\n if (pages) return { status: 400, error: pages }\n if (context.plan) {\n const shape = aiSitePlanRefusal(context.plan, { freeTaste })\n if (shape) return { status: 400, error: shape }\n }\n if (!context.hostId) {\n return {\n status: 400,\n error: 'Open the site the scaffold is for before starting the job',\n }\n }\n const owner = await resolveOrgIdForHost(context.hostId)\n if (!owner || owner !== context.orgId)\n return { status: 404, error: 'Unknown site' }\n if (!aiJobStepRunnerFor('page')) {\n return { status: 400, error: AI_SITE_NO_PAGE_STEP_COPY }\n }\n return null\n}\n\n/** Whether a scaffold's workspace spends the Free taste (AGL-3594); a missing org reads as paid, as the band has always been. */\nexport function aiSiteFreeTaste(org: object | null | undefined): boolean {\n return Boolean(org) && resolveEffectivePlan(org as never) === 'free'\n}\n\n/**\n * Whether a scaffold drafts its welcome email (AGL-3594): where it was asked\n * for, and never on the Free taste, whose credits its pages need.\n */\nexport function aiSiteWelcomeEmail(inputs: AiSiteJobInputs, freeTaste: boolean): boolean {\n return inputs.welcomeEmail && !freeTaste\n}\n\n/** What one delegated pass came to: the unit's spend as the job records it, and whether the unit is built. */\nexport interface AiJobUnitPass {\n outcome: AiJobStepOutcome\n /** The unit reported what it built and asked for no pass of its own. */\n built: boolean\n}\n\n/**\n * One delegated pass (AGL-2911, AGL-3031): the unit handed to the runner of\n * the kind that owns it, under the job derived for it, and its outcome as the\n * delegating job records it. A delegate proposes a plan for its own job,\n * which does not exist, so no plan is carried back. A unit that stopped for a\n * person, refused or failed is the delegating job's stop; a unit that asked\n * for another pass keeps its turn; and a unit that finished without reporting\n * what it built fails with `emptyCopy`, since another pass would ask the same\n * thing again. The tokens it spent are on the bill either way.\n */\nexport async function aiRunJobUnit(\n context: AiJobStepContext,\n input: {\n unit: AiSiteUnit\n units: readonly AiSiteUnit[]\n runner: AiJobStepRunner\n emptyCopy: string\n },\n): Promise<AiJobUnitPass> {\n const { job } = context\n const outputs = job.outputs ?? []\n const outcome = await input.runner({\n ...context,\n job: aiSiteUnitJob(job, input.unit, aiSiteBuiltRefs(input.units, outputs)),\n })\n const spent: AiJobStepOutcome = {\n outputs: outcome.outputs,\n usage: outcome.usage,\n estCostUsd: outcome.estCostUsd,\n model: outcome.model,\n stopReason: outcome.stopReason,\n ...(outcome.effort ? { effort: outcome.effort } : {}),\n ...(outcome.refused ? { refused: true } : {}),\n ...(outcome.failure ? { failure: outcome.failure } : {}),\n ...(outcome.review ? { review: outcome.review } : {}),\n }\n if (spent.review || spent.refused || spent.failure) return { outcome: spent, built: false }\n if (outcome.continue) return { outcome: { ...spent, continue: true }, built: false }\n if (!outcome.outputs.length) return { outcome: { ...spent, failure: input.emptyCopy }, built: false }\n return { outcome: spent, built: true }\n}\n\nexport interface AiJobSiteStepDeps {\n /** The runner registry; specs hand in a fake for a kind they drive. */\n runnerFor?: typeof aiJobStepRunnerFor\n}\n\nexport function createAiJobSiteStep(\n deps: AiJobSiteStepDeps = {},\n): AiJobStepRunner {\n const runnerFor = deps.runnerFor ?? aiJobStepRunnerFor\n return async (context): Promise<AiJobStepOutcome> => {\n const { job } = context\n // The scaffold asks no model of its own. What it names where it spends\n // nothing is the model its plan ran on, which is the one exchange this\n // job has already had of its own.\n const model = context.modelFor?.('job.plan') ?? aiModelForStep('job.plan')\n const plan = aiConfirmedPlan(job)\n if (!plan)\n return { ...aiUnspentOutcome(model), failure: AI_SITE_NO_PLAN_COPY }\n // The resume door refuses such a plan at confirmation once it can read\n // one; a plan confirmed before a site changed still stops here, unspent.\n const freeTaste = aiSiteFreeTaste(context.org)\n const refusal = aiSitePlanRefusal(plan, { freeTaste })\n if (refusal) return { ...aiUnspentOutcome(model), failure: refusal }\n const inputs = parseAiSiteJobInputs(job.inputs)\n if (typeof inputs === 'string')\n return { ...aiUnspentOutcome(model), failure: inputs }\n\n // A kind this deployment has not loaded is not among the units at all, so\n // a scaffold owes only what something can build.\n const units = aiSiteJobUnits(plan, {\n welcomeEmail: aiSiteWelcomeEmail(inputs, freeTaste),\n }).filter((unit) => runnerFor(unit.jobKind))\n const outputs = job.outputs ?? []\n const pending = aiSitePendingUnits(units, outputs)\n if (!pending.length) return aiUnspentOutcome(model)\n const unit = pending[0]\n const runner = runnerFor(unit.jobKind)\n if (!runner) return aiUnspentOutcome(model)\n\n const pass = await aiRunJobUnit(context, {\n unit,\n units,\n runner,\n emptyCopy: AI_SITE_UNIT_EMPTY_COPY,\n })\n // The site's own listing rides out beside the first unit's output, once\n // (AGL-2918): it is derived from the answers rather than generated, so it\n // is ready before anything is built and costs the pass nothing.\n const outcome: AiJobStepOutcome = {\n ...pass.outcome,\n outputs: [...aiSiteSeoOutputs(job), ...pass.outcome.outputs],\n }\n // A built unit continues the scaffold while units remain after it.\n return pass.built && pending.length > 1 ? { ...outcome, continue: true } : outcome\n }\n}\n\nexport const runAiJobSiteStep = createAiJobSiteStep()\n\n/**\n * The least time a scaffold's next pass needs (AGL-3035). The scaffold asks no\n * model itself: each pass hands one unit to the step registered for its kind,\n * under the job derived for it, so it needs what that step registers for that\n * job — a palette change's or a layout's time for those units, a page pass's\n * for a page. A scaffold with nothing left to build, or with no plan it can\n * build, spends nothing, and a page pass's time covers it.\n */\nexport function aiSiteJobRunMinimumMs(job: AiJob): number {\n const plan = aiConfirmedPlan(job)\n const inputs = parseAiSiteJobInputs(job.inputs)\n if (!plan || typeof inputs === 'string') return AI_JOB_PAGE_STEP_MINIMUM_MS\n const units = aiSiteJobUnits(plan, { welcomeEmail: inputs.welcomeEmail }).filter((unit) =>\n aiJobStepRunnerFor(unit.jobKind),\n )\n const outputs = job.outputs ?? []\n const [unit] = aiSitePendingUnits(units, outputs)\n if (!unit) return AI_JOB_PAGE_STEP_MINIMUM_MS\n return aiJobStepRunMinimumMs(aiSiteUnitJob(job, unit, aiSiteBuiltRefs(units, outputs)))\n}\n\n/**\n * Registers the site scaffold with the least time a pass needs — every\n * scaffold builds pages, and a page pass needs the least of any unit — and the\n * time its next pass needs, the passes a whole site's plan may take, and the\n * check a site job passes before it is created or resumed.\n */\nexport function registerAiSiteJob(): void {\n registerAiJobStep('site', runAiJobSiteStep, {\n minimumMs: AI_JOB_PAGE_STEP_MINIMUM_MS,\n minimumMsFor: aiSiteJobRunMinimumMs,\n })\n registerAiJobStepPasses('site', AI_SITE_MAX_PASSES)\n registerAiJobAdmission('site', aiSiteJobAdmission)\n}\n"],"names":["resolveEffectivePlan","resolveOrgIdForHost","isAiPlanNewRef","AI_SITE_EMAIL_TYPE","AI_SITE_MAX_SECTIONS","AI_SITE_PAGES","aiSitePagesRefusal","aiSitePlanRefusal","aiSiteSubmissions","aiSiteWords","parseAiSiteJobInputs","AI_SITE_SEO_OUTPUT_ID","aiSiteSeoProposalForInputs","aiModelForStep","registerAiJobAdmission","aiRecordedJobDraftId","aiConfirmedPlan","aiUnspentOutcome","AI_JOB_BRIEF_MAX_CHARS","AI_JOB_PAGE_STEP_MINIMUM_MS","aiJobStepRunMinimumMs","aiJobStepRunnerFor","registerAiJobStep","registerAiJobStepPasses","UNIT_KINDS","theme","jobKind","resource","layout","form","component","page","email","CREATION_UNIT_KINDS","aiCreationUnit","creation","slot","kind","label","name","CREATION_UNITS","unit","create","AI_SITE_NO_PLAN_COPY","AI_SITE_NO_PAGE_STEP_COPY","AI_SITE_UNIT_EMPTY_COPY","AI_SITE_MAX_PASSES","max","length","aiSiteJobUnits","plan","options","units","find","entry","push","screens","forEach","screen","index","title","welcomeEmail","aiSitePendingUnits","outputs","done","Map","output","set","get","filter","left","aiSiteSeoOutputs","job","some","id","proposal","inputs","hostId","aiSiteBuiltRefs","byResource","rows","built","row","shift","toLowerCase","aiSiteResolvedRef","ref","slice","indexOf","trim","aiSiteBriefLines","brief","lines","site","businessName","businessType","audience","city","brand","Boolean","join","aiSiteEmailBriefLines","submissions","aiSiteUnitJobId","recorded","$id","aiSiteUnitJob","labels","values","unitPlan","reuse","placedByScreens","Set","flatMap","sections","section","uses","has","duplicateOf","why","purpose","template","record","dataset","map","slug","steps","mode","emailType","aiSiteJobAdmission","context","status","error","freeTaste","aiSiteFreeTaste","org","pages","shape","owner","orgId","aiSiteWelcomeEmail","aiRunJobUnit","input","outcome","runner","spent","usage","estCostUsd","model","stopReason","effort","refused","failure","review","continue","emptyCopy","createAiJobSiteStep","deps","runnerFor","modelFor","refusal","pending","pass","runAiJobSiteStep","aiSiteJobRunMinimumMs","registerAiSiteJob","minimumMs","minimumMsFor"],"mappings":"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"}
@@ -92,6 +92,13 @@ export interface AiJobStepOutcome {
92
92
  * step back, so trying again runs the same one.
93
93
  */
94
94
  review?: AiJobReview;
95
+ /**
96
+ * The answer was refused by the platform's own checks, not the model's
97
+ * (AGL-3594): the machine meters the step as any other and then gives the
98
+ * job's credits back to the meters it was charged to (`refundJobCredits`),
99
+ * a bounded number of times a day.
100
+ */
101
+ uncredited?: boolean;
95
102
  }
96
103
  export interface AiJobStepContext {
97
104
  job: AiJob;