@aglyn/plugins-ai 1.0.0-beta.258 → 1.0.0-beta.260

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.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-template-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 { throwIfAiJobCanceled } from './ai-job-cancel'\nimport { hostCollectionKind } from '@aglyn/aglyn/app-utils/collection-kind'\nimport { normalizeScreenSlug } from '@aglyn/aglyn/app-utils/screen-route'\nimport type { AglynOrgBilling } from '@aglyn/aglyn/foundation/definitions/org-billing.types'\nimport { duplicateResource } from '@aglyn/tenant-data-admin/server/duplicate-resource'\nimport { CANVAS_ROOT_ELEMENT_ID } from '@aglyn/aglyn/foundation/constants/canvas'\nimport type { AiJob, AiJobOutput, AiJobPlan, AiJobReview } from '../model/ai-jobs.types'\nimport {\n AI_TEMPLATE_SUBJECT_DEFINITIONS,\n AI_TEMPLATE_SUBJECTS,\n aiBindingTokensIn,\n aiTemplateAddressTokens,\n isAiAddressToken,\n isAiUnreadToken,\n parseAiTemplateJobInputs,\n type AiTemplateSubjectDefinition,\n} from '../model/ai-template-subjects'\nimport { AI_STEP_TIERS } from '../providers/catalog'\nimport { AI_ROUTING_TABLE, aiModelForStep } from '../providers/routing'\nimport {\n aiDoctrineTreeTool,\n runValidatedGeneration,\n type AiValidatedTree,\n} from '../runtime/ai-doctrine'\nimport {\n walkTree,\n type AiDoctrineNode,\n type AiDoctrineViolation,\n} from '../runtime/ai-doctrine-validators'\nimport type { AiLoadEstimate } from '../runtime/ai-palette'\nimport { AI_PALETTE } from '../runtime/ai-palette.generated'\nimport type { AiSystemBlock } from '../runtime/ai-runtime'\nimport { aiTemplateExamplesSystemBlock } from '../runtime/ai-template-examples'\nimport { readSiteInventory } from '../runtime/site-inventory'\nimport { registerAiJobAdmission, type AiJobAdmission } from './ai-job-admission'\nimport { aiJobDraftId } from './ai-job-draft-ids'\nimport {\n aiDraftAdmissionRefusal,\n aiDraftAllowanceRefusal,\n aiSiteSubdomain,\n readAiDraft,\n readAiTemplateDraftNodes,\n writeAiDraft,\n type AiDraftRecord,\n} from './ai-job-drafts'\nimport {\n aiConfirmedPlan,\n aiCopySourceLines,\n aiDoctrineReview,\n aiGenerationSpent,\n aiJobBriefLine,\n aiLimitReview,\n aiModelNodeIds,\n aiPlacedComponentIds,\n aiPlanCreation,\n aiPlanReferenceLines,\n aiPlanReuseViolations,\n aiUnspentOutcome,\n} from './ai-job-generation'\nimport { aiPlannedTemplateTokens, aiPlanTemplateTokenViolations } from './ai-job-plan-conformance'\nimport {\n aiBuildingPlanCreations,\n aiJobPlanCreationsRefusal,\n aiPlanCreationsRunMinimumMs,\n} from './ai-job-plan-creations'\nimport type { AiJobStepRunner } from './ai-job-text-step'\nimport { aiJobStepBudget } from './ai-job-budget'\nimport { registerAiJobStep } from './ai-jobs'\n\n/**\n * The template step (AGL-2909): the generation step of a `template` job, run\n * once a member confirmed the job's plan.\n *\n * It builds ONE page template — the page a site renders once per record: a\n * content collection's entry, a product, an author — through\n * `runValidatedGeneration('template', …)`. Everything that differs per\n * record is a binding token from the subject's vocabulary\n * (`model/ai-template-subjects.ts`), which is the list the besigner's insert\n * picker offers for that page; a link or media prop may hold one of those\n * tokens whole, which the palette validator admits only because this door\n * names them. The door adds one check of its own through `extend` (rule 8):\n * no token the page does not fill, an h1 bound to the subject's title, no\n * block that fills itself only on another subject's page, and no address,\n * slug or timestamp printed in its copy.\n *\n * The model is shown the platform's starter pages as examples, in a cached\n * block (`runtime/ai-template-examples.ts`), and writes against them.\n *\n * The template lands in the site's library as a new draft\n * (`ai-job-drafts.ts`), inert until a member uses it: bound to no\n * collection, set as no product or author page, served nowhere. A plan that\n * starts from a copy of a template the site has gets that copy instead.\n */\n\n/** The longest answer a template may run to; the tree is held to the template budget either way. */\nexport const AI_JOB_TEMPLATE_MAX_TOKENS = AI_ROUTING_TABLE['job.template'].maxTokens\n\n/**\n * The template step's time (AGL-3035, AGL-3036): its two inventory-lookup\n * rounds, its answer and its re-ask at the routing table's ceiling on the tier\n * `job.template` is served from, fitted to what a beat can give a step. It\n * runs on the job beat, never at an inline door, and asks for no more of the\n * ceiling than fits that time on the model it runs.\n */\nexport const AI_JOB_TEMPLATE_STEP_BUDGET = aiJobStepBudget({\n tier: AI_STEP_TIERS['job.template'],\n maxTokens: AI_JOB_TEMPLATE_MAX_TOKENS,\n})\n\n/** The least time one template step needs before it starts. */\nexport const AI_JOB_TEMPLATE_STEP_MINIMUM_MS = AI_JOB_TEMPLATE_STEP_BUDGET.minimumMs\n\n/** The template step's own instructions, cached after the doctrine and before the examples. */\nexport const AI_JOB_TEMPLATE_INSTRUCTIONS: readonly AiSystemBlock[] = [\n {\n text: [\n 'You build one page template: the page a website renders once for each record it keeps, such as each entry of a content collection, each product or each author. Answer with submit_template: the whole page as one flat node map.',\n 'Everything that differs from one record to the next is a binding token, never typed copy. The request names what the page is for, the tokens that page fills and the blocks that fill themselves there; bind only those tokens.',\n 'The page’s one h1 holds the title token the request names. Copy that reads the same on every record, such as a section heading, is written in the site’s voice.',\n 'A link or media prop holds exactly one token from the request’s link and media tokens, such as an Image whose src is {{entry.coverImage}}; its alt text may combine words and tokens. Copy never prints a link or media token, a slug or a timestamp.',\n 'A template carries no third-party player. It plays a video token only when the confirmed plan lists it, in a Video (video) whose src is that token and whose poster is the cover image token.',\n 'The page renders inside the site’s layout, so it carries no header, navigation or footer of its own.',\n ].join('\\n'),\n },\n]\n\n/** A content collection an entry template is for, as the prompt and the draft name it. */\nexport interface AiTemplateCollection {\n id: string\n name: string\n slug: string\n}\n\n/** The content collection by that id on the site, or `null` when there is none. */\nexport async function readAiTemplateCollection(\n firestore: FirebaseFirestore.Firestore,\n hostId: string,\n collectionId: string,\n): Promise<AiTemplateCollection | null> {\n const snapshot = await firestore\n .collection('hosts')\n .doc(hostId)\n .collection('collections')\n .doc(collectionId)\n .get()\n if (!snapshot.exists) return null\n const data = (snapshot.data() ?? {}) as Record<string, unknown>\n if (hostCollectionKind(data) !== 'content') return null\n const text = (value: unknown) => (typeof value === 'string' ? value.trim() : '')\n const slug = text(data['slug'])\n return {\n id: collectionId,\n name: text(data['displayName']) || text(data['name']) || slug || collectionId,\n slug,\n }\n}\n\n/** The template's name: the plan's, else the collection's or the subject's. */\nexport function aiTemplateDraftName(\n definition: AiTemplateSubjectDefinition,\n collection: AiTemplateCollection | null,\n planned: string | null | undefined,\n): string {\n return planned || (collection ? `${collection.name} entry template` : definition.defaultName)\n}\n\n/** The address Use template offers for the page it creates. */\nexport function aiTemplateDraftSlug(\n definition: AiTemplateSubjectDefinition,\n collection: AiTemplateCollection | null,\n): string | null {\n return (\n normalizeScreenSlug(\n collection\n ? `${collection.slug || collection.id}-entry-template`\n : `${definition.subject}-page-template`,\n ) ?? null\n )\n}\n\n/** The template job as the generation's user turn, with the template a short copy would have started from (AGL-3143 §14). */\nexport function aiJobTemplatePrompt(\n job: Pick<AiJob, 'brief'>,\n definition: AiTemplateSubjectDefinition,\n collection: AiTemplateCollection | null,\n plan: AiJobPlan | null,\n name: string,\n source: readonly string[] = [],\n): string {\n const of = collection\n ? ` of the \"${collection.name}\" collection${collection.slug ? ` (/${collection.slug})` : ''}`\n : ''\n return [\n `Template name: ${name}`,\n `The page is for ${definition.noun}${of}.`,\n `Title token: ${definition.titleToken}`,\n `Tokens this page fills: ${definition.tokens.map((entry) => `${entry.token} ${entry.label}`).join('; ')}`,\n `Link and media tokens: ${aiTemplateAddressTokens(definition).join(', ')}`,\n definition.blocks.length\n ? `Blocks that fill themselves on this page: ${definition.blocks.join(', ')}`\n : 'No block fills itself on this page: bind the tokens.',\n aiJobBriefLine(job),\n ...aiPlanReferenceLines(plan),\n ...source,\n ].join('\\n')\n}\n\n/** Which subject's page each self-filling block belongs to. */\nconst BLOCK_SUBJECTS = new Map<string, AiTemplateSubjectDefinition>(\n AI_TEMPLATE_SUBJECTS.flatMap((subject) =>\n AI_TEMPLATE_SUBJECT_DEFINITIONS[subject].blocks.map(\n (block) => [block, AI_TEMPLATE_SUBJECT_DEFINITIONS[subject]] as const,\n ),\n ),\n)\n\n/** Every string a prop value holds, however deeply. */\nfunction stringsIn(value: unknown, out: string[] = []): string[] {\n if (typeof value === 'string') out.push(value)\n else if (Array.isArray(value)) for (const inner of value) stringsIn(inner, out)\n else if (value && typeof value === 'object') {\n for (const inner of Object.values(value as Record<string, unknown>)) stringsIn(inner, out)\n }\n return out\n}\n\n/** The page's title: a Typography that renders as an h1. */\nfunction isPageTitle(node: AiDoctrineNode): boolean {\n if (node.componentId !== 'muiTypography') return false\n const element = typeof node.props?.['component'] === 'string' ? String(node.props['component']) : ''\n return (element || String(node.props?.['variant'] ?? '')) === 'h1'\n}\n\n/**\n * The tokens a prop prints for a reader that no reader reads: a link or\n * picture token, a slug or a timestamp, in copy (AGL-3143 §16). Copy is a\n * prop the palette gives the text role, read as words where it holds more\n * than one token or is the element's children. A block handed one token\n * whole reads it as a value, as Entry Meta's avatar takes the author's\n * portrait and a Video's publication date takes the timestamp, and a\n * markdown link's target is followed, not read.\n */\nfunction printedTokens(node: AiDoctrineNode): string[] {\n const roles = AI_PALETTE[node.componentId]?.propRoles ?? {}\n return Object.entries(node.props ?? {}).flatMap(([key, value]) => {\n if (typeof value !== 'string' || roles[key] !== 'text') return []\n const tokens = aiBindingTokensIn(value.replace(/\\]\\([^)]*\\)/g, ']'))\n const unread = tokens.filter((token) => isAiAddressToken(token) || isAiUnreadToken(token))\n const whole = tokens.length === 1 && value.trim() === tokens[0]\n return unread.length && (key === 'children' || !whole) ? unread : []\n })\n}\n\n/**\n * The template door's check on a tree the doctrine admitted (rule 8): every\n * token is one the page fills, the h1 binds the subject's title, no block\n * that fills itself on another subject's page is placed — except inside a\n * Collection Entries card, where each fills from the card's own entry — and\n * no copy prints an address, a slug or a timestamp.\n */\nexport function aiTemplateBindingCheck(\n definition: AiTemplateSubjectDefinition,\n): (tree: AiValidatedTree) => AiDoctrineViolation[] {\n const allowed = new Set(definition.tokens.map((entry) => entry.token))\n return (tree) => {\n const nodes = tree.nodes as unknown as Record<string, AiDoctrineNode>\n const unknown = new Set<string>()\n const unknownAt: string[] = []\n const foreign: Array<{ id: string; componentId: string; owner: AiTemplateSubjectDefinition }> = []\n const titles: string[] = []\n const printed = new Set<string>()\n const printedAt: string[] = []\n let titleBound = false\n for (const { id, node, ancestors } of walkTree({ rootId: tree.rootId, nodes })) {\n const tokens = stringsIn(node.props).flatMap(aiBindingTokensIn)\n const strays = tokens.filter((token) => !allowed.has(token))\n if (strays.length) {\n for (const token of strays) unknown.add(token)\n unknownAt.push(id)\n }\n const shown = printedTokens(node).filter((token) => allowed.has(token))\n if (shown.length) {\n for (const token of shown) printed.add(token)\n printedAt.push(id)\n }\n const owner = BLOCK_SUBJECTS.get(node.componentId)\n const inCard = ancestors.some((ancestor) => nodes[ancestor]?.componentId === 'collectionEntries')\n if (owner && owner.subject !== definition.subject && !inCard) {\n foreign.push({ id, componentId: node.componentId, owner })\n }\n if (isPageTitle(node)) {\n titles.push(id)\n if (tokens.includes(definition.titleToken)) titleBound = true\n }\n }\n const violations: AiDoctrineViolation[] = []\n if (unknown.size) {\n violations.push({\n rule: 8,\n code: 'unknown-binding',\n message: `This binds ${[...unknown].slice(0, 3).join(', ')}, which ${definition.noun}’s page does not fill. Bind only the tokens listed for this page.`,\n nodeIds: aiModelNodeIds(unknownAt, tree.sourceIds),\n })\n }\n if (printed.size) {\n violations.push({\n rule: 8,\n code: 'printed-token',\n message: `This prints ${[...printed].slice(0, 3).join(', ')} in the page's copy, where a reader sees an address, a slug or a timestamp. A link or picture token goes whole in a link or media prop, and a slug or a timestamp stays off the page.`,\n nodeIds: aiModelNodeIds(printedAt, tree.sourceIds),\n })\n }\n // The doctrine already refuses a page with no h1 or with several.\n if (titles.length === 1 && !titleBound) {\n violations.push({\n rule: 8,\n code: 'typed-title',\n message: `The page title is typed, so every record would show the same one. Put ${definition.titleToken} in the h1.`,\n nodeIds: aiModelNodeIds(titles, tree.sourceIds),\n })\n }\n if (foreign.length) {\n const [first] = foreign\n violations.push({\n rule: 8,\n code: 'foreign-block',\n message: `The ${AI_PALETTE[first.componentId]?.displayName ?? first.componentId} block fills itself only on ${first.owner.noun}’s page, and this template is for ${definition.noun}. Remove it.`,\n nodeIds: aiModelNodeIds(\n foreign.map((entry) => entry.id),\n tree.sourceIds,\n ),\n })\n }\n return violations\n }\n}\n\nexport interface AiTemplateJobAdmissionDeps {\n readCollection?: typeof readAiTemplateCollection\n}\n\n/**\n * A template job is admitted with inputs that say what the page is for, for\n * a site of the job's own org that holds the collection it names, with a\n * template to spare.\n */\nexport function createAiTemplateJobAdmission(deps: AiTemplateJobAdmissionDeps = {}): AiJobAdmission {\n const readCollection = deps.readCollection ?? readAiTemplateCollection\n return async (context) => {\n const inputs = parseAiTemplateJobInputs(context.inputs)\n if (typeof inputs === 'string') return { status: 400, error: inputs }\n return aiDraftAdmissionRefusal(context.firestore, {\n orgId: context.orgId,\n hostId: context.hostId,\n kind: 'template',\n org: context.org,\n ownCheck: async (hostId) =>\n inputs.collectionId && !(await readCollection(context.firestore, hostId, inputs.collectionId))\n ? { status: 404, error: 'That content collection is not on this site' }\n : aiJobPlanCreationsRefusal('template', context, hostId),\n })\n }\n}\n\nexport interface AiJobTemplateStepDeps {\n /** The inventory reader; specs hand in a fake. */\n readInventory?: typeof readSiteInventory\n /** The platform's duplicate module, for a plan that starts from a copy. */\n duplicate?: typeof duplicateResource\n readCollection?: typeof readAiTemplateCollection\n /** The starter examples block; built from the starters once per process otherwise. */\n examples?: () => AiSystemBlock\n}\n\n/**\n * The binding tokens the confirmed plan gives the template, against the\n * template a COPY actually produced (AGL-3024); `null` when the copy keeps\n * the plan, or when the plan promised no token to keep.\n *\n * The same hole as `aiCopiedLayoutReview` and `aiCopiedPageReview`, on the\n * third kind that can start from a duplicate. The generate path already holds\n * a built template to `aiPlanTemplateTokenViolations`; the copy path reached\n * none of it, so a plan reading \"duplicate it and bind the author's photo\"\n * got the duplicate and reported Done.\n *\n * A copy that cannot be read back settles nothing, and a promise this step\n * cannot settle is reviewed by a person rather than reported as kept.\n */\nexport async function aiCopiedTemplateReview(\n firestore: FirebaseFirestore.Firestore,\n input: {\n hostId: string\n id: string\n name: string\n plan: AiJobPlan | null\n definition: AiTemplateSubjectDefinition\n },\n): Promise<AiJobReview | null> {\n const { tokens, unreadable } = aiPlannedTemplateTokens(input.plan, input.definition)\n if (!tokens.length && !unreadable.length) return null\n const stored = await readAiTemplateDraftNodes(firestore, {\n hostId: input.hostId,\n id: input.id,\n })\n const nodes = (stored ?? {}) as unknown as Record<string, AiDoctrineNode>\n if (!nodes[CANVAS_ROOT_ELEMENT_ID]) {\n return {\n reason: 'doctrine',\n message: `\"${input.name}\" was copied from the template the plan names, and this job could not read the copy back to check it shows what the plan gives it. Open the template and check it before you apply it.`,\n findings: [],\n }\n }\n const violations = aiPlanTemplateTokenViolations(input.plan, input.definition, {\n rootId: CANVAS_ROOT_ELEMENT_ID,\n nodes,\n })\n if (!violations.length) return null\n return aiDoctrineReview({\n message: `\"${input.name}\" is a copy of the template the plan names, and a copy is all it is: ${violations[0].message}`,\n violations,\n })\n}\n\nexport function createAiJobTemplateStep(deps: AiJobTemplateStepDeps = {}): AiJobStepRunner {\n const readInventory = deps.readInventory ?? readSiteInventory\n const duplicate = deps.duplicate ?? duplicateResource\n const readCollection = deps.readCollection ?? readAiTemplateCollection\n const examples = deps.examples ?? aiTemplateExamplesSystemBlock\n return async ({ job, now, signal, firestore, modelFor }) => {\n const hostId = job.hostId\n if (!hostId) throw new Error('a template job names no site, and its admission refuses one')\n const inputs = parseAiTemplateJobInputs(job.inputs)\n if (typeof inputs === 'string') {\n throw new Error(`a template job's inputs no longer read as they were admitted: ${inputs}`)\n }\n const definition = AI_TEMPLATE_SUBJECT_DEFINITIONS[inputs.subject]\n const output = (draft: AiDraftRecord, load?: AiLoadEstimate | null): AiJobOutput => ({\n resource: 'template',\n id: draft.id,\n versionId: null,\n hostId,\n hostSubdomain: draft.hostSubdomain,\n label: draft.name,\n ...(load ? { load } : {}),\n })\n // The switch's answer for this job, else the routing table's (AGL-2942).\n const model = modelFor?.('job.template') ?? aiModelForStep('job.template')\n\n // A run cut off after its draft was written reports that draft, found by the id the job recorded.\n const draftId = aiJobDraftId(job, 'template')\n const written = await readAiDraft(firestore, { kind: 'template', hostId, id: draftId })\n if (written) return aiUnspentOutcome(model, { outputs: [output(written)] })\n\n const [inventory, orgSnapshot, collection] = await Promise.all([\n readInventory(job.orgId, hostId, { firestore }),\n firestore.collection('orgs').doc(job.orgId).get(),\n inputs.collectionId\n ? readCollection(firestore, hostId, inputs.collectionId)\n : Promise.resolve(null),\n ])\n if (inputs.collectionId && !collection) {\n throw new Error(`collection ${inputs.collectionId} left site ${hostId} after its template job was admitted`)\n }\n const org = (orgSnapshot.data() ?? null) as Partial<AglynOrgBilling> | null\n const plan = aiConfirmedPlan(job)\n const creation = aiPlanCreation(plan, 'template')\n const name = aiTemplateDraftName(definition, collection, creation?.name)\n\n // The plan starts from a copy of a template the site has (rule 15): the\n // copy is the draft, and nothing is generated. A source gone since the\n // plan was made is built from the brief instead, and one that cannot keep\n // the plan's tokens is built FROM (AGL-3143 §14): copying it could only end\n // in the review below. A source this step cannot read still copies.\n const source = creation?.duplicateOf\n ? inventory.templates.find((template) => template.id === creation.duplicateOf)\n : undefined\n const sourceReview = source\n ? await aiCopiedTemplateReview(firestore, { hostId, id: source.id, name: source.name, plan, definition })\n : null\n const sourceShort = Boolean(sourceReview?.findings.length)\n const sourceLines =\n source && sourceShort\n ? aiCopySourceLines(\n 'template',\n source.name,\n (await readAiTemplateDraftNodes(firestore, { hostId, id: source.id })) as Record<string, unknown> | null,\n )\n : []\n if (creation?.duplicateOf && source && !sourceShort) {\n const copy = await duplicate('template', {\n orgId: job.orgId,\n hostId,\n sourceId: creation.duplicateOf,\n name,\n uid: job.createdBy,\n org: org as Record<string, unknown> | null,\n // The same claim the page step makes (AGL-3024): a copy core mints is\n // written under core's own id, never `draftId`, so a step that runs\n // again — a pass resumed, a run cut off after the copy — would mint a\n // second. The job's recorded id makes core replay the one it made.\n attemptKey: draftId,\n })\n if (copy.ok) {\n const hostSubdomain = await aiSiteSubdomain(firestore, hostId)\n // The copy is reported with what the plan promised of it read back\n // (AGL-3024): this branch generates nothing, so nothing else can.\n const review = await aiCopiedTemplateReview(firestore, {\n hostId,\n id: copy.id,\n name: copy.name,\n plan,\n definition,\n })\n if (review) return aiUnspentOutcome(model, { review })\n return aiUnspentOutcome(model, {\n outputs: [output({ id: copy.id, versionId: null, name: copy.name, hostSubdomain })],\n })\n }\n if (copy.ok === false && copy.status === 403) {\n return aiUnspentOutcome(model, { review: aiLimitReview(copy.error) })\n }\n }\n\n const allowance = await aiDraftAllowanceRefusal(firestore, { kind: 'template', hostId, org })\n if (allowance) return aiUnspentOutcome(model, { review: aiLimitReview(allowance) })\n\n const result = await runValidatedGeneration('template', {\n step: 'job.template',\n model,\n instructions: [...AI_JOB_TEMPLATE_INSTRUCTIONS, examples()],\n inventory,\n messages: [\n {\n role: 'user',\n content: aiJobTemplatePrompt(job, definition, collection, plan, name, sourceLines),\n },\n ],\n tool: aiDoctrineTreeTool('template'),\n maxTokens: AI_JOB_TEMPLATE_STEP_BUDGET.maxTokens(model),\n ...(AI_ROUTING_TABLE['job.template'].thinking ? { thinking: AI_ROUTING_TABLE['job.template'].thinking } : {}),\n ...(AI_ROUTING_TABLE['job.template'].effort ? { effort: AI_ROUTING_TABLE['job.template'].effort } : {}),\n context: { bindingTokens: aiTemplateAddressTokens(definition) },\n // Rule 8's binding check, and the confirmed plan kept in both halves: a\n // token the plan promised that the template does not bind (AGL-3143\n // §11), and a component the plan reuses that it does not place. The\n // layout and component doors hold their builds to that reuse; the\n // template door did not, so it could drop a promised component and\n // still answer `Done` (AGL-3024).\n extend: (tree) => [\n ...aiTemplateBindingCheck(definition)(tree),\n ...aiPlanTemplateTokenViolations(plan, definition, {\n rootId: tree.rootId,\n nodes: tree.nodes as unknown as Record<string, AiDoctrineNode>,\n }),\n ...aiPlanReuseViolations(inventory, plan, aiPlacedComponentIds(tree), 'template'),\n ],\n ...(signal ? { signal } : {}),\n })\n const spent = aiGenerationSpent(result)\n if (result.status === 'refused') return { ...spent, refused: true }\n if (result.status === 'needs_input') return { ...spent, review: aiDoctrineReview(result) }\n\n // Never written once the job is canceled (AGL-3616).\n throwIfAiJobCanceled(signal)\n const draft = await writeAiDraft(firestore, {\n kind: 'template',\n hostId,\n id: draftId,\n uid: job.createdBy,\n org,\n name,\n nodes: result.value.nodes,\n slug: aiTemplateDraftSlug(definition, collection),\n now,\n })\n if (draft.ok === false) {\n if (draft.status === 404) throw new Error(`site ${hostId} vanished while its template was generated`)\n return { ...spent, review: aiLimitReview(draft.error) }\n }\n return { ...spent, outputs: [output(draft, result.value.load)] }\n }\n}\n\n/** The template step, building the components and forms its plan creates before the template (AGL-3143 §15). */\nexport const runAiJobTemplateStep = aiBuildingPlanCreations('template', 'job.template', createAiJobTemplateStep())\n\n/** Registers the template step and the check a template job passes before it is created or resumed. */\nexport function registerAiTemplateJob(): void {\n registerAiJobStep('template', runAiJobTemplateStep, {\n minimumMs: AI_JOB_TEMPLATE_STEP_MINIMUM_MS,\n minimumMsFor: aiPlanCreationsRunMinimumMs('template'),\n })\n registerAiJobAdmission('template', createAiTemplateJobAdmission())\n}\n"],"names":["throwIfAiJobCanceled","hostCollectionKind","normalizeScreenSlug","duplicateResource","CANVAS_ROOT_ELEMENT_ID","AI_TEMPLATE_SUBJECT_DEFINITIONS","AI_TEMPLATE_SUBJECTS","aiBindingTokensIn","aiTemplateAddressTokens","isAiAddressToken","isAiUnreadToken","parseAiTemplateJobInputs","AI_STEP_TIERS","AI_ROUTING_TABLE","aiModelForStep","aiDoctrineTreeTool","runValidatedGeneration","walkTree","AI_PALETTE","aiTemplateExamplesSystemBlock","readSiteInventory","registerAiJobAdmission","aiJobDraftId","aiDraftAdmissionRefusal","aiDraftAllowanceRefusal","aiSiteSubdomain","readAiDraft","readAiTemplateDraftNodes","writeAiDraft","aiConfirmedPlan","aiCopySourceLines","aiDoctrineReview","aiGenerationSpent","aiJobBriefLine","aiLimitReview","aiModelNodeIds","aiPlacedComponentIds","aiPlanCreation","aiPlanReferenceLines","aiPlanReuseViolations","aiUnspentOutcome","aiPlannedTemplateTokens","aiPlanTemplateTokenViolations","aiBuildingPlanCreations","aiJobPlanCreationsRefusal","aiPlanCreationsRunMinimumMs","aiJobStepBudget","registerAiJobStep","AI_JOB_TEMPLATE_MAX_TOKENS","maxTokens","AI_JOB_TEMPLATE_STEP_BUDGET","tier","AI_JOB_TEMPLATE_STEP_MINIMUM_MS","minimumMs","AI_JOB_TEMPLATE_INSTRUCTIONS","text","join","readAiTemplateCollection","firestore","hostId","collectionId","snapshot","collection","doc","get","exists","data","value","trim","slug","id","name","aiTemplateDraftName","definition","planned","defaultName","aiTemplateDraftSlug","subject","aiJobTemplatePrompt","job","plan","source","of","noun","titleToken","tokens","map","entry","token","label","blocks","length","BLOCK_SUBJECTS","Map","flatMap","block","stringsIn","out","push","Array","isArray","inner","Object","values","isPageTitle","node","componentId","element","props","String","printedTokens","roles","propRoles","entries","key","replace","unread","filter","whole","aiTemplateBindingCheck","allowed","Set","tree","nodes","unknown","unknownAt","foreign","titles","printed","printedAt","titleBound","ancestors","rootId","strays","has","add","shown","owner","inCard","some","ancestor","includes","violations","size","rule","code","message","slice","nodeIds","sourceIds","first","displayName","createAiTemplateJobAdmission","deps","readCollection","context","inputs","status","error","orgId","kind","org","ownCheck","aiCopiedTemplateReview","input","unreadable","stored","reason","findings","createAiJobTemplateStep","readInventory","duplicate","examples","now","signal","modelFor","orgSnapshot","Error","output","draft","load","resource","versionId","hostSubdomain","model","draftId","written","outputs","inventory","Promise","all","resolve","creation","duplicateOf","templates","find","template","undefined","sourceReview","sourceShort","Boolean","sourceLines","copy","sourceId","uid","createdBy","attemptKey","ok","review","allowance","result","step","instructions","messages","role","content","tool","thinking","effort","bindingTokens","extend","spent","refused","runAiJobTemplateStep","registerAiTemplateJob","minimumMsFor"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-template-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 { throwIfAiJobCanceled } from './ai-job-cancel'\nimport { hostCollectionKind } from '@aglyn/aglyn/app-utils/collection-kind'\nimport { normalizeScreenSlug } from '@aglyn/aglyn/app-utils/screen-route'\nimport type { AglynOrgBilling } from '@aglyn/aglyn/foundation/definitions/org-billing.types'\nimport { duplicateResource } from '@aglyn/tenant-data-admin/server/duplicate-resource'\nimport { CANVAS_ROOT_ELEMENT_ID } from '@aglyn/aglyn/foundation/constants/canvas'\nimport type { AiJob, AiJobOutput, AiJobPlan, AiJobReview } from '../model/ai-jobs.types'\nimport {\n AI_TEMPLATE_SUBJECT_DEFINITIONS,\n AI_TEMPLATE_SUBJECTS,\n aiBindingTokensIn,\n aiTemplateAddressTokens,\n isAiAddressToken,\n isAiUnreadToken,\n parseAiTemplateJobInputs,\n type AiTemplateSubjectDefinition,\n} from '../model/ai-template-subjects'\nimport { AI_STEP_TIERS } from '../providers/catalog'\nimport { AI_ROUTING_TABLE, aiModelForStep } from '../providers/routing'\nimport {\n aiDoctrineTreeTool,\n runValidatedGeneration,\n type AiValidatedTree,\n} from '../runtime/ai-doctrine'\nimport {\n walkTree,\n type AiDoctrineNode,\n type AiDoctrineViolation,\n} from '../runtime/ai-doctrine-validators'\nimport type { AiLoadEstimate } from '../runtime/ai-palette'\nimport { AI_PALETTE } from '../runtime/ai-palette.generated'\nimport type { AiSystemBlock } from '../runtime/ai-runtime'\nimport { aiTemplateExamplesSystemBlock } from '../runtime/ai-template-examples'\nimport { readSiteInventory } from '../runtime/site-inventory'\nimport { registerAiJobAdmission, type AiJobAdmission } from './ai-job-admission'\nimport { aiJobDraftId } from './ai-job-draft-ids'\nimport { aiLayoutMotionSiteOf } from './ai-job-page-language'\nimport { aiWithSiteMotion } from '../layout-language/ai-layout-motion'\nimport {\n aiDraftAdmissionRefusal,\n aiDraftAllowanceRefusal,\n aiSiteSubdomain,\n readAiDraft,\n readAiTemplateDraftNodes,\n writeAiDraft,\n type AiDraftRecord,\n} from './ai-job-drafts'\nimport {\n aiConfirmedPlan,\n aiCopySourceLines,\n aiDoctrineReview,\n aiGenerationSpent,\n aiJobBriefLine,\n aiLimitReview,\n aiModelNodeIds,\n aiPlacedComponentIds,\n aiPlanCreation,\n aiPlanReferenceLines,\n aiPlanReuseViolations,\n aiUnspentOutcome,\n} from './ai-job-generation'\nimport { aiPlannedTemplateTokens, aiPlanTemplateTokenViolations } from './ai-job-plan-conformance'\nimport {\n aiBuildingPlanCreations,\n aiJobPlanCreationsRefusal,\n aiPlanCreationsRunMinimumMs,\n} from './ai-job-plan-creations'\nimport type { AiJobStepRunner } from './ai-job-text-step'\nimport { aiJobStepBudget } from './ai-job-budget'\nimport { registerAiJobStep } from './ai-jobs'\n\n/**\n * The template step (AGL-2909): the generation step of a `template` job, run\n * once a member confirmed the job's plan.\n *\n * It builds ONE page template — the page a site renders once per record: a\n * content collection's entry, a product, an author — through\n * `runValidatedGeneration('template', …)`. Everything that differs per\n * record is a binding token from the subject's vocabulary\n * (`model/ai-template-subjects.ts`), which is the list the besigner's insert\n * picker offers for that page; a link or media prop may hold one of those\n * tokens whole, which the palette validator admits only because this door\n * names them. The door adds one check of its own through `extend` (rule 8):\n * no token the page does not fill, an h1 bound to the subject's title, no\n * block that fills itself only on another subject's page, and no address,\n * slug or timestamp printed in its copy.\n *\n * The model is shown the platform's starter pages as examples, in a cached\n * block (`runtime/ai-template-examples.ts`), and writes against them.\n *\n * The template lands in the site's library as a new draft\n * (`ai-job-drafts.ts`), inert until a member uses it: bound to no\n * collection, set as no product or author page, served nowhere. A plan that\n * starts from a copy of a template the site has gets that copy instead.\n */\n\n/** The longest answer a template may run to; the tree is held to the template budget either way. */\nexport const AI_JOB_TEMPLATE_MAX_TOKENS = AI_ROUTING_TABLE['job.template'].maxTokens\n\n/**\n * The template step's time (AGL-3035, AGL-3036): its two inventory-lookup\n * rounds, its answer and its re-ask at the routing table's ceiling on the tier\n * `job.template` is served from, fitted to what a beat can give a step. It\n * runs on the job beat, never at an inline door, and asks for no more of the\n * ceiling than fits that time on the model it runs.\n */\nexport const AI_JOB_TEMPLATE_STEP_BUDGET = aiJobStepBudget({\n tier: AI_STEP_TIERS['job.template'],\n maxTokens: AI_JOB_TEMPLATE_MAX_TOKENS,\n})\n\n/** The least time one template step needs before it starts. */\nexport const AI_JOB_TEMPLATE_STEP_MINIMUM_MS = AI_JOB_TEMPLATE_STEP_BUDGET.minimumMs\n\n/** The template step's own instructions, cached after the doctrine and before the examples. */\nexport const AI_JOB_TEMPLATE_INSTRUCTIONS: readonly AiSystemBlock[] = [\n {\n text: [\n 'You build one page template: the page a website renders once for each record it keeps, such as each entry of a content collection, each product or each author. Answer with submit_template: the whole page as one flat node map.',\n 'Everything that differs from one record to the next is a binding token, never typed copy. The request names what the page is for, the tokens that page fills and the blocks that fill themselves there; bind only those tokens.',\n 'The page’s one h1 holds the title token the request names. Copy that reads the same on every record, such as a section heading, is written in the site’s voice.',\n 'A link or media prop holds exactly one token from the request’s link and media tokens, such as an Image whose src is {{entry.coverImage}}; its alt text may combine words and tokens. Copy never prints a link or media token, a slug or a timestamp.',\n 'A template carries no third-party player. It plays a video token only when the confirmed plan lists it, in a Video (video) whose src is that token and whose poster is the cover image token.',\n 'The page renders inside the site’s layout, so it carries no header, navigation or footer of its own.',\n ].join('\\n'),\n },\n]\n\n/** A content collection an entry template is for, as the prompt and the draft name it. */\nexport interface AiTemplateCollection {\n id: string\n name: string\n slug: string\n}\n\n/** The content collection by that id on the site, or `null` when there is none. */\nexport async function readAiTemplateCollection(\n firestore: FirebaseFirestore.Firestore,\n hostId: string,\n collectionId: string,\n): Promise<AiTemplateCollection | null> {\n const snapshot = await firestore\n .collection('hosts')\n .doc(hostId)\n .collection('collections')\n .doc(collectionId)\n .get()\n if (!snapshot.exists) return null\n const data = (snapshot.data() ?? {}) as Record<string, unknown>\n if (hostCollectionKind(data) !== 'content') return null\n const text = (value: unknown) => (typeof value === 'string' ? value.trim() : '')\n const slug = text(data['slug'])\n return {\n id: collectionId,\n name: text(data['displayName']) || text(data['name']) || slug || collectionId,\n slug,\n }\n}\n\n/** The template's name: the plan's, else the collection's or the subject's. */\nexport function aiTemplateDraftName(\n definition: AiTemplateSubjectDefinition,\n collection: AiTemplateCollection | null,\n planned: string | null | undefined,\n): string {\n return planned || (collection ? `${collection.name} entry template` : definition.defaultName)\n}\n\n/** The address Use template offers for the page it creates. */\nexport function aiTemplateDraftSlug(\n definition: AiTemplateSubjectDefinition,\n collection: AiTemplateCollection | null,\n): string | null {\n return (\n normalizeScreenSlug(\n collection\n ? `${collection.slug || collection.id}-entry-template`\n : `${definition.subject}-page-template`,\n ) ?? null\n )\n}\n\n/** The template job as the generation's user turn, with the template a short copy would have started from (AGL-3143 §14). */\nexport function aiJobTemplatePrompt(\n job: Pick<AiJob, 'brief'>,\n definition: AiTemplateSubjectDefinition,\n collection: AiTemplateCollection | null,\n plan: AiJobPlan | null,\n name: string,\n source: readonly string[] = [],\n): string {\n const of = collection\n ? ` of the \"${collection.name}\" collection${collection.slug ? ` (/${collection.slug})` : ''}`\n : ''\n return [\n `Template name: ${name}`,\n `The page is for ${definition.noun}${of}.`,\n `Title token: ${definition.titleToken}`,\n `Tokens this page fills: ${definition.tokens.map((entry) => `${entry.token} ${entry.label}`).join('; ')}`,\n `Link and media tokens: ${aiTemplateAddressTokens(definition).join(', ')}`,\n definition.blocks.length\n ? `Blocks that fill themselves on this page: ${definition.blocks.join(', ')}`\n : 'No block fills itself on this page: bind the tokens.',\n aiJobBriefLine(job),\n ...aiPlanReferenceLines(plan),\n ...source,\n ].join('\\n')\n}\n\n/** Which subject's page each self-filling block belongs to. */\nconst BLOCK_SUBJECTS = new Map<string, AiTemplateSubjectDefinition>(\n AI_TEMPLATE_SUBJECTS.flatMap((subject) =>\n AI_TEMPLATE_SUBJECT_DEFINITIONS[subject].blocks.map(\n (block) => [block, AI_TEMPLATE_SUBJECT_DEFINITIONS[subject]] as const,\n ),\n ),\n)\n\n/** Every string a prop value holds, however deeply. */\nfunction stringsIn(value: unknown, out: string[] = []): string[] {\n if (typeof value === 'string') out.push(value)\n else if (Array.isArray(value)) for (const inner of value) stringsIn(inner, out)\n else if (value && typeof value === 'object') {\n for (const inner of Object.values(value as Record<string, unknown>)) stringsIn(inner, out)\n }\n return out\n}\n\n/** The page's title: a Typography that renders as an h1. */\nfunction isPageTitle(node: AiDoctrineNode): boolean {\n if (node.componentId !== 'muiTypography') return false\n const element = typeof node.props?.['component'] === 'string' ? String(node.props['component']) : ''\n return (element || String(node.props?.['variant'] ?? '')) === 'h1'\n}\n\n/**\n * The tokens a prop prints for a reader that no reader reads: a link or\n * picture token, a slug or a timestamp, in copy (AGL-3143 §16). Copy is a\n * prop the palette gives the text role, read as words where it holds more\n * than one token or is the element's children. A block handed one token\n * whole reads it as a value, as Entry Meta's avatar takes the author's\n * portrait and a Video's publication date takes the timestamp, and a\n * markdown link's target is followed, not read.\n */\nfunction printedTokens(node: AiDoctrineNode): string[] {\n const roles = AI_PALETTE[node.componentId]?.propRoles ?? {}\n return Object.entries(node.props ?? {}).flatMap(([key, value]) => {\n if (typeof value !== 'string' || roles[key] !== 'text') return []\n const tokens = aiBindingTokensIn(value.replace(/\\]\\([^)]*\\)/g, ']'))\n const unread = tokens.filter((token) => isAiAddressToken(token) || isAiUnreadToken(token))\n const whole = tokens.length === 1 && value.trim() === tokens[0]\n return unread.length && (key === 'children' || !whole) ? unread : []\n })\n}\n\n/**\n * The template door's check on a tree the doctrine admitted (rule 8): every\n * token is one the page fills, the h1 binds the subject's title, no block\n * that fills itself on another subject's page is placed — except inside a\n * Collection Entries card, where each fills from the card's own entry — and\n * no copy prints an address, a slug or a timestamp.\n */\nexport function aiTemplateBindingCheck(\n definition: AiTemplateSubjectDefinition,\n): (tree: AiValidatedTree) => AiDoctrineViolation[] {\n const allowed = new Set(definition.tokens.map((entry) => entry.token))\n return (tree) => {\n const nodes = tree.nodes as unknown as Record<string, AiDoctrineNode>\n const unknown = new Set<string>()\n const unknownAt: string[] = []\n const foreign: Array<{ id: string; componentId: string; owner: AiTemplateSubjectDefinition }> = []\n const titles: string[] = []\n const printed = new Set<string>()\n const printedAt: string[] = []\n let titleBound = false\n for (const { id, node, ancestors } of walkTree({ rootId: tree.rootId, nodes })) {\n const tokens = stringsIn(node.props).flatMap(aiBindingTokensIn)\n const strays = tokens.filter((token) => !allowed.has(token))\n if (strays.length) {\n for (const token of strays) unknown.add(token)\n unknownAt.push(id)\n }\n const shown = printedTokens(node).filter((token) => allowed.has(token))\n if (shown.length) {\n for (const token of shown) printed.add(token)\n printedAt.push(id)\n }\n const owner = BLOCK_SUBJECTS.get(node.componentId)\n const inCard = ancestors.some((ancestor) => nodes[ancestor]?.componentId === 'collectionEntries')\n if (owner && owner.subject !== definition.subject && !inCard) {\n foreign.push({ id, componentId: node.componentId, owner })\n }\n if (isPageTitle(node)) {\n titles.push(id)\n if (tokens.includes(definition.titleToken)) titleBound = true\n }\n }\n const violations: AiDoctrineViolation[] = []\n if (unknown.size) {\n violations.push({\n rule: 8,\n code: 'unknown-binding',\n message: `This binds ${[...unknown].slice(0, 3).join(', ')}, which ${definition.noun}’s page does not fill. Bind only the tokens listed for this page.`,\n nodeIds: aiModelNodeIds(unknownAt, tree.sourceIds),\n })\n }\n if (printed.size) {\n violations.push({\n rule: 8,\n code: 'printed-token',\n message: `This prints ${[...printed].slice(0, 3).join(', ')} in the page's copy, where a reader sees an address, a slug or a timestamp. A link or picture token goes whole in a link or media prop, and a slug or a timestamp stays off the page.`,\n nodeIds: aiModelNodeIds(printedAt, tree.sourceIds),\n })\n }\n // The doctrine already refuses a page with no h1 or with several.\n if (titles.length === 1 && !titleBound) {\n violations.push({\n rule: 8,\n code: 'typed-title',\n message: `The page title is typed, so every record would show the same one. Put ${definition.titleToken} in the h1.`,\n nodeIds: aiModelNodeIds(titles, tree.sourceIds),\n })\n }\n if (foreign.length) {\n const [first] = foreign\n violations.push({\n rule: 8,\n code: 'foreign-block',\n message: `The ${AI_PALETTE[first.componentId]?.displayName ?? first.componentId} block fills itself only on ${first.owner.noun}’s page, and this template is for ${definition.noun}. Remove it.`,\n nodeIds: aiModelNodeIds(\n foreign.map((entry) => entry.id),\n tree.sourceIds,\n ),\n })\n }\n return violations\n }\n}\n\nexport interface AiTemplateJobAdmissionDeps {\n readCollection?: typeof readAiTemplateCollection\n}\n\n/**\n * A template job is admitted with inputs that say what the page is for, for\n * a site of the job's own org that holds the collection it names, with a\n * template to spare.\n */\nexport function createAiTemplateJobAdmission(deps: AiTemplateJobAdmissionDeps = {}): AiJobAdmission {\n const readCollection = deps.readCollection ?? readAiTemplateCollection\n return async (context) => {\n const inputs = parseAiTemplateJobInputs(context.inputs)\n if (typeof inputs === 'string') return { status: 400, error: inputs }\n return aiDraftAdmissionRefusal(context.firestore, {\n orgId: context.orgId,\n hostId: context.hostId,\n kind: 'template',\n org: context.org,\n ownCheck: async (hostId) =>\n inputs.collectionId && !(await readCollection(context.firestore, hostId, inputs.collectionId))\n ? { status: 404, error: 'That content collection is not on this site' }\n : aiJobPlanCreationsRefusal('template', context, hostId),\n })\n }\n}\n\nexport interface AiJobTemplateStepDeps {\n /** The inventory reader; specs hand in a fake. */\n readInventory?: typeof readSiteInventory\n /** The platform's duplicate module, for a plan that starts from a copy. */\n duplicate?: typeof duplicateResource\n readCollection?: typeof readAiTemplateCollection\n /** The starter examples block; built from the starters once per process otherwise. */\n examples?: () => AiSystemBlock\n}\n\n/**\n * The binding tokens the confirmed plan gives the template, against the\n * template a COPY actually produced (AGL-3024); `null` when the copy keeps\n * the plan, or when the plan promised no token to keep.\n *\n * The same hole as `aiCopiedLayoutReview` and `aiCopiedPageReview`, on the\n * third kind that can start from a duplicate. The generate path already holds\n * a built template to `aiPlanTemplateTokenViolations`; the copy path reached\n * none of it, so a plan reading \"duplicate it and bind the author's photo\"\n * got the duplicate and reported Done.\n *\n * A copy that cannot be read back settles nothing, and a promise this step\n * cannot settle is reviewed by a person rather than reported as kept.\n */\nexport async function aiCopiedTemplateReview(\n firestore: FirebaseFirestore.Firestore,\n input: {\n hostId: string\n id: string\n name: string\n plan: AiJobPlan | null\n definition: AiTemplateSubjectDefinition\n },\n): Promise<AiJobReview | null> {\n const { tokens, unreadable } = aiPlannedTemplateTokens(input.plan, input.definition)\n if (!tokens.length && !unreadable.length) return null\n const stored = await readAiTemplateDraftNodes(firestore, {\n hostId: input.hostId,\n id: input.id,\n })\n const nodes = (stored ?? {}) as unknown as Record<string, AiDoctrineNode>\n if (!nodes[CANVAS_ROOT_ELEMENT_ID]) {\n return {\n reason: 'doctrine',\n message: `\"${input.name}\" was copied from the template the plan names, and this job could not read the copy back to check it shows what the plan gives it. Open the template and check it before you apply it.`,\n findings: [],\n }\n }\n const violations = aiPlanTemplateTokenViolations(input.plan, input.definition, {\n rootId: CANVAS_ROOT_ELEMENT_ID,\n nodes,\n })\n if (!violations.length) return null\n return aiDoctrineReview({\n message: `\"${input.name}\" is a copy of the template the plan names, and a copy is all it is: ${violations[0].message}`,\n violations,\n })\n}\n\nexport function createAiJobTemplateStep(deps: AiJobTemplateStepDeps = {}): AiJobStepRunner {\n const readInventory = deps.readInventory ?? readSiteInventory\n const duplicate = deps.duplicate ?? duplicateResource\n const readCollection = deps.readCollection ?? readAiTemplateCollection\n const examples = deps.examples ?? aiTemplateExamplesSystemBlock\n return async ({ job, now, signal, firestore, modelFor }) => {\n const hostId = job.hostId\n if (!hostId) throw new Error('a template job names no site, and its admission refuses one')\n const inputs = parseAiTemplateJobInputs(job.inputs)\n if (typeof inputs === 'string') {\n throw new Error(`a template job's inputs no longer read as they were admitted: ${inputs}`)\n }\n const definition = AI_TEMPLATE_SUBJECT_DEFINITIONS[inputs.subject]\n const output = (draft: AiDraftRecord, load?: AiLoadEstimate | null): AiJobOutput => ({\n resource: 'template',\n id: draft.id,\n versionId: null,\n hostId,\n hostSubdomain: draft.hostSubdomain,\n label: draft.name,\n ...(load ? { load } : {}),\n })\n // The switch's answer for this job, else the routing table's (AGL-2942).\n const model = modelFor?.('job.template') ?? aiModelForStep('job.template')\n\n // A run cut off after its draft was written reports that draft, found by the id the job recorded.\n const draftId = aiJobDraftId(job, 'template')\n const written = await readAiDraft(firestore, { kind: 'template', hostId, id: draftId })\n if (written) return aiUnspentOutcome(model, { outputs: [output(written)] })\n\n const [inventory, orgSnapshot, collection] = await Promise.all([\n readInventory(job.orgId, hostId, { firestore }),\n firestore.collection('orgs').doc(job.orgId).get(),\n inputs.collectionId\n ? readCollection(firestore, hostId, inputs.collectionId)\n : Promise.resolve(null),\n ])\n if (inputs.collectionId && !collection) {\n throw new Error(`collection ${inputs.collectionId} left site ${hostId} after its template job was admitted`)\n }\n const org = (orgSnapshot.data() ?? null) as Partial<AglynOrgBilling> | null\n const plan = aiConfirmedPlan(job)\n const creation = aiPlanCreation(plan, 'template')\n const name = aiTemplateDraftName(definition, collection, creation?.name)\n\n // The plan starts from a copy of a template the site has (rule 15): the\n // copy is the draft, and nothing is generated. A source gone since the\n // plan was made is built from the brief instead, and one that cannot keep\n // the plan's tokens is built FROM (AGL-3143 §14): copying it could only end\n // in the review below. A source this step cannot read still copies.\n const source = creation?.duplicateOf\n ? inventory.templates.find((template) => template.id === creation.duplicateOf)\n : undefined\n const sourceReview = source\n ? await aiCopiedTemplateReview(firestore, { hostId, id: source.id, name: source.name, plan, definition })\n : null\n const sourceShort = Boolean(sourceReview?.findings.length)\n const sourceLines =\n source && sourceShort\n ? aiCopySourceLines(\n 'template',\n source.name,\n (await readAiTemplateDraftNodes(firestore, { hostId, id: source.id })) as Record<string, unknown> | null,\n )\n : []\n if (creation?.duplicateOf && source && !sourceShort) {\n const copy = await duplicate('template', {\n orgId: job.orgId,\n hostId,\n sourceId: creation.duplicateOf,\n name,\n uid: job.createdBy,\n org: org as Record<string, unknown> | null,\n // The same claim the page step makes (AGL-3024): a copy core mints is\n // written under core's own id, never `draftId`, so a step that runs\n // again — a pass resumed, a run cut off after the copy — would mint a\n // second. The job's recorded id makes core replay the one it made.\n attemptKey: draftId,\n })\n if (copy.ok) {\n const hostSubdomain = await aiSiteSubdomain(firestore, hostId)\n // The copy is reported with what the plan promised of it read back\n // (AGL-3024): this branch generates nothing, so nothing else can.\n const review = await aiCopiedTemplateReview(firestore, {\n hostId,\n id: copy.id,\n name: copy.name,\n plan,\n definition,\n })\n if (review) return aiUnspentOutcome(model, { review })\n return aiUnspentOutcome(model, {\n outputs: [output({ id: copy.id, versionId: null, name: copy.name, hostSubdomain })],\n })\n }\n if (copy.ok === false && copy.status === 403) {\n return aiUnspentOutcome(model, { review: aiLimitReview(copy.error) })\n }\n }\n\n const allowance = await aiDraftAllowanceRefusal(firestore, { kind: 'template', hostId, org })\n if (allowance) return aiUnspentOutcome(model, { review: aiLimitReview(allowance) })\n\n const result = await runValidatedGeneration('template', {\n step: 'job.template',\n model,\n instructions: [...AI_JOB_TEMPLATE_INSTRUCTIONS, examples()],\n inventory,\n messages: [\n {\n role: 'user',\n content: aiJobTemplatePrompt(job, definition, collection, plan, name, sourceLines),\n },\n ],\n tool: aiDoctrineTreeTool('template'),\n maxTokens: AI_JOB_TEMPLATE_STEP_BUDGET.maxTokens(model),\n ...(AI_ROUTING_TABLE['job.template'].thinking ? { thinking: AI_ROUTING_TABLE['job.template'].thinking } : {}),\n ...(AI_ROUTING_TABLE['job.template'].effort ? { effort: AI_ROUTING_TABLE['job.template'].effort } : {}),\n context: { bindingTokens: aiTemplateAddressTokens(definition) },\n // Rule 8's binding check, and the confirmed plan kept in both halves: a\n // token the plan promised that the template does not bind (AGL-3143\n // §11), and a component the plan reuses that it does not place. The\n // layout and component doors hold their builds to that reuse; the\n // template door did not, so it could drop a promised component and\n // still answer `Done` (AGL-3024).\n extend: (tree) => [\n ...aiTemplateBindingCheck(definition)(tree),\n ...aiPlanTemplateTokenViolations(plan, definition, {\n rootId: tree.rootId,\n nodes: tree.nodes as unknown as Record<string, AiDoctrineNode>,\n }),\n ...aiPlanReuseViolations(inventory, plan, aiPlacedComponentIds(tree), 'template'),\n ],\n ...(signal ? { signal } : {}),\n })\n const spent = aiGenerationSpent(result)\n if (result.status === 'refused') return { ...spent, refused: true }\n if (result.status === 'needs_input') return { ...spent, review: aiDoctrineReview(result) }\n\n // Never written once the job is canceled (AGL-3616).\n throwIfAiJobCanceled(signal)\n const draft = await writeAiDraft(firestore, {\n kind: 'template',\n hostId,\n id: draftId,\n uid: job.createdBy,\n org,\n name,\n // The site's entrance motion, as its pages have (AGL-3838): a post or a\n // record opens the way the rest of the site does.\n nodes: aiWithSiteMotion(result.value.nodes, aiLayoutMotionSiteOf(job, ''), { rootId: result.value.rootId }),\n slug: aiTemplateDraftSlug(definition, collection),\n now,\n })\n if (draft.ok === false) {\n if (draft.status === 404) throw new Error(`site ${hostId} vanished while its template was generated`)\n return { ...spent, review: aiLimitReview(draft.error) }\n }\n return { ...spent, outputs: [output(draft, result.value.load)] }\n }\n}\n\n/** The template step, building the components and forms its plan creates before the template (AGL-3143 §15). */\nexport const runAiJobTemplateStep = aiBuildingPlanCreations('template', 'job.template', createAiJobTemplateStep())\n\n/** Registers the template step and the check a template job passes before it is created or resumed. */\nexport function registerAiTemplateJob(): void {\n registerAiJobStep('template', runAiJobTemplateStep, {\n minimumMs: AI_JOB_TEMPLATE_STEP_MINIMUM_MS,\n minimumMsFor: aiPlanCreationsRunMinimumMs('template'),\n })\n registerAiJobAdmission('template', createAiTemplateJobAdmission())\n}\n"],"names":["throwIfAiJobCanceled","hostCollectionKind","normalizeScreenSlug","duplicateResource","CANVAS_ROOT_ELEMENT_ID","AI_TEMPLATE_SUBJECT_DEFINITIONS","AI_TEMPLATE_SUBJECTS","aiBindingTokensIn","aiTemplateAddressTokens","isAiAddressToken","isAiUnreadToken","parseAiTemplateJobInputs","AI_STEP_TIERS","AI_ROUTING_TABLE","aiModelForStep","aiDoctrineTreeTool","runValidatedGeneration","walkTree","AI_PALETTE","aiTemplateExamplesSystemBlock","readSiteInventory","registerAiJobAdmission","aiJobDraftId","aiLayoutMotionSiteOf","aiWithSiteMotion","aiDraftAdmissionRefusal","aiDraftAllowanceRefusal","aiSiteSubdomain","readAiDraft","readAiTemplateDraftNodes","writeAiDraft","aiConfirmedPlan","aiCopySourceLines","aiDoctrineReview","aiGenerationSpent","aiJobBriefLine","aiLimitReview","aiModelNodeIds","aiPlacedComponentIds","aiPlanCreation","aiPlanReferenceLines","aiPlanReuseViolations","aiUnspentOutcome","aiPlannedTemplateTokens","aiPlanTemplateTokenViolations","aiBuildingPlanCreations","aiJobPlanCreationsRefusal","aiPlanCreationsRunMinimumMs","aiJobStepBudget","registerAiJobStep","AI_JOB_TEMPLATE_MAX_TOKENS","maxTokens","AI_JOB_TEMPLATE_STEP_BUDGET","tier","AI_JOB_TEMPLATE_STEP_MINIMUM_MS","minimumMs","AI_JOB_TEMPLATE_INSTRUCTIONS","text","join","readAiTemplateCollection","firestore","hostId","collectionId","snapshot","collection","doc","get","exists","data","value","trim","slug","id","name","aiTemplateDraftName","definition","planned","defaultName","aiTemplateDraftSlug","subject","aiJobTemplatePrompt","job","plan","source","of","noun","titleToken","tokens","map","entry","token","label","blocks","length","BLOCK_SUBJECTS","Map","flatMap","block","stringsIn","out","push","Array","isArray","inner","Object","values","isPageTitle","node","componentId","element","props","String","printedTokens","roles","propRoles","entries","key","replace","unread","filter","whole","aiTemplateBindingCheck","allowed","Set","tree","nodes","unknown","unknownAt","foreign","titles","printed","printedAt","titleBound","ancestors","rootId","strays","has","add","shown","owner","inCard","some","ancestor","includes","violations","size","rule","code","message","slice","nodeIds","sourceIds","first","displayName","createAiTemplateJobAdmission","deps","readCollection","context","inputs","status","error","orgId","kind","org","ownCheck","aiCopiedTemplateReview","input","unreadable","stored","reason","findings","createAiJobTemplateStep","readInventory","duplicate","examples","now","signal","modelFor","orgSnapshot","Error","output","draft","load","resource","versionId","hostSubdomain","model","draftId","written","outputs","inventory","Promise","all","resolve","creation","duplicateOf","templates","find","template","undefined","sourceReview","sourceShort","Boolean","sourceLines","copy","sourceId","uid","createdBy","attemptKey","ok","review","allowance","result","step","instructions","messages","role","content","tool","thinking","effort","bindingTokens","extend","spent","refused","runAiJobTemplateStep","registerAiTemplateJob","minimumMsFor"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,oBAAoB,QAAQ,qBAAiB;AACtD,SAASC,kBAAkB,QAAQ,yCAAwC;AAC3E,SAASC,mBAAmB,QAAQ,sCAAqC;AAEzE,SAASC,iBAAiB,QAAQ,qDAAoD;AACtF,SAASC,sBAAsB,QAAQ,2CAA0C;AAEjF,SACEC,+BAA+B,EAC/BC,oBAAoB,EACpBC,iBAAiB,EACjBC,uBAAuB,EACvBC,gBAAgB,EAChBC,eAAe,EACfC,wBAAwB,QAEnB,mCAA+B;AACtC,SAASC,aAAa,QAAQ,0BAAsB;AACpD,SAASC,gBAAgB,EAAEC,cAAc,QAAQ,0BAAsB;AACvE,SACEC,kBAAkB,EAClBC,sBAAsB,QAEjB,4BAAwB;AAC/B,SACEC,QAAQ,QAGH,uCAAmC;AAE1C,SAASC,UAAU,QAAQ,qCAAiC;AAE5D,SAASC,6BAA6B,QAAQ,qCAAiC;AAC/E,SAASC,iBAAiB,QAAQ,+BAA2B;AAC7D,SAASC,sBAAsB,QAA6B,wBAAoB;AAChF,SAASC,YAAY,QAAQ,wBAAoB;AACjD,SAASC,oBAAoB,QAAQ,4BAAwB;AAC7D,SAASC,gBAAgB,QAAQ,yCAAqC;AACtE,SACEC,uBAAuB,EACvBC,uBAAuB,EACvBC,eAAe,EACfC,WAAW,EACXC,wBAAwB,EACxBC,YAAY,QAEP,qBAAiB;AACxB,SACEC,eAAe,EACfC,iBAAiB,EACjBC,gBAAgB,EAChBC,iBAAiB,EACjBC,cAAc,EACdC,aAAa,EACbC,cAAc,EACdC,oBAAoB,EACpBC,cAAc,EACdC,oBAAoB,EACpBC,qBAAqB,EACrBC,gBAAgB,QACX,yBAAqB;AAC5B,SAASC,uBAAuB,EAAEC,6BAA6B,QAAQ,+BAA2B;AAClG,SACEC,uBAAuB,EACvBC,yBAAyB,EACzBC,2BAA2B,QACtB,6BAAyB;AAEhC,SAASC,eAAe,QAAQ,qBAAiB;AACjD,SAASC,iBAAiB,QAAQ,eAAW;AAE7C;;;;;;;;;;;;;;;;;;;;;;;CAuBC,GAED,kGAAkG,GAClG,OAAO,MAAMC,6BAA6BrC,gBAAgB,CAAC,eAAe,CAACsC,SAAS,CAAA;AAEpF;;;;;;CAMC,GACD,OAAO,MAAMC,8BAA8BJ,gBAAgB;IACzDK,MAAMzC,aAAa,CAAC,eAAe;IACnCuC,WAAWD;AACb,GAAE;AAEF,6DAA6D,GAC7D,OAAO,MAAMI,kCAAkCF,4BAA4BG,SAAS,CAAA;AAEpF,6FAA6F,GAC7F,OAAO,MAAMC,+BAAyD;IACpE;QACEC,MAAM;YACJ;YACA;YACA;YACA;YACA;YACA;SACD,CAACC,IAAI,CAAC;IACT;CACD,CAAA;AASD,iFAAiF,GACjF,OAAO,eAAeC,yBACpBC,SAAsC,EACtCC,MAAc,EACdC,YAAoB;QASNC;IAPd,MAAMA,WAAW,MAAMH,UACpBI,UAAU,CAAC,SACXC,GAAG,CAACJ,QACJG,UAAU,CAAC,eACXC,GAAG,CAACH,cACJI,GAAG;IACN,IAAI,CAACH,SAASI,MAAM,EAAE,OAAO;IAC7B,MAAMC,QAAQL,iBAAAA,SAASK,IAAI,cAAbL,iBAAmB,CAAC;IAClC,IAAI9D,mBAAmBmE,UAAU,WAAW,OAAO;IACnD,MAAMX,OAAO,CAACY,QAAoB,OAAOA,UAAU,WAAWA,MAAMC,IAAI,KAAK;IAC7E,MAAMC,OAAOd,KAAKW,IAAI,CAAC,OAAO;IAC9B,OAAO;QACLI,IAAIV;QACJW,MAAMhB,KAAKW,IAAI,CAAC,cAAc,KAAKX,KAAKW,IAAI,CAAC,OAAO,KAAKG,QAAQT;QACjES;IACF;AACF;AAEA,6EAA6E,GAC7E,OAAO,SAASG,oBACdC,UAAuC,EACvCX,UAAuC,EACvCY,OAAkC;IAElC,OAAOA,WAAYZ,CAAAA,aAAa,GAAGA,WAAWS,IAAI,CAAC,eAAe,CAAC,GAAGE,WAAWE,WAAW,AAAD;AAC7F;AAEA,6DAA6D,GAC7D,OAAO,SAASC,oBACdH,UAAuC,EACvCX,UAAuC;QAGrC9D;IADF,QACEA,uBAAAA,oBACE8D,aACI,GAAGA,WAAWO,IAAI,IAAIP,WAAWQ,EAAE,CAAC,eAAe,CAAC,GACpD,GAAGG,WAAWI,OAAO,CAAC,cAAc,CAAC,aAH3C7E,uBAIK;AAET;AAEA,2HAA2H,GAC3H,OAAO,SAAS8E,oBACdC,GAAyB,EACzBN,UAAuC,EACvCX,UAAuC,EACvCkB,IAAsB,EACtBT,IAAY,EACZU,SAA4B,EAAE;IAE9B,MAAMC,KAAKpB,aACP,CAAC,SAAS,EAAEA,WAAWS,IAAI,CAAC,YAAY,EAAET,WAAWO,IAAI,GAAG,CAAC,GAAG,EAAEP,WAAWO,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,GAC3F;IACJ,OAAO;QACL,CAAC,eAAe,EAAEE,MAAM;QACxB,CAAC,gBAAgB,EAAEE,WAAWU,IAAI,GAAGD,GAAG,CAAC,CAAC;QAC1C,CAAC,aAAa,EAAET,WAAWW,UAAU,EAAE;QACvC,CAAC,wBAAwB,EAAEX,WAAWY,MAAM,CAACC,GAAG,CAAC,CAACC,QAAU,GAAGA,MAAMC,KAAK,CAAC,CAAC,EAAED,MAAME,KAAK,EAAE,EAAEjC,IAAI,CAAC,OAAO;QACzG,CAAC,uBAAuB,EAAElD,wBAAwBmE,YAAYjB,IAAI,CAAC,OAAO;QAC1EiB,WAAWiB,MAAM,CAACC,MAAM,GACpB,CAAC,0CAA0C,EAAElB,WAAWiB,MAAM,CAAClC,IAAI,CAAC,OAAO,GAC3E;QACJvB,eAAe8C;WACZzC,qBAAqB0C;WACrBC;KACJ,CAACzB,IAAI,CAAC;AACT;AAEA,6DAA6D,GAC7D,MAAMoC,iBAAiB,IAAIC,IACzBzF,qBAAqB0F,OAAO,CAAC,CAACjB,UAC5B1E,+BAA+B,CAAC0E,QAAQ,CAACa,MAAM,CAACJ,GAAG,CACjD,CAACS,QAAU;YAACA;YAAO5F,+BAA+B,CAAC0E,QAAQ;SAAC;AAKlE,qDAAqD,GACrD,SAASmB,UAAU7B,KAAc,EAAE8B,MAAgB,EAAE;IACnD,IAAI,OAAO9B,UAAU,UAAU8B,IAAIC,IAAI,CAAC/B;SACnC,IAAIgC,MAAMC,OAAO,CAACjC,QAAQ,KAAK,MAAMkC,SAASlC,MAAO6B,UAAUK,OAAOJ;SACtE,IAAI9B,SAAS,OAAOA,UAAU,UAAU;QAC3C,KAAK,MAAMkC,SAASC,OAAOC,MAAM,CAACpC,OAAmC6B,UAAUK,OAAOJ;IACxF;IACA,OAAOA;AACT;AAEA,0DAA0D,GAC1D,SAASO,YAAYC,IAAoB;;QAEhBA,aACGA;IAF1B,IAAIA,KAAKC,WAAW,KAAK,iBAAiB,OAAO;IACjD,MAAMC,UAAU,SAAOF,cAAAA,KAAKG,KAAK,qBAAVH,WAAY,CAAC,YAAY,MAAK,WAAWI,OAAOJ,KAAKG,KAAK,CAAC,YAAY,IAAI;IAClG,OAAO,AAACD,CAAAA,WAAWE,gBAAOJ,eAAAA,KAAKG,KAAK,qBAAVH,YAAY,CAAC,UAAU,mBAAI,GAAE,MAAO;AAChE;AAEA;;;;;;;;CAQC,GACD,SAASK,cAAcL,IAAoB;cAEnBA;QADRzF;IAAd,MAAM+F,iBAAQ/F,+BAAAA,UAAU,CAACyF,KAAKC,WAAW,CAAC,qBAA5B1F,6BAA8BgG,SAAS,mBAAI,CAAC;IAC1D,OAAOV,OAAOW,OAAO,EAACR,cAAAA,KAAKG,KAAK,YAAVH,cAAc,CAAC,GAAGX,OAAO,CAAC,CAAC,CAACoB,KAAK/C,MAAM;QAC3D,IAAI,OAAOA,UAAU,YAAY4C,KAAK,CAACG,IAAI,KAAK,QAAQ,OAAO,EAAE;QACjE,MAAM7B,SAAShF,kBAAkB8D,MAAMgD,OAAO,CAAC,gBAAgB;QAC/D,MAAMC,SAAS/B,OAAOgC,MAAM,CAAC,CAAC7B,QAAUjF,iBAAiBiF,UAAUhF,gBAAgBgF;QACnF,MAAM8B,QAAQjC,OAAOM,MAAM,KAAK,KAAKxB,MAAMC,IAAI,OAAOiB,MAAM,CAAC,EAAE;QAC/D,OAAO+B,OAAOzB,MAAM,IAAKuB,CAAAA,QAAQ,cAAc,CAACI,KAAI,IAAKF,SAAS,EAAE;IACtE;AACF;AAEA;;;;;;CAMC,GACD,OAAO,SAASG,uBACd9C,UAAuC;IAEvC,MAAM+C,UAAU,IAAIC,IAAIhD,WAAWY,MAAM,CAACC,GAAG,CAAC,CAACC,QAAUA,MAAMC,KAAK;IACpE,OAAO,CAACkC;QACN,MAAMC,QAAQD,KAAKC,KAAK;QACxB,MAAMC,UAAU,IAAIH;QACpB,MAAMI,YAAsB,EAAE;QAC9B,MAAMC,UAA0F,EAAE;QAClG,MAAMC,SAAmB,EAAE;QAC3B,MAAMC,UAAU,IAAIP;QACpB,MAAMQ,YAAsB,EAAE;QAC9B,IAAIC,aAAa;QACjB,KAAK,MAAM,EAAE5D,EAAE,EAAEmC,IAAI,EAAE0B,SAAS,EAAE,IAAIpH,SAAS;YAAEqH,QAAQV,KAAKU,MAAM;YAAET;QAAM,GAAI;YAC9E,MAAMtC,SAASW,UAAUS,KAAKG,KAAK,EAAEd,OAAO,CAACzF;YAC7C,MAAMgI,SAAShD,OAAOgC,MAAM,CAAC,CAAC7B,QAAU,CAACgC,QAAQc,GAAG,CAAC9C;YACrD,IAAI6C,OAAO1C,MAAM,EAAE;gBACjB,KAAK,MAAMH,SAAS6C,OAAQT,QAAQW,GAAG,CAAC/C;gBACxCqC,UAAU3B,IAAI,CAAC5B;YACjB;YACA,MAAMkE,QAAQ1B,cAAcL,MAAMY,MAAM,CAAC,CAAC7B,QAAUgC,QAAQc,GAAG,CAAC9C;YAChE,IAAIgD,MAAM7C,MAAM,EAAE;gBAChB,KAAK,MAAMH,SAASgD,MAAOR,QAAQO,GAAG,CAAC/C;gBACvCyC,UAAU/B,IAAI,CAAC5B;YACjB;YACA,MAAMmE,QAAQ7C,eAAe5B,GAAG,CAACyC,KAAKC,WAAW;YACjD,MAAMgC,SAASP,UAAUQ,IAAI,CAAC,CAACC;oBAAajB;uBAAAA,EAAAA,kBAAAA,KAAK,CAACiB,SAAS,qBAAfjB,gBAAiBjB,WAAW,MAAK;;YAC7E,IAAI+B,SAASA,MAAM5D,OAAO,KAAKJ,WAAWI,OAAO,IAAI,CAAC6D,QAAQ;gBAC5DZ,QAAQ5B,IAAI,CAAC;oBAAE5B;oBAAIoC,aAAaD,KAAKC,WAAW;oBAAE+B;gBAAM;YAC1D;YACA,IAAIjC,YAAYC,OAAO;gBACrBsB,OAAO7B,IAAI,CAAC5B;gBACZ,IAAIe,OAAOwD,QAAQ,CAACpE,WAAWW,UAAU,GAAG8C,aAAa;YAC3D;QACF;QACA,MAAMY,aAAoC,EAAE;QAC5C,IAAIlB,QAAQmB,IAAI,EAAE;YAChBD,WAAW5C,IAAI,CAAC;gBACd8C,MAAM;gBACNC,MAAM;gBACNC,SAAS,CAAC,WAAW,EAAE;uBAAItB;iBAAQ,CAACuB,KAAK,CAAC,GAAG,GAAG3F,IAAI,CAAC,MAAM,QAAQ,EAAEiB,WAAWU,IAAI,CAAC,iEAAiE,CAAC;gBACvJiE,SAASjH,eAAe0F,WAAWH,KAAK2B,SAAS;YACnD;QACF;QACA,IAAIrB,QAAQe,IAAI,EAAE;YAChBD,WAAW5C,IAAI,CAAC;gBACd8C,MAAM;gBACNC,MAAM;gBACNC,SAAS,CAAC,YAAY,EAAE;uBAAIlB;iBAAQ,CAACmB,KAAK,CAAC,GAAG,GAAG3F,IAAI,CAAC,MAAM,qLAAqL,CAAC;gBAClP4F,SAASjH,eAAe8F,WAAWP,KAAK2B,SAAS;YACnD;QACF;QACA,kEAAkE;QAClE,IAAItB,OAAOpC,MAAM,KAAK,KAAK,CAACuC,YAAY;YACtCY,WAAW5C,IAAI,CAAC;gBACd8C,MAAM;gBACNC,MAAM;gBACNC,SAAS,CAAC,sEAAsE,EAAEzE,WAAWW,UAAU,CAAC,WAAW,CAAC;gBACpHgE,SAASjH,eAAe4F,QAAQL,KAAK2B,SAAS;YAChD;QACF;QACA,IAAIvB,QAAQnC,MAAM,EAAE;;gBAKA3E;YAJlB,MAAM,CAACsI,MAAM,GAAGxB;YAChBgB,WAAW5C,IAAI,CAAC;gBACd8C,MAAM;gBACNC,MAAM;gBACNC,SAAS,CAAC,IAAI,WAAElI,gCAAAA,UAAU,CAACsI,MAAM5C,WAAW,CAAC,qBAA7B1F,8BAA+BuI,WAAW,mBAAID,MAAM5C,WAAW,CAAC,4BAA4B,EAAE4C,MAAMb,KAAK,CAACtD,IAAI,CAAC,kCAAkC,EAAEV,WAAWU,IAAI,CAAC,YAAY,CAAC;gBAChMiE,SAASjH,eACP2F,QAAQxC,GAAG,CAAC,CAACC,QAAUA,MAAMjB,EAAE,GAC/BoD,KAAK2B,SAAS;YAElB;QACF;QACA,OAAOP;IACT;AACF;AAMA;;;;CAIC,GACD,OAAO,SAASU,6BAA6BC,OAAmC,CAAC,CAAC;QACzDA;IAAvB,MAAMC,kBAAiBD,uBAAAA,KAAKC,cAAc,YAAnBD,uBAAuBhG;IAC9C,OAAO,OAAOkG;QACZ,MAAMC,SAASnJ,yBAAyBkJ,QAAQC,MAAM;QACtD,IAAI,OAAOA,WAAW,UAAU,OAAO;YAAEC,QAAQ;YAAKC,OAAOF;QAAO;QACpE,OAAOrI,wBAAwBoI,QAAQjG,SAAS,EAAE;YAChDqG,OAAOJ,QAAQI,KAAK;YACpBpG,QAAQgG,QAAQhG,MAAM;YACtBqG,MAAM;YACNC,KAAKN,QAAQM,GAAG;YAChBC,UAAU,OAAOvG,SACfiG,OAAOhG,YAAY,IAAI,CAAE,MAAM8F,eAAeC,QAAQjG,SAAS,EAAEC,QAAQiG,OAAOhG,YAAY,IACxF;oBAAEiG,QAAQ;oBAAKC,OAAO;gBAA8C,IACpElH,0BAA0B,YAAY+G,SAAShG;QACvD;IACF;AACF;AAYA;;;;;;;;;;;;;CAaC,GACD,OAAO,eAAewG,uBACpBzG,SAAsC,EACtC0G,KAMC;IAED,MAAM,EAAE/E,MAAM,EAAEgF,UAAU,EAAE,GAAG5H,wBAAwB2H,MAAMpF,IAAI,EAAEoF,MAAM3F,UAAU;IACnF,IAAI,CAACY,OAAOM,MAAM,IAAI,CAAC0E,WAAW1E,MAAM,EAAE,OAAO;IACjD,MAAM2E,SAAS,MAAM3I,yBAAyB+B,WAAW;QACvDC,QAAQyG,MAAMzG,MAAM;QACpBW,IAAI8F,MAAM9F,EAAE;IACd;IACA,MAAMqD,QAAS2C,iBAAAA,SAAU,CAAC;IAC1B,IAAI,CAAC3C,KAAK,CAACzH,uBAAuB,EAAE;QAClC,OAAO;YACLqK,QAAQ;YACRrB,SAAS,CAAC,CAAC,EAAEkB,MAAM7F,IAAI,CAAC,sLAAsL,CAAC;YAC/MiG,UAAU,EAAE;QACd;IACF;IACA,MAAM1B,aAAapG,8BAA8B0H,MAAMpF,IAAI,EAAEoF,MAAM3F,UAAU,EAAE;QAC7E2D,QAAQlI;QACRyH;IACF;IACA,IAAI,CAACmB,WAAWnD,MAAM,EAAE,OAAO;IAC/B,OAAO5D,iBAAiB;QACtBmH,SAAS,CAAC,CAAC,EAAEkB,MAAM7F,IAAI,CAAC,qEAAqE,EAAEuE,UAAU,CAAC,EAAE,CAACI,OAAO,EAAE;QACtHJ;IACF;AACF;AAEA,OAAO,SAAS2B,wBAAwBhB,OAA8B,CAAC,CAAC;QAChDA,qBACJA,iBACKA,sBACNA;IAHjB,MAAMiB,iBAAgBjB,sBAAAA,KAAKiB,aAAa,YAAlBjB,sBAAsBvI;IAC5C,MAAMyJ,aAAYlB,kBAAAA,KAAKkB,SAAS,YAAdlB,kBAAkBxJ;IACpC,MAAMyJ,kBAAiBD,uBAAAA,KAAKC,cAAc,YAAnBD,uBAAuBhG;IAC9C,MAAMmH,YAAWnB,iBAAAA,KAAKmB,QAAQ,YAAbnB,iBAAiBxI;IAClC,OAAO,OAAO,EAAE8D,GAAG,EAAE8F,GAAG,EAAEC,MAAM,EAAEpH,SAAS,EAAEqH,QAAQ,EAAE;kBAmCxCC;QAlCb,MAAMrH,SAASoB,IAAIpB,MAAM;QACzB,IAAI,CAACA,QAAQ,MAAM,IAAIsH,MAAM;QAC7B,MAAMrB,SAASnJ,yBAAyBsE,IAAI6E,MAAM;QAClD,IAAI,OAAOA,WAAW,UAAU;YAC9B,MAAM,IAAIqB,MAAM,CAAC,8DAA8D,EAAErB,QAAQ;QAC3F;QACA,MAAMnF,aAAatE,+BAA+B,CAACyJ,OAAO/E,OAAO,CAAC;QAClE,MAAMqG,SAAS,CAACC,OAAsBC,OAA+C;gBACnFC,UAAU;gBACV/G,IAAI6G,MAAM7G,EAAE;gBACZgH,WAAW;gBACX3H;gBACA4H,eAAeJ,MAAMI,aAAa;gBAClC9F,OAAO0F,MAAM5G,IAAI;eACb6G,OAAO;gBAAEA;YAAK,IAAI,CAAC;QAEzB,yEAAyE;QACzE,MAAMI,gBAAQT,4BAAAA,SAAW,kCAAmBnK,eAAe;QAE3D,kGAAkG;QAClG,MAAM6K,UAAUrK,aAAa2D,KAAK;QAClC,MAAM2G,UAAU,MAAMhK,YAAYgC,WAAW;YAAEsG,MAAM;YAAYrG;YAAQW,IAAImH;QAAQ;QACrF,IAAIC,SAAS,OAAOlJ,iBAAiBgJ,OAAO;YAAEG,SAAS;gBAACT,OAAOQ;aAAS;QAAC;QAEzE,MAAM,CAACE,WAAWZ,aAAalH,WAAW,GAAG,MAAM+H,QAAQC,GAAG,CAAC;YAC7DpB,cAAc3F,IAAIgF,KAAK,EAAEpG,QAAQ;gBAAED;YAAU;YAC7CA,UAAUI,UAAU,CAAC,QAAQC,GAAG,CAACgB,IAAIgF,KAAK,EAAE/F,GAAG;YAC/C4F,OAAOhG,YAAY,GACf8F,eAAehG,WAAWC,QAAQiG,OAAOhG,YAAY,IACrDiI,QAAQE,OAAO,CAAC;SACrB;QACD,IAAInC,OAAOhG,YAAY,IAAI,CAACE,YAAY;YACtC,MAAM,IAAImH,MAAM,CAAC,WAAW,EAAErB,OAAOhG,YAAY,CAAC,WAAW,EAAED,OAAO,oCAAoC,CAAC;QAC7G;QACA,MAAMsG,OAAOe,oBAAAA,YAAY9G,IAAI,cAAhB8G,oBAAsB;QACnC,MAAMhG,OAAOnD,gBAAgBkD;QAC7B,MAAMiH,WAAW3J,eAAe2C,MAAM;QACtC,MAAMT,OAAOC,oBAAoBC,YAAYX,YAAYkI,4BAAAA,SAAUzH,IAAI;QAEvE,wEAAwE;QACxE,uEAAuE;QACvE,0EAA0E;QAC1E,4EAA4E;QAC5E,oEAAoE;QACpE,MAAMU,SAAS+G,CAAAA,4BAAAA,SAAUC,WAAW,IAChCL,UAAUM,SAAS,CAACC,IAAI,CAAC,CAACC,WAAaA,SAAS9H,EAAE,KAAK0H,SAASC,WAAW,IAC3EI;QACJ,MAAMC,eAAerH,SACjB,MAAMkF,uBAAuBzG,WAAW;YAAEC;YAAQW,IAAIW,OAAOX,EAAE;YAAEC,MAAMU,OAAOV,IAAI;YAAES;YAAMP;QAAW,KACrG;QACJ,MAAM8H,cAAcC,QAAQF,gCAAAA,aAAc9B,QAAQ,CAAC7E,MAAM;QACzD,MAAM8G,cACJxH,UAAUsH,cACNzK,kBACE,YACAmD,OAAOV,IAAI,EACV,MAAM5C,yBAAyB+B,WAAW;YAAEC;YAAQW,IAAIW,OAAOX,EAAE;QAAC,MAErE,EAAE;QACR,IAAI0H,CAAAA,4BAAAA,SAAUC,WAAW,KAAIhH,UAAU,CAACsH,aAAa;YACnD,MAAMG,OAAO,MAAM/B,UAAU,YAAY;gBACvCZ,OAAOhF,IAAIgF,KAAK;gBAChBpG;gBACAgJ,UAAUX,SAASC,WAAW;gBAC9B1H;gBACAqI,KAAK7H,IAAI8H,SAAS;gBAClB5C,KAAKA;gBACL,sEAAsE;gBACtE,oEAAoE;gBACpE,sEAAsE;gBACtE,mEAAmE;gBACnE6C,YAAYrB;YACd;YACA,IAAIiB,KAAKK,EAAE,EAAE;gBACX,MAAMxB,gBAAgB,MAAM9J,gBAAgBiC,WAAWC;gBACvD,mEAAmE;gBACnE,kEAAkE;gBAClE,MAAMqJ,SAAS,MAAM7C,uBAAuBzG,WAAW;oBACrDC;oBACAW,IAAIoI,KAAKpI,EAAE;oBACXC,MAAMmI,KAAKnI,IAAI;oBACfS;oBACAP;gBACF;gBACA,IAAIuI,QAAQ,OAAOxK,iBAAiBgJ,OAAO;oBAAEwB;gBAAO;gBACpD,OAAOxK,iBAAiBgJ,OAAO;oBAC7BG,SAAS;wBAACT,OAAO;4BAAE5G,IAAIoI,KAAKpI,EAAE;4BAAEgH,WAAW;4BAAM/G,MAAMmI,KAAKnI,IAAI;4BAAEgH;wBAAc;qBAAG;gBACrF;YACF;YACA,IAAImB,KAAKK,EAAE,KAAK,SAASL,KAAK7C,MAAM,KAAK,KAAK;gBAC5C,OAAOrH,iBAAiBgJ,OAAO;oBAAEwB,QAAQ9K,cAAcwK,KAAK5C,KAAK;gBAAE;YACrE;QACF;QAEA,MAAMmD,YAAY,MAAMzL,wBAAwBkC,WAAW;YAAEsG,MAAM;YAAYrG;YAAQsG;QAAI;QAC3F,IAAIgD,WAAW,OAAOzK,iBAAiBgJ,OAAO;YAAEwB,QAAQ9K,cAAc+K;QAAW;QAEjF,MAAMC,SAAS,MAAMpM,uBAAuB,YAAY;YACtDqM,MAAM;YACN3B;YACA4B,cAAc;mBAAI9J;gBAA8BsH;aAAW;YAC3DgB;YACAyB,UAAU;gBACR;oBACEC,MAAM;oBACNC,SAASzI,oBAAoBC,KAAKN,YAAYX,YAAYkB,MAAMT,MAAMkI;gBACxE;aACD;YACDe,MAAM3M,mBAAmB;YACzBoC,WAAWC,4BAA4BD,SAAS,CAACuI;WAC7C7K,gBAAgB,CAAC,eAAe,CAAC8M,QAAQ,GAAG;YAAEA,UAAU9M,gBAAgB,CAAC,eAAe,CAAC8M,QAAQ;QAAC,IAAI,CAAC,GACvG9M,gBAAgB,CAAC,eAAe,CAAC+M,MAAM,GAAG;YAAEA,QAAQ/M,gBAAgB,CAAC,eAAe,CAAC+M,MAAM;QAAC,IAAI,CAAC;YACrG/D,SAAS;gBAAEgE,eAAerN,wBAAwBmE;YAAY;YAC9D,wEAAwE;YACxE,oEAAoE;YACpE,oEAAoE;YACpE,kEAAkE;YAClE,mEAAmE;YACnE,kCAAkC;YAClCmJ,QAAQ,CAAClG,OAAS;uBACbH,uBAAuB9C,YAAYiD;uBACnChF,8BAA8BsC,MAAMP,YAAY;wBACjD2D,QAAQV,KAAKU,MAAM;wBACnBT,OAAOD,KAAKC,KAAK;oBACnB;uBACGpF,sBAAsBqJ,WAAW5G,MAAM5C,qBAAqBsF,OAAO;iBACvE;WACGoD,SAAS;YAAEA;QAAO,IAAI,CAAC;QAE7B,MAAM+C,QAAQ7L,kBAAkBkL;QAChC,IAAIA,OAAOrD,MAAM,KAAK,WAAW,OAAO,aAAKgE;YAAOC,SAAS;;QAC7D,IAAIZ,OAAOrD,MAAM,KAAK,eAAe,OAAO,aAAKgE;YAAOb,QAAQjL,iBAAiBmL;;QAEjF,qDAAqD;QACrDpN,qBAAqBgL;QACrB,MAAMK,QAAQ,MAAMvJ,aAAa8B,WAAW;YAC1CsG,MAAM;YACNrG;YACAW,IAAImH;YACJmB,KAAK7H,IAAI8H,SAAS;YAClB5C;YACA1F;YACA,wEAAwE;YACxE,kDAAkD;YAClDoD,OAAOrG,iBAAiB4L,OAAO/I,KAAK,CAACwD,KAAK,EAAEtG,qBAAqB0D,KAAK,KAAK;gBAAEqD,QAAQ8E,OAAO/I,KAAK,CAACiE,MAAM;YAAC;YACzG/D,MAAMO,oBAAoBH,YAAYX;YACtC+G;QACF;QACA,IAAIM,MAAM4B,EAAE,KAAK,OAAO;YACtB,IAAI5B,MAAMtB,MAAM,KAAK,KAAK,MAAM,IAAIoB,MAAM,CAAC,KAAK,EAAEtH,OAAO,0CAA0C,CAAC;YACpG,OAAO,aAAKkK;gBAAOb,QAAQ9K,cAAciJ,MAAMrB,KAAK;;QACtD;QACA,OAAO,aAAK+D;YAAOlC,SAAS;gBAACT,OAAOC,OAAO+B,OAAO/I,KAAK,CAACiH,IAAI;aAAE;;IAChE;AACF;AAEA,8GAA8G,GAC9G,OAAO,MAAM2C,uBAAuBpL,wBAAwB,YAAY,gBAAgB8H,2BAA0B;AAElH,qGAAqG,GACrG,OAAO,SAASuD;IACdjL,kBAAkB,YAAYgL,sBAAsB;QAClD1K,WAAWD;QACX6K,cAAcpL,4BAA4B;IAC5C;IACA1B,uBAAuB,YAAYqI;AACrC"}
@@ -0,0 +1,75 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import type { AnimationEase, AnimationPreset } from '@aglyn/aglyn/app-utils/element-animation';
18
+ import { type ElementMotionCharacter } from '@aglyn/aglyn/app-utils/element-motion';
19
+ /**
20
+ * How an AI-built site moves (AGL-3838): a small motion CHARACTER per kind of
21
+ * site, turned by the site's design seed, which the platform's entrance pass
22
+ * (`element-motion.ts`) writes onto every page the AI builds.
23
+ *
24
+ * Decided in code, never by the model: no prompt names a preset, no answer
25
+ * carries one, and no credit is spent on it. The same site — same kind, same
26
+ * seed — always moves the same way on every page, and two sites of one kind
27
+ * are drawn apart by their seeds, the way their looks are.
28
+ *
29
+ * The moods, by the kinds `ai-site-kinds.ts` lists:
30
+ * - CALM — practices, health, reading: a fade, on a gentle curve, slower.
31
+ * - BOLD — studios, fitness, music, events: a rise with overshoot, and
32
+ * pictures that zoom in.
33
+ * - GALLERY — portfolios and photography: pictures zoom in, one after
34
+ * another; the words fade or rise.
35
+ * - STANDARD — everything else: a smooth rise.
36
+ *
37
+ * Every number stays inside the vocabulary's caps, and inside 400–800 ms for
38
+ * a duration and 60–120 ms between staggered items: slower reads as a site
39
+ * that is loading, faster as one that is twitching.
40
+ */
41
+ export type AiSiteMotionMood = 'calm' | 'bold' | 'gallery' | 'standard';
42
+ interface AiSiteMotionRange {
43
+ /** The word presets the seed picks among. */
44
+ presets: readonly AnimationPreset[];
45
+ /** The picture presets the seed picks among. */
46
+ media: readonly AnimationPreset[];
47
+ eases: readonly AnimationEase[];
48
+ /** Inclusive millisecond ranges. */
49
+ duration: readonly [number, number];
50
+ stagger: readonly [number, number];
51
+ hero: readonly [number, number];
52
+ }
53
+ /** Each mood's range. The seed picks one character from it. */
54
+ export declare const AI_SITE_MOTION_MOODS: Readonly<Record<AiSiteMotionMood, AiSiteMotionRange>>;
55
+ /** Each site kind's mood; a kind not listed is standard. */
56
+ export declare const AI_SITE_MOTION_KINDS: Readonly<Record<string, AiSiteMotionMood>>;
57
+ /** The mood a kind of site moves in. */
58
+ export declare function aiSiteMotionMood(kind: string | null | undefined): AiSiteMotionMood;
59
+ /** The motion character of a site: its kind's mood, turned by its seed. */
60
+ export declare function aiSiteMotion(input: {
61
+ kind?: string | null;
62
+ seed: number;
63
+ }): ElementMotionCharacter;
64
+ /**
65
+ * A page the AI built, with its site's entrance motion written onto it —
66
+ * or the same node map, when there was nothing left to move.
67
+ */
68
+ export declare function aiWithSiteMotion<T extends Record<string, any>>(nodes: T, site: {
69
+ kind?: string | null;
70
+ seed: number;
71
+ }, page?: {
72
+ rootId?: string;
73
+ sectionIds?: readonly string[];
74
+ }): T;
75
+ export {};
@@ -0,0 +1,172 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ import { _ as _extends } from "@swc/helpers/_/_extends";
17
+ import { applyElementMotion } from "@aglyn/aglyn/app-utils/element-motion";
18
+ /** Each mood's range. The seed picks one character from it. */ export const AI_SITE_MOTION_MOODS = {
19
+ calm: {
20
+ presets: [
21
+ 'fade'
22
+ ],
23
+ media: [
24
+ 'fade'
25
+ ],
26
+ eases: [
27
+ 'gentle-end',
28
+ 'gentle-both'
29
+ ],
30
+ duration: [
31
+ 650,
32
+ 800
33
+ ],
34
+ stagger: [
35
+ 100,
36
+ 120
37
+ ],
38
+ hero: [
39
+ 130,
40
+ 160
41
+ ]
42
+ },
43
+ bold: {
44
+ presets: [
45
+ 'slide-up'
46
+ ],
47
+ media: [
48
+ 'zoom-in'
49
+ ],
50
+ eases: [
51
+ 'overshoot'
52
+ ],
53
+ duration: [
54
+ 450,
55
+ 600
56
+ ],
57
+ stagger: [
58
+ 60,
59
+ 80
60
+ ],
61
+ hero: [
62
+ 80,
63
+ 100
64
+ ]
65
+ },
66
+ gallery: {
67
+ presets: [
68
+ 'fade',
69
+ 'slide-up'
70
+ ],
71
+ media: [
72
+ 'zoom-in'
73
+ ],
74
+ eases: [
75
+ 'smooth'
76
+ ],
77
+ duration: [
78
+ 600,
79
+ 750
80
+ ],
81
+ stagger: [
82
+ 80,
83
+ 110
84
+ ],
85
+ hero: [
86
+ 110,
87
+ 130
88
+ ]
89
+ },
90
+ standard: {
91
+ presets: [
92
+ 'slide-up'
93
+ ],
94
+ media: [
95
+ 'slide-up',
96
+ 'fade'
97
+ ],
98
+ eases: [
99
+ 'smooth'
100
+ ],
101
+ duration: [
102
+ 500,
103
+ 700
104
+ ],
105
+ stagger: [
106
+ 70,
107
+ 100
108
+ ],
109
+ hero: [
110
+ 100,
111
+ 130
112
+ ]
113
+ }
114
+ };
115
+ /** Each site kind's mood; a kind not listed is standard. */ export const AI_SITE_MOTION_KINDS = {
116
+ professional: 'calm',
117
+ wellness: 'calm',
118
+ yoga: 'calm',
119
+ beauty: 'calm',
120
+ blog: 'calm',
121
+ studio: 'bold',
122
+ fitness: 'bold',
123
+ music: 'bold',
124
+ events: 'bold',
125
+ portfolio: 'gallery',
126
+ photography: 'gallery'
127
+ };
128
+ /** The mood a kind of site moves in. */ export function aiSiteMotionMood(kind) {
129
+ return kind && AI_SITE_MOTION_KINDS[kind] || 'standard';
130
+ }
131
+ /**
132
+ * A small deterministic generator over a seed (mulberry32), mixed with its
133
+ * own constant so a site's motion does not move in step with its layout.
134
+ */ function random(seed) {
135
+ let state = (seed ^ 0x6a09e667) >>> 0;
136
+ return ()=>{
137
+ state = state + 0x6d2b79f5 >>> 0;
138
+ let t = state;
139
+ t = Math.imul(t ^ t >>> 15, t | 1);
140
+ t ^= t + Math.imul(t ^ t >>> 7, t | 61);
141
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
142
+ };
143
+ }
144
+ const pick = (next, list)=>list[Math.floor(next() * list.length) % list.length];
145
+ /** A value in an inclusive range, on a step: 10 ms for a duration, 5 ms for a gap. */ const within = (next, [low, high], step)=>low + Math.floor(next() * (Math.floor((high - low) / step) + 1)) * step;
146
+ /** The motion character of a site: its kind's mood, turned by its seed. */ export function aiSiteMotion(input) {
147
+ const range = AI_SITE_MOTION_MOODS[aiSiteMotionMood(input.kind)];
148
+ const next = random(input.seed >>> 0);
149
+ return {
150
+ preset: pick(next, range.presets),
151
+ mediaPreset: pick(next, range.media),
152
+ ease: pick(next, range.eases),
153
+ durationMs: within(next, range.duration, 10),
154
+ staggerStepMs: within(next, range.stagger, 5),
155
+ heroStepMs: within(next, range.hero, 5)
156
+ };
157
+ }
158
+ /**
159
+ * A page the AI built, with its site's entrance motion written onto it —
160
+ * or the same node map, when there was nothing left to move.
161
+ */ export function aiWithSiteMotion(nodes, site, page = {}) {
162
+ var _page_sectionIds;
163
+ return applyElementMotion(nodes, _extends({
164
+ character: aiSiteMotion(site)
165
+ }, page.rootId ? {
166
+ rootId: page.rootId
167
+ } : {}, ((_page_sectionIds = page.sectionIds) == null ? void 0 : _page_sectionIds.length) ? {
168
+ sectionIds: page.sectionIds
169
+ } : {}));
170
+ }
171
+
172
+ //# sourceMappingURL=ai-layout-motion.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/layout-language/ai-layout-motion.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { AnimationEase, AnimationPreset } from '@aglyn/aglyn/app-utils/element-animation'\nimport {\n applyElementMotion,\n type ElementMotionCharacter,\n} from '@aglyn/aglyn/app-utils/element-motion'\n\n/**\n * How an AI-built site moves (AGL-3838): a small motion CHARACTER per kind of\n * site, turned by the site's design seed, which the platform's entrance pass\n * (`element-motion.ts`) writes onto every page the AI builds.\n *\n * Decided in code, never by the model: no prompt names a preset, no answer\n * carries one, and no credit is spent on it. The same site — same kind, same\n * seed — always moves the same way on every page, and two sites of one kind\n * are drawn apart by their seeds, the way their looks are.\n *\n * The moods, by the kinds `ai-site-kinds.ts` lists:\n * - CALM — practices, health, reading: a fade, on a gentle curve, slower.\n * - BOLD — studios, fitness, music, events: a rise with overshoot, and\n * pictures that zoom in.\n * - GALLERY — portfolios and photography: pictures zoom in, one after\n * another; the words fade or rise.\n * - STANDARD — everything else: a smooth rise.\n *\n * Every number stays inside the vocabulary's caps, and inside 400–800 ms for\n * a duration and 60–120 ms between staggered items: slower reads as a site\n * that is loading, faster as one that is twitching.\n */\n\nexport type AiSiteMotionMood = 'calm' | 'bold' | 'gallery' | 'standard'\n\ninterface AiSiteMotionRange {\n /** The word presets the seed picks among. */\n presets: readonly AnimationPreset[]\n /** The picture presets the seed picks among. */\n media: readonly AnimationPreset[]\n eases: readonly AnimationEase[]\n /** Inclusive millisecond ranges. */\n duration: readonly [number, number]\n stagger: readonly [number, number]\n hero: readonly [number, number]\n}\n\n/** Each mood's range. The seed picks one character from it. */\nexport const AI_SITE_MOTION_MOODS: Readonly<Record<AiSiteMotionMood, AiSiteMotionRange>> = {\n calm: {\n presets: ['fade'],\n media: ['fade'],\n eases: ['gentle-end', 'gentle-both'],\n duration: [650, 800],\n stagger: [100, 120],\n hero: [130, 160],\n },\n bold: {\n presets: ['slide-up'],\n media: ['zoom-in'],\n eases: ['overshoot'],\n duration: [450, 600],\n stagger: [60, 80],\n hero: [80, 100],\n },\n gallery: {\n presets: ['fade', 'slide-up'],\n media: ['zoom-in'],\n eases: ['smooth'],\n duration: [600, 750],\n stagger: [80, 110],\n hero: [110, 130],\n },\n standard: {\n presets: ['slide-up'],\n media: ['slide-up', 'fade'],\n eases: ['smooth'],\n duration: [500, 700],\n stagger: [70, 100],\n hero: [100, 130],\n },\n}\n\n/** Each site kind's mood; a kind not listed is standard. */\nexport const AI_SITE_MOTION_KINDS: Readonly<Record<string, AiSiteMotionMood>> = {\n professional: 'calm',\n wellness: 'calm',\n yoga: 'calm',\n beauty: 'calm',\n blog: 'calm',\n studio: 'bold',\n fitness: 'bold',\n music: 'bold',\n events: 'bold',\n portfolio: 'gallery',\n photography: 'gallery',\n}\n\n/** The mood a kind of site moves in. */\nexport function aiSiteMotionMood(kind: string | null | undefined): AiSiteMotionMood {\n return (kind && AI_SITE_MOTION_KINDS[kind]) || 'standard'\n}\n\n/**\n * A small deterministic generator over a seed (mulberry32), mixed with its\n * own constant so a site's motion does not move in step with its layout.\n */\nfunction random(seed: number): () => number {\n let state = (seed ^ 0x6a09e667) >>> 0\n return () => {\n state = (state + 0x6d2b79f5) >>> 0\n let t = state\n t = Math.imul(t ^ (t >>> 15), t | 1)\n t ^= t + Math.imul(t ^ (t >>> 7), t | 61)\n return ((t ^ (t >>> 14)) >>> 0) / 4294967296\n }\n}\n\nconst pick = <T>(next: () => number, list: readonly T[]): T => list[Math.floor(next() * list.length) % list.length] as T\n/** A value in an inclusive range, on a step: 10 ms for a duration, 5 ms for a gap. */\nconst within = (next: () => number, [low, high]: readonly [number, number], step: number): number =>\n low + Math.floor(next() * (Math.floor((high - low) / step) + 1)) * step\n\n/** The motion character of a site: its kind's mood, turned by its seed. */\nexport function aiSiteMotion(input: { kind?: string | null; seed: number }): ElementMotionCharacter {\n const range = AI_SITE_MOTION_MOODS[aiSiteMotionMood(input.kind)]\n const next = random(input.seed >>> 0)\n return {\n preset: pick(next, range.presets),\n mediaPreset: pick(next, range.media),\n ease: pick(next, range.eases),\n durationMs: within(next, range.duration, 10),\n staggerStepMs: within(next, range.stagger, 5),\n heroStepMs: within(next, range.hero, 5),\n }\n}\n\n/**\n * A page the AI built, with its site's entrance motion written onto it —\n * or the same node map, when there was nothing left to move.\n */\nexport function aiWithSiteMotion<T extends Record<string, any>>(\n nodes: T,\n site: { kind?: string | null; seed: number },\n page: { rootId?: string; sectionIds?: readonly string[] } = {},\n): T {\n return applyElementMotion(nodes, {\n character: aiSiteMotion(site),\n ...(page.rootId ? { rootId: page.rootId } : {}),\n ...(page.sectionIds?.length ? { sectionIds: page.sectionIds } : {}),\n })\n}\n"],"names":["applyElementMotion","AI_SITE_MOTION_MOODS","calm","presets","media","eases","duration","stagger","hero","bold","gallery","standard","AI_SITE_MOTION_KINDS","professional","wellness","yoga","beauty","blog","studio","fitness","music","events","portfolio","photography","aiSiteMotionMood","kind","random","seed","state","t","Math","imul","pick","next","list","floor","length","within","low","high","step","aiSiteMotion","input","range","preset","mediaPreset","ease","durationMs","staggerStepMs","heroStepMs","aiWithSiteMotion","nodes","site","page","character","rootId","sectionIds"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC;AAGD,SACEA,kBAAkB,QAEb,wCAAuC;AAuC9C,6DAA6D,GAC7D,OAAO,MAAMC,uBAA8E;IACzFC,MAAM;QACJC,SAAS;YAAC;SAAO;QACjBC,OAAO;YAAC;SAAO;QACfC,OAAO;YAAC;YAAc;SAAc;QACpCC,UAAU;YAAC;YAAK;SAAI;QACpBC,SAAS;YAAC;YAAK;SAAI;QACnBC,MAAM;YAAC;YAAK;SAAI;IAClB;IACAC,MAAM;QACJN,SAAS;YAAC;SAAW;QACrBC,OAAO;YAAC;SAAU;QAClBC,OAAO;YAAC;SAAY;QACpBC,UAAU;YAAC;YAAK;SAAI;QACpBC,SAAS;YAAC;YAAI;SAAG;QACjBC,MAAM;YAAC;YAAI;SAAI;IACjB;IACAE,SAAS;QACPP,SAAS;YAAC;YAAQ;SAAW;QAC7BC,OAAO;YAAC;SAAU;QAClBC,OAAO;YAAC;SAAS;QACjBC,UAAU;YAAC;YAAK;SAAI;QACpBC,SAAS;YAAC;YAAI;SAAI;QAClBC,MAAM;YAAC;YAAK;SAAI;IAClB;IACAG,UAAU;QACRR,SAAS;YAAC;SAAW;QACrBC,OAAO;YAAC;YAAY;SAAO;QAC3BC,OAAO;YAAC;SAAS;QACjBC,UAAU;YAAC;YAAK;SAAI;QACpBC,SAAS;YAAC;YAAI;SAAI;QAClBC,MAAM;YAAC;YAAK;SAAI;IAClB;AACF,EAAC;AAED,0DAA0D,GAC1D,OAAO,MAAMI,uBAAmE;IAC9EC,cAAc;IACdC,UAAU;IACVC,MAAM;IACNC,QAAQ;IACRC,MAAM;IACNC,QAAQ;IACRC,SAAS;IACTC,OAAO;IACPC,QAAQ;IACRC,WAAW;IACXC,aAAa;AACf,EAAC;AAED,sCAAsC,GACtC,OAAO,SAASC,iBAAiBC,IAA+B;IAC9D,OAAO,AAACA,QAAQb,oBAAoB,CAACa,KAAK,IAAK;AACjD;AAEA;;;CAGC,GACD,SAASC,OAAOC,IAAY;IAC1B,IAAIC,QAAQ,AAACD,CAAAA,OAAO,UAAS,MAAO;IACpC,OAAO;QACLC,QAAQ,AAACA,QAAQ,eAAgB;QACjC,IAAIC,IAAID;QACRC,IAAIC,KAAKC,IAAI,CAACF,IAAKA,MAAM,IAAKA,IAAI;QAClCA,KAAKA,IAAIC,KAAKC,IAAI,CAACF,IAAKA,MAAM,GAAIA,IAAI;QACtC,OAAO,AAAC,CAAA,AAACA,CAAAA,IAAKA,MAAM,EAAE,MAAO,CAAA,IAAK;IACpC;AACF;AAEA,MAAMG,OAAO,CAAIC,MAAoBC,OAA0BA,IAAI,CAACJ,KAAKK,KAAK,CAACF,SAASC,KAAKE,MAAM,IAAIF,KAAKE,MAAM,CAAC;AACnH,oFAAoF,GACpF,MAAMC,SAAS,CAACJ,MAAoB,CAACK,KAAKC,KAAgC,EAAEC,OAC1EF,MAAMR,KAAKK,KAAK,CAACF,SAAUH,CAAAA,KAAKK,KAAK,CAAC,AAACI,CAAAA,OAAOD,GAAE,IAAKE,QAAQ,CAAA,KAAMA;AAErE,yEAAyE,GACzE,OAAO,SAASC,aAAaC,KAA6C;IACxE,MAAMC,QAAQ1C,oBAAoB,CAACuB,iBAAiBkB,MAAMjB,IAAI,EAAE;IAChE,MAAMQ,OAAOP,OAAOgB,MAAMf,IAAI,KAAK;IACnC,OAAO;QACLiB,QAAQZ,KAAKC,MAAMU,MAAMxC,OAAO;QAChC0C,aAAab,KAAKC,MAAMU,MAAMvC,KAAK;QACnC0C,MAAMd,KAAKC,MAAMU,MAAMtC,KAAK;QAC5B0C,YAAYV,OAAOJ,MAAMU,MAAMrC,QAAQ,EAAE;QACzC0C,eAAeX,OAAOJ,MAAMU,MAAMpC,OAAO,EAAE;QAC3C0C,YAAYZ,OAAOJ,MAAMU,MAAMnC,IAAI,EAAE;IACvC;AACF;AAEA;;;CAGC,GACD,OAAO,SAAS0C,iBACdC,KAAQ,EACRC,IAA4C,EAC5CC,OAA4D,CAAC,CAAC;QAKxDA;IAHN,OAAOrD,mBAAmBmD,OAAO;QAC/BG,WAAWb,aAAaW;OACpBC,KAAKE,MAAM,GAAG;QAAEA,QAAQF,KAAKE,MAAM;IAAC,IAAI,CAAC,GACzCF,EAAAA,mBAAAA,KAAKG,UAAU,qBAAfH,iBAAiBjB,MAAM,IAAG;QAAEoB,YAAYH,KAAKG,UAAU;IAAC,IAAI,CAAC;AAErE"}
@@ -68,7 +68,7 @@
68
68
  description: 'An AI build (a site, or a multi-part build) finished, but some of its parts failed on the platform’s side, so the customer got less than they asked for. Read which parts and why, then search the logs for the job id.',
69
69
  tier: 'should',
70
70
  category: 'ops',
71
- title: 'An AI {{kind}} build finished with {{failed}} of {{total}} parts failed',
71
+ title: 'An AI {{kind}} job finished with {{failed}} of {{total}} parts failed',
72
72
  body: 'AI job {{jobId}} ({{kind}}) in workspace {{orgId}} finished, but {{failed}} of its {{total}} parts failed on our side: {{items}}. The customer was given back those parts’ credits. Further partial {{kind}} builds within the hour are counted into the next alert.',
73
73
  link: '/admin/orgs/{{orgId}}',
74
74
  delivery: 'immediate',
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/plugins/ai/src/lib/operator-alerts.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { OperatorAlertDefinition } from '@aglyn/aglyn/app-utils/operator-alerts'\nimport { AI_PLUGIN_ID } from './constants'\n\n/**\n * The AI plugin's operator alert (AGL-3377): the platform's provider account\n * refused its key or ran out of credit, so every AI feature on the install\n * fails. Provider-generic — each adapter classifies its own vendor's answer\n * as an `accountProblem`, and the runtime raises this with the provider's\n * label — so a self-hoster on any provider hears the same thing.\n */\nexport const AI_PROVIDER_UNAVAILABLE: OperatorAlertDefinition = {\n type: 'ai.providerUnavailable',\n pluginId: AI_PLUGIN_ID,\n label: 'Platform AI provider unavailable',\n description:\n 'The platform’s AI provider refused its key or ran out of credit, so every AI feature on the install is failing until the key or the balance is fixed.',\n tier: 'should',\n category: 'ops',\n title: '{{provider}} is refusing requests',\n body:\n 'The platform’s {{provider}} account refused a request because {{reason}}. AI features fail until it is fixed ({{keyEnv}}).',\n delivery: 'immediate',\n dedupeWindowMinutes: 6 * 60,\n defaultEnabled: true,\n}\n\n/**\n * An AI job failed on our side: a provider error, a step that produced\n * nothing usable, a build that delivered nothing. Not a model declining the\n * brief, and not a site with AI switched off — those are the customer's, and\n * the customer is told. Deduped per job kind, so an outage is one alert an\n * hour for each kind it breaks rather than one per job; the runner's own\n * words are in `{{error}}`, and every failure is in the log as `ai job failed`.\n */\nexport const AI_JOB_FAILED: OperatorAlertDefinition = {\n type: 'ai.jobFailed',\n pluginId: AI_PLUGIN_ID,\n label: 'An AI job failed',\n description:\n 'An AI job (a site, page, build, theme…) failed on the platform’s side, so a customer got nothing for their request. Read the error, then search the logs for “ai job failed” to see whether it is one job or every job of that kind.',\n tier: 'should',\n category: 'ops',\n title: 'An AI {{kind}} job failed',\n body:\n 'AI job {{jobId}} ({{kind}}) in workspace {{orgId}} failed at step {{step}}: {{error}}. The customer was told and given back the credits. Further {{kind}} failures within the hour are counted into the next alert.',\n link: '/admin/orgs/{{orgId}}',\n delivery: 'immediate',\n dedupeWindowMinutes: 60,\n defaultEnabled: true,\n}\n\n/**\n * A build finished with part of it missing: it delivered something, so the\n * job is `done`, but one or more of its items failed on our side (AGL-3683).\n * The customer has a site or build with holes in it and no failed job to\n * show for it, which is why it is its own alert. Items the model declined\n * are the customer's and do not count. Deduped per job kind, like\n * `ai.jobFailed`.\n */\nexport const AI_BUILD_PARTLY_FAILED: OperatorAlertDefinition = {\n type: 'ai.buildPartlyFailed',\n pluginId: AI_PLUGIN_ID,\n label: 'An AI build finished with parts missing',\n description:\n 'An AI build (a site, or a multi-part build) finished, but some of its parts failed on the platform’s side, so the customer got less than they asked for. Read which parts and why, then search the logs for the job id.',\n tier: 'should',\n category: 'ops',\n title: 'An AI {{kind}} build finished with {{failed}} of {{total}} parts failed',\n body:\n 'AI job {{jobId}} ({{kind}}) in workspace {{orgId}} finished, but {{failed}} of its {{total}} parts failed on our side: {{items}}. The customer was given back those parts’ credits. Further partial {{kind}} builds within the hour are counted into the next alert.',\n link: '/admin/orgs/{{orgId}}',\n delivery: 'immediate',\n dedupeWindowMinutes: 60,\n defaultEnabled: true,\n}\n"],"names":["AI_PLUGIN_ID","AI_PROVIDER_UNAVAILABLE","type","pluginId","label","description","tier","category","title","body","delivery","dedupeWindowMinutes","defaultEnabled","AI_JOB_FAILED","link","AI_BUILD_PARTLY_FAILED"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAGD,SAASA,YAAY,QAAQ,iBAAa;AAE1C;;;;;;CAMC,GACD,OAAO,MAAMC,0BAAmD;IAC9DC,MAAM;IACNC,UAAUH;IACVI,OAAO;IACPC,aACE;IACFC,MAAM;IACNC,UAAU;IACVC,OAAO;IACPC,MACE;IACFC,UAAU;IACVC,qBAAqB,IAAI;IACzBC,gBAAgB;AAClB,EAAC;AAED;;;;;;;CAOC,GACD,OAAO,MAAMC,gBAAyC;IACpDX,MAAM;IACNC,UAAUH;IACVI,OAAO;IACPC,aACE;IACFC,MAAM;IACNC,UAAU;IACVC,OAAO;IACPC,MACE;IACFK,MAAM;IACNJ,UAAU;IACVC,qBAAqB;IACrBC,gBAAgB;AAClB,EAAC;AAED;;;;;;;CAOC,GACD,OAAO,MAAMG,yBAAkD;IAC7Db,MAAM;IACNC,UAAUH;IACVI,OAAO;IACPC,aACE;IACFC,MAAM;IACNC,UAAU;IACVC,OAAO;IACPC,MACE;IACFK,MAAM;IACNJ,UAAU;IACVC,qBAAqB;IACrBC,gBAAgB;AAClB,EAAC"}
1
+ {"version":3,"sources":["../../../../../../libs/plugins/ai/src/lib/operator-alerts.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { OperatorAlertDefinition } from '@aglyn/aglyn/app-utils/operator-alerts'\nimport { AI_PLUGIN_ID } from './constants'\n\n/**\n * The AI plugin's operator alert (AGL-3377): the platform's provider account\n * refused its key or ran out of credit, so every AI feature on the install\n * fails. Provider-generic — each adapter classifies its own vendor's answer\n * as an `accountProblem`, and the runtime raises this with the provider's\n * label — so a self-hoster on any provider hears the same thing.\n */\nexport const AI_PROVIDER_UNAVAILABLE: OperatorAlertDefinition = {\n type: 'ai.providerUnavailable',\n pluginId: AI_PLUGIN_ID,\n label: 'Platform AI provider unavailable',\n description:\n 'The platform’s AI provider refused its key or ran out of credit, so every AI feature on the install is failing until the key or the balance is fixed.',\n tier: 'should',\n category: 'ops',\n title: '{{provider}} is refusing requests',\n body:\n 'The platform’s {{provider}} account refused a request because {{reason}}. AI features fail until it is fixed ({{keyEnv}}).',\n delivery: 'immediate',\n dedupeWindowMinutes: 6 * 60,\n defaultEnabled: true,\n}\n\n/**\n * An AI job failed on our side: a provider error, a step that produced\n * nothing usable, a build that delivered nothing. Not a model declining the\n * brief, and not a site with AI switched off — those are the customer's, and\n * the customer is told. Deduped per job kind, so an outage is one alert an\n * hour for each kind it breaks rather than one per job; the runner's own\n * words are in `{{error}}`, and every failure is in the log as `ai job failed`.\n */\nexport const AI_JOB_FAILED: OperatorAlertDefinition = {\n type: 'ai.jobFailed',\n pluginId: AI_PLUGIN_ID,\n label: 'An AI job failed',\n description:\n 'An AI job (a site, page, build, theme…) failed on the platform’s side, so a customer got nothing for their request. Read the error, then search the logs for “ai job failed” to see whether it is one job or every job of that kind.',\n tier: 'should',\n category: 'ops',\n title: 'An AI {{kind}} job failed',\n body:\n 'AI job {{jobId}} ({{kind}}) in workspace {{orgId}} failed at step {{step}}: {{error}}. The customer was told and given back the credits. Further {{kind}} failures within the hour are counted into the next alert.',\n link: '/admin/orgs/{{orgId}}',\n delivery: 'immediate',\n dedupeWindowMinutes: 60,\n defaultEnabled: true,\n}\n\n/**\n * A build finished with part of it missing: it delivered something, so the\n * job is `done`, but one or more of its items failed on our side (AGL-3683).\n * The customer has a site or build with holes in it and no failed job to\n * show for it, which is why it is its own alert. Items the model declined\n * are the customer's and do not count. Deduped per job kind, like\n * `ai.jobFailed`.\n */\nexport const AI_BUILD_PARTLY_FAILED: OperatorAlertDefinition = {\n type: 'ai.buildPartlyFailed',\n pluginId: AI_PLUGIN_ID,\n label: 'An AI build finished with parts missing',\n description:\n 'An AI build (a site, or a multi-part build) finished, but some of its parts failed on the platform’s side, so the customer got less than they asked for. Read which parts and why, then search the logs for the job id.',\n tier: 'should',\n category: 'ops',\n title: 'An AI {{kind}} job finished with {{failed}} of {{total}} parts failed',\n body:\n 'AI job {{jobId}} ({{kind}}) in workspace {{orgId}} finished, but {{failed}} of its {{total}} parts failed on our side: {{items}}. The customer was given back those parts’ credits. Further partial {{kind}} builds within the hour are counted into the next alert.',\n link: '/admin/orgs/{{orgId}}',\n delivery: 'immediate',\n dedupeWindowMinutes: 60,\n defaultEnabled: true,\n}\n"],"names":["AI_PLUGIN_ID","AI_PROVIDER_UNAVAILABLE","type","pluginId","label","description","tier","category","title","body","delivery","dedupeWindowMinutes","defaultEnabled","AI_JOB_FAILED","link","AI_BUILD_PARTLY_FAILED"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAGD,SAASA,YAAY,QAAQ,iBAAa;AAE1C;;;;;;CAMC,GACD,OAAO,MAAMC,0BAAmD;IAC9DC,MAAM;IACNC,UAAUH;IACVI,OAAO;IACPC,aACE;IACFC,MAAM;IACNC,UAAU;IACVC,OAAO;IACPC,MACE;IACFC,UAAU;IACVC,qBAAqB,IAAI;IACzBC,gBAAgB;AAClB,EAAC;AAED;;;;;;;CAOC,GACD,OAAO,MAAMC,gBAAyC;IACpDX,MAAM;IACNC,UAAUH;IACVI,OAAO;IACPC,aACE;IACFC,MAAM;IACNC,UAAU;IACVC,OAAO;IACPC,MACE;IACFK,MAAM;IACNJ,UAAU;IACVC,qBAAqB;IACrBC,gBAAgB;AAClB,EAAC;AAED;;;;;;;CAOC,GACD,OAAO,MAAMG,yBAAkD;IAC7Db,MAAM;IACNC,UAAUH;IACVI,OAAO;IACPC,aACE;IACFC,MAAM;IACNC,UAAU;IACVC,OAAO;IACPC,MACE;IACFK,MAAM;IACNJ,UAAU;IACVC,qBAAqB;IACrBC,gBAAgB;AAClB,EAAC"}
@@ -600,6 +600,18 @@ export declare function aiTreeCopy(tree: AiDoctrineTree, context?: AiNodeTreeCon
600
600
  * line, an unknown language's included, to English's.
601
601
  */
602
602
  export declare function aiDanglingWord(line: string, language?: string | null): string | null;
603
+ /**
604
+ * A search listing value held to `max` characters, cut at a word. A cut that
605
+ * leaves a joining word or mark at the end ("…Practice Areas and", "…across
606
+ * Springfield,") takes it off too, so a search result never shows a phrase
607
+ * that stops mid-thought (AGL-3813), in the plan's language (AGL-3828): a
608
+ * Spanish title cut to "Pan dulce y" reads "Pan dulce". A value with no space
609
+ * past half of `max` is cut at `max` itself, mid-word.
610
+ *
611
+ * The page step clips what it stores with it; `aiSettlePlanSeo` clips a plan
612
+ * with it only where the cut lands on a whole word (AGL-3837).
613
+ */
614
+ export declare function aiClipAtWord(value: string, max: number, language?: string | null): string;
603
615
  /**
604
616
  * Rule 14, for a line cut short (AGL-3072). Copy in the site's voice is
605
617
  * finished copy: a heading, a subhead or a body text that ends on an article
@@ -1088,6 +1100,23 @@ export declare const AI_SEO_TITLE_MAX = 70;
1088
1100
  export declare const AI_SEO_DESCRIPTION_MAX = 170;
1089
1101
  /** Rule 10 (plan): every screen has a free, well-formed slug, an SEO title and description. */
1090
1102
  export declare function detectMissingNavAndSeo(plan: AiBuildPlan, inventory: AiSiteInventory | null): AiDoctrineViolation[];
1103
+ /**
1104
+ * A page's search title or description a few words past its length is cut
1105
+ * at a word in code, before the rules read the plan (AGL-3837), rather than
1106
+ * re-asked: rule 10's `plan-seo` has one answer for it, "shorter", and a
1107
+ * model asked again for a shorter line misses by as little as it did the
1108
+ * first time. The live Free travel-blog start of 2026-10-15 wrote a home
1109
+ * description of 173 characters against 170 on both answers and stopped.
1110
+ *
1111
+ * The cut is the one the page step stores a listing with (`aiClipAtWord`):
1112
+ * at the last whole word inside `AI_SEO_TITLE_MAX` or
1113
+ * `AI_SEO_DESCRIPTION_MAX`, without a joining mark (" |", ",", ":", "–") or a
1114
+ * word that leaves the line unfinished (`aiDanglingWord`, in the plan's
1115
+ * language) at its end. A missing or empty value, and one whose cut would
1116
+ * split a word or keep under half the length, are left for the rule to
1117
+ * re-ask.
1118
+ */
1119
+ export declare function aiSettlePlanSeo(plan: AiBuildPlan): AiBuildPlan;
1091
1120
  /** Every piece of copy a plan carries, with its path. */
1092
1121
  export declare function aiPlanCopy(plan: AiBuildPlan): AiCopySample[];
1093
1122
  /** Rule 15 (plan): a screen or template like one the site has starts as its duplicate. */