@aglyn/plugins-ai 1.0.0-beta.146 → 1.0.0-beta.149

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 { 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 type { AiJob, AiJobOutput, AiJobPlan } from '../model/ai-jobs.types'\nimport {\n AI_TEMPLATE_SUBJECT_DEFINITIONS,\n AI_TEMPLATE_SUBJECTS,\n aiBindingTokensIn,\n aiTemplateAddressTokens,\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 writeAiDraft,\n type AiDraftRecord,\n} from './ai-job-drafts'\nimport {\n aiConfirmedPlan,\n aiDoctrineReview,\n aiGenerationSpent,\n aiJobBriefLine,\n aiLimitReview,\n aiModelNodeIds,\n aiPlanCreation,\n aiPlanReferenceLines,\n aiUnspentOutcome,\n} from './ai-job-generation'\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, and no\n * block that fills itself only on another subject's page.\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.',\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. */\nexport function aiJobTemplatePrompt(\n job: Pick<AiJob, 'brief'>,\n definition: AiTemplateSubjectDefinition,\n collection: AiTemplateCollection | null,\n plan: AiJobPlan | null,\n name: 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 ].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 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, and 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.\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 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 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 // 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 : null,\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\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.\n if (\n creation?.duplicateOf &&\n inventory.templates.some((template) => template.id === creation.duplicateOf)\n ) {\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 })\n if (copy.ok) {\n const hostSubdomain = await aiSiteSubdomain(firestore, hostId)\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),\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 extend: aiTemplateBindingCheck(definition),\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 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\nexport const runAiJobTemplateStep = 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, { minimumMs: AI_JOB_TEMPLATE_STEP_MINIMUM_MS })\n registerAiJobAdmission('template', createAiTemplateJobAdmission())\n}\n"],"names":["hostCollectionKind","normalizeScreenSlug","duplicateResource","AI_TEMPLATE_SUBJECT_DEFINITIONS","AI_TEMPLATE_SUBJECTS","aiBindingTokensIn","aiTemplateAddressTokens","parseAiTemplateJobInputs","AI_STEP_TIERS","AI_ROUTING_TABLE","aiModelForStep","aiDoctrineTreeTool","runValidatedGeneration","walkTree","AI_PALETTE","aiTemplateExamplesSystemBlock","readSiteInventory","registerAiJobAdmission","aiJobDraftId","aiDraftAdmissionRefusal","aiDraftAllowanceRefusal","aiSiteSubdomain","readAiDraft","writeAiDraft","aiConfirmedPlan","aiDoctrineReview","aiGenerationSpent","aiJobBriefLine","aiLimitReview","aiModelNodeIds","aiPlanCreation","aiPlanReferenceLines","aiUnspentOutcome","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","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","aiTemplateBindingCheck","allowed","Set","tree","nodes","unknown","unknownAt","foreign","titles","titleBound","ancestors","rootId","strays","filter","has","add","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","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","template","copy","sourceId","uid","createdBy","ok","review","allowance","result","step","instructions","messages","role","content","tool","thinking","effort","bindingTokens","extend","spent","refused","runAiJobTemplateStep","registerAiTemplateJob"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,kBAAkB,QAAQ,yCAAwC;AAC3E,SAASC,mBAAmB,QAAQ,sCAAqC;AAEzE,SAASC,iBAAiB,QAAQ,qDAAoD;AAEtF,SACEC,+BAA+B,EAC/BC,oBAAoB,EACpBC,iBAAiB,EACjBC,uBAAuB,EACvBC,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,SACEC,uBAAuB,EACvBC,uBAAuB,EACvBC,eAAe,EACfC,WAAW,EACXC,YAAY,QAEP,qBAAiB;AACxB,SACEC,eAAe,EACfC,gBAAgB,EAChBC,iBAAiB,EACjBC,cAAc,EACdC,aAAa,EACbC,cAAc,EACdC,cAAc,EACdC,oBAAoB,EACpBC,gBAAgB,QACX,yBAAqB;AAE5B,SAASC,eAAe,QAAQ,qBAAiB;AACjD,SAASC,iBAAiB,QAAQ,eAAW;AAE7C;;;;;;;;;;;;;;;;;;;;;;CAsBC,GAED,kGAAkG,GAClG,OAAO,MAAMC,6BAA6B1B,gBAAgB,CAAC,eAAe,CAAC2B,SAAS,CAAA;AAEpF;;;;;;CAMC,GACD,OAAO,MAAMC,8BAA8BJ,gBAAgB;IACzDK,MAAM9B,aAAa,CAAC,eAAe;IACnC4B,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;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,IAAIhD,mBAAmBqD,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;QAGrChD;IADF,QACEA,uBAAAA,oBACEgD,aACI,GAAGA,WAAWO,IAAI,IAAIP,WAAWQ,EAAE,CAAC,eAAe,CAAC,GACpD,GAAGG,WAAWI,OAAO,CAAC,cAAc,CAAC,aAH3C/D,uBAIK;AAET;AAEA,oDAAoD,GACpD,OAAO,SAASgE,oBACdC,GAAyB,EACzBN,UAAuC,EACvCX,UAAuC,EACvCkB,IAAsB,EACtBT,IAAY;IAEZ,MAAMU,KAAKnB,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,WAAWS,IAAI,GAAGD,GAAG,CAAC,CAAC;QAC1C,CAAC,aAAa,EAAER,WAAWU,UAAU,EAAE;QACvC,CAAC,wBAAwB,EAAEV,WAAWW,MAAM,CAACC,GAAG,CAAC,CAACC,QAAU,GAAGA,MAAMC,KAAK,CAAC,CAAC,EAAED,MAAME,KAAK,EAAE,EAAEhC,IAAI,CAAC,OAAO;QACzG,CAAC,uBAAuB,EAAErC,wBAAwBsD,YAAYjB,IAAI,CAAC,OAAO;QAC1EiB,WAAWgB,MAAM,CAACC,MAAM,GACpB,CAAC,0CAA0C,EAAEjB,WAAWgB,MAAM,CAACjC,IAAI,CAAC,OAAO,GAC3E;QACJhB,eAAeuC;WACZnC,qBAAqBoC;KACzB,CAACxB,IAAI,CAAC;AACT;AAEA,6DAA6D,GAC7D,MAAMmC,iBAAiB,IAAIC,IACzB3E,qBAAqB4E,OAAO,CAAC,CAAChB,UAC5B7D,+BAA+B,CAAC6D,QAAQ,CAACY,MAAM,CAACJ,GAAG,CACjD,CAACS,QAAU;YAACA;YAAO9E,+BAA+B,CAAC6D,QAAQ;SAAC;AAKlE,qDAAqD,GACrD,SAASkB,UAAU5B,KAAc,EAAE6B,MAAgB,EAAE;IACnD,IAAI,OAAO7B,UAAU,UAAU6B,IAAIC,IAAI,CAAC9B;SACnC,IAAI+B,MAAMC,OAAO,CAAChC,QAAQ,KAAK,MAAMiC,SAASjC,MAAO4B,UAAUK,OAAOJ;SACtE,IAAI7B,SAAS,OAAOA,UAAU,UAAU;QAC3C,KAAK,MAAMiC,SAASC,OAAOC,MAAM,CAACnC,OAAmC4B,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;;;;;CAKC,GACD,OAAO,SAASK,uBACdpC,UAAuC;IAEvC,MAAMqC,UAAU,IAAIC,IAAItC,WAAWW,MAAM,CAACC,GAAG,CAAC,CAACC,QAAUA,MAAMC,KAAK;IACpE,OAAO,CAACyB;QACN,MAAMC,QAAQD,KAAKC,KAAK;QACxB,MAAMC,UAAU,IAAIH;QACpB,MAAMI,YAAsB,EAAE;QAC9B,MAAMC,UAA0F,EAAE;QAClG,MAAMC,SAAmB,EAAE;QAC3B,IAAIC,aAAa;QACjB,KAAK,MAAM,EAAEhD,EAAE,EAAEkC,IAAI,EAAEe,SAAS,EAAE,IAAI7F,SAAS;YAAE8F,QAAQR,KAAKQ,MAAM;YAAEP;QAAM,GAAI;YAC9E,MAAM7B,SAASW,UAAUS,KAAKG,KAAK,EAAEd,OAAO,CAAC3E;YAC7C,MAAMuG,SAASrC,OAAOsC,MAAM,CAAC,CAACnC,QAAU,CAACuB,QAAQa,GAAG,CAACpC;YACrD,IAAIkC,OAAO/B,MAAM,EAAE;gBACjB,KAAK,MAAMH,SAASkC,OAAQP,QAAQU,GAAG,CAACrC;gBACxC4B,UAAUlB,IAAI,CAAC3B;YACjB;YACA,MAAMuD,QAAQlC,eAAe3B,GAAG,CAACwC,KAAKC,WAAW;YACjD,MAAMqB,SAASP,UAAUQ,IAAI,CAAC,CAACC;oBAAaf;uBAAAA,EAAAA,kBAAAA,KAAK,CAACe,SAAS,qBAAff,gBAAiBR,WAAW,MAAK;;YAC7E,IAAIoB,SAASA,MAAMhD,OAAO,KAAKJ,WAAWI,OAAO,IAAI,CAACiD,QAAQ;gBAC5DV,QAAQnB,IAAI,CAAC;oBAAE3B;oBAAImC,aAAaD,KAAKC,WAAW;oBAAEoB;gBAAM;YAC1D;YACA,IAAItB,YAAYC,OAAO;gBACrBa,OAAOpB,IAAI,CAAC3B;gBACZ,IAAIc,OAAO6C,QAAQ,CAACxD,WAAWU,UAAU,GAAGmC,aAAa;YAC3D;QACF;QACA,MAAMY,aAAoC,EAAE;QAC5C,IAAIhB,QAAQiB,IAAI,EAAE;YAChBD,WAAWjC,IAAI,CAAC;gBACdmC,MAAM;gBACNC,MAAM;gBACNC,SAAS,CAAC,WAAW,EAAE;uBAAIpB;iBAAQ,CAACqB,KAAK,CAAC,GAAG,GAAG/E,IAAI,CAAC,MAAM,QAAQ,EAAEiB,WAAWS,IAAI,CAAC,iEAAiE,CAAC;gBACvJsD,SAAS9F,eAAeyE,WAAWH,KAAKyB,SAAS;YACnD;QACF;QACA,kEAAkE;QAClE,IAAIpB,OAAO3B,MAAM,KAAK,KAAK,CAAC4B,YAAY;YACtCY,WAAWjC,IAAI,CAAC;gBACdmC,MAAM;gBACNC,MAAM;gBACNC,SAAS,CAAC,sEAAsE,EAAE7D,WAAWU,UAAU,CAAC,WAAW,CAAC;gBACpHqD,SAAS9F,eAAe2E,QAAQL,KAAKyB,SAAS;YAChD;QACF;QACA,IAAIrB,QAAQ1B,MAAM,EAAE;;gBAKA/D;YAJlB,MAAM,CAAC+G,MAAM,GAAGtB;YAChBc,WAAWjC,IAAI,CAAC;gBACdmC,MAAM;gBACNC,MAAM;gBACNC,SAAS,CAAC,IAAI,WAAE3G,gCAAAA,UAAU,CAAC+G,MAAMjC,WAAW,CAAC,qBAA7B9E,8BAA+BgH,WAAW,mBAAID,MAAMjC,WAAW,CAAC,4BAA4B,EAAEiC,MAAMb,KAAK,CAAC3C,IAAI,CAAC,kCAAkC,EAAET,WAAWS,IAAI,CAAC,YAAY,CAAC;gBAChMsD,SAAS9F,eACP0E,QAAQ/B,GAAG,CAAC,CAACC,QAAUA,MAAMhB,EAAE,GAC/B0C,KAAKyB,SAAS;YAElB;QACF;QACA,OAAOP;IACT;AACF;AAMA;;;;CAIC,GACD,OAAO,SAASU,6BAA6BC,OAAmC,CAAC,CAAC;QACzDA;IAAvB,MAAMC,kBAAiBD,uBAAAA,KAAKC,cAAc,YAAnBD,uBAAuBpF;IAC9C,OAAO,OAAOsF;QACZ,MAAMC,SAAS5H,yBAAyB2H,QAAQC,MAAM;QACtD,IAAI,OAAOA,WAAW,UAAU,OAAO;YAAEC,QAAQ;YAAKC,OAAOF;QAAO;QACpE,OAAOhH,wBAAwB+G,QAAQrF,SAAS,EAAE;YAChDyF,OAAOJ,QAAQI,KAAK;YACpBxF,QAAQoF,QAAQpF,MAAM;YACtByF,MAAM;YACNC,KAAKN,QAAQM,GAAG;YAChBC,UAAU,OAAO3F,SACfqF,OAAOpF,YAAY,IAAI,CAAE,MAAMkF,eAAeC,QAAQrF,SAAS,EAAEC,QAAQqF,OAAOpF,YAAY,IACxF;oBAAEqF,QAAQ;oBAAKC,OAAO;gBAA8C,IACpE;QACR;IACF;AACF;AAYA,OAAO,SAASK,wBAAwBV,OAA8B,CAAC,CAAC;QAChDA,qBACJA,iBACKA,sBACNA;IAHjB,MAAMW,iBAAgBX,sBAAAA,KAAKW,aAAa,YAAlBX,sBAAsBhH;IAC5C,MAAM4H,aAAYZ,kBAAAA,KAAKY,SAAS,YAAdZ,kBAAkB9H;IACpC,MAAM+H,kBAAiBD,uBAAAA,KAAKC,cAAc,YAAnBD,uBAAuBpF;IAC9C,MAAMiG,YAAWb,iBAAAA,KAAKa,QAAQ,YAAbb,iBAAiBjH;IAClC,OAAO,OAAO,EAAEmD,GAAG,EAAE4E,GAAG,EAAEC,MAAM,EAAElG,SAAS,EAAEmG,QAAQ,EAAE;kBAmCxCC;QAlCb,MAAMnG,SAASoB,IAAIpB,MAAM;QACzB,IAAI,CAACA,QAAQ,MAAM,IAAIoG,MAAM;QAC7B,MAAMf,SAAS5H,yBAAyB2D,IAAIiE,MAAM;QAClD,IAAI,OAAOA,WAAW,UAAU;YAC9B,MAAM,IAAIe,MAAM,CAAC,8DAA8D,EAAEf,QAAQ;QAC3F;QACA,MAAMvE,aAAazD,+BAA+B,CAACgI,OAAOnE,OAAO,CAAC;QAClE,MAAMmF,SAAS,CAACC,OAAsBC,OAA+C;gBACnFC,UAAU;gBACV7F,IAAI2F,MAAM3F,EAAE;gBACZ8F,WAAW;gBACXzG;gBACA0G,eAAeJ,MAAMI,aAAa;gBAClC7E,OAAOyE,MAAM1F,IAAI;eACb2F,OAAO;gBAAEA;YAAK,IAAI,CAAC;QAEzB,yEAAyE;QACzE,MAAMI,gBAAQT,4BAAAA,SAAW,kCAAmBtI,eAAe;QAE3D,kGAAkG;QAClG,MAAMgJ,UAAUxI,aAAagD,KAAK;QAClC,MAAMyF,UAAU,MAAMrI,YAAYuB,WAAW;YAAE0F,MAAM;YAAYzF;YAAQW,IAAIiG;QAAQ;QACrF,IAAIC,SAAS,OAAO3H,iBAAiByH,OAAO;YAAEG,SAAS;gBAACT,OAAOQ;aAAS;QAAC;QAEzE,MAAM,CAACE,WAAWZ,aAAahG,WAAW,GAAG,MAAM6G,QAAQC,GAAG,CAAC;YAC7DpB,cAAczE,IAAIoE,KAAK,EAAExF,QAAQ;gBAAED;YAAU;YAC7CA,UAAUI,UAAU,CAAC,QAAQC,GAAG,CAACgB,IAAIoE,KAAK,EAAEnF,GAAG;YAC/CgF,OAAOpF,YAAY,GACfkF,eAAepF,WAAWC,QAAQqF,OAAOpF,YAAY,IACrD+G,QAAQE,OAAO,CAAC;SACrB;QACD,IAAI7B,OAAOpF,YAAY,IAAI,CAACE,YAAY;YACtC,MAAM,IAAIiG,MAAM,CAAC,WAAW,EAAEf,OAAOpF,YAAY,CAAC,WAAW,EAAED,OAAO,oCAAoC,CAAC;QAC7G;QACA,MAAM0F,OAAOS,oBAAAA,YAAY5F,IAAI,cAAhB4F,oBAAsB;QACnC,MAAM9E,OAAO3C,gBAAgB0C;QAC7B,MAAM+F,WAAWnI,eAAeqC,MAAM;QACtC,MAAMT,OAAOC,oBAAoBC,YAAYX,YAAYgH,4BAAAA,SAAUvG,IAAI;QAEvE,wEAAwE;QACxE,uEAAuE;QACvE,iDAAiD;QACjD,IACEuG,CAAAA,4BAAAA,SAAUC,WAAW,KACrBL,UAAUM,SAAS,CAACjD,IAAI,CAAC,CAACkD,WAAaA,SAAS3G,EAAE,KAAKwG,SAASC,WAAW,GAC3E;YACA,MAAMG,OAAO,MAAMzB,UAAU,YAAY;gBACvCN,OAAOpE,IAAIoE,KAAK;gBAChBxF;gBACAwH,UAAUL,SAASC,WAAW;gBAC9BxG;gBACA6G,KAAKrG,IAAIsG,SAAS;gBAClBhC,KAAKA;YACP;YACA,IAAI6B,KAAKI,EAAE,EAAE;gBACX,MAAMjB,gBAAgB,MAAMnI,gBAAgBwB,WAAWC;gBACvD,OAAOd,iBAAiByH,OAAO;oBAC7BG,SAAS;wBAACT,OAAO;4BAAE1F,IAAI4G,KAAK5G,EAAE;4BAAE8F,WAAW;4BAAM7F,MAAM2G,KAAK3G,IAAI;4BAAE8F;wBAAc;qBAAG;gBACrF;YACF;YACA,IAAIa,KAAKI,EAAE,KAAK,SAASJ,KAAKjC,MAAM,KAAK,KAAK;gBAC5C,OAAOpG,iBAAiByH,OAAO;oBAAEiB,QAAQ9I,cAAcyI,KAAKhC,KAAK;gBAAE;YACrE;QACF;QAEA,MAAMsC,YAAY,MAAMvJ,wBAAwByB,WAAW;YAAE0F,MAAM;YAAYzF;YAAQ0F;QAAI;QAC3F,IAAImC,WAAW,OAAO3I,iBAAiByH,OAAO;YAAEiB,QAAQ9I,cAAc+I;QAAW;QAEjF,MAAMC,SAAS,MAAMhK,uBAAuB,YAAY;YACtDiK,MAAM;YACNpB;YACAqB,cAAc;mBAAIrI;gBAA8BoG;aAAW;YAC3DgB;YACAkB,UAAU;gBACR;oBACEC,MAAM;oBACNC,SAAShH,oBAAoBC,KAAKN,YAAYX,YAAYkB,MAAMT;gBAClE;aACD;YACDwH,MAAMvK,mBAAmB;YACzByB,WAAWC,4BAA4BD,SAAS,CAACqH;WAC7ChJ,gBAAgB,CAAC,eAAe,CAAC0K,QAAQ,GAAG;YAAEA,UAAU1K,gBAAgB,CAAC,eAAe,CAAC0K,QAAQ;QAAC,IAAI,CAAC,GACvG1K,gBAAgB,CAAC,eAAe,CAAC2K,MAAM,GAAG;YAAEA,QAAQ3K,gBAAgB,CAAC,eAAe,CAAC2K,MAAM;QAAC,IAAI,CAAC;YACrGlD,SAAS;gBAAEmD,eAAe/K,wBAAwBsD;YAAY;YAC9D0H,QAAQtF,uBAAuBpC;WAC3BmF,SAAS;YAAEA;QAAO,IAAI,CAAC;QAE7B,MAAMwC,QAAQ7J,kBAAkBkJ;QAChC,IAAIA,OAAOxC,MAAM,KAAK,WAAW,OAAO,aAAKmD;YAAOC,SAAS;;QAC7D,IAAIZ,OAAOxC,MAAM,KAAK,eAAe,OAAO,aAAKmD;YAAOb,QAAQjJ,iBAAiBmJ;;QAEjF,MAAMxB,QAAQ,MAAM7H,aAAasB,WAAW;YAC1C0F,MAAM;YACNzF;YACAW,IAAIiG;YACJa,KAAKrG,IAAIsG,SAAS;YAClBhC;YACA9E;YACA0C,OAAOwE,OAAOtH,KAAK,CAAC8C,KAAK;YACzB5C,MAAMO,oBAAoBH,YAAYX;YACtC6F;QACF;QACA,IAAIM,MAAMqB,EAAE,KAAK,OAAO;YACtB,IAAIrB,MAAMhB,MAAM,KAAK,KAAK,MAAM,IAAIc,MAAM,CAAC,KAAK,EAAEpG,OAAO,0CAA0C,CAAC;YACpG,OAAO,aAAKyI;gBAAOb,QAAQ9I,cAAcwH,MAAMf,KAAK;;QACtD;QACA,OAAO,aAAKkD;YAAO3B,SAAS;gBAACT,OAAOC,OAAOwB,OAAOtH,KAAK,CAAC+F,IAAI;aAAE;;IAChE;AACF;AAEA,OAAO,MAAMoC,uBAAuB/C,0BAAyB;AAE7D,qGAAqG,GACrG,OAAO,SAASgD;IACdxJ,kBAAkB,YAAYuJ,sBAAsB;QAAEjJ,WAAWD;IAAgC;IACjGtB,uBAAuB,YAAY8G;AACrC"}
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 { 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 type { AiJob, AiJobOutput, AiJobPlan } from '../model/ai-jobs.types'\nimport {\n AI_TEMPLATE_SUBJECT_DEFINITIONS,\n AI_TEMPLATE_SUBJECTS,\n aiBindingTokensIn,\n aiTemplateAddressTokens,\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 writeAiDraft,\n type AiDraftRecord,\n} from './ai-job-drafts'\nimport {\n aiConfirmedPlan,\n aiDoctrineReview,\n aiGenerationSpent,\n aiJobBriefLine,\n aiLimitReview,\n aiModelNodeIds,\n aiPlanCreation,\n aiPlanReferenceLines,\n aiUnspentOutcome,\n} from './ai-job-generation'\nimport { aiPlanTemplateTokenViolations } from './ai-job-plan-conformance'\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, and no\n * block that fills itself only on another subject's page.\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.',\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. */\nexport function aiJobTemplatePrompt(\n job: Pick<AiJob, 'brief'>,\n definition: AiTemplateSubjectDefinition,\n collection: AiTemplateCollection | null,\n plan: AiJobPlan | null,\n name: 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 ].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 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, and 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.\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 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 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 // 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 : null,\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\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.\n if (\n creation?.duplicateOf &&\n inventory.templates.some((template) => template.id === creation.duplicateOf)\n ) {\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 })\n if (copy.ok) {\n const hostSubdomain = await aiSiteSubdomain(firestore, hostId)\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),\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: a token the\n // plan promised that the template does not bind (AGL-3143 §11).\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 ],\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 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\nexport const runAiJobTemplateStep = 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, { minimumMs: AI_JOB_TEMPLATE_STEP_MINIMUM_MS })\n registerAiJobAdmission('template', createAiTemplateJobAdmission())\n}\n"],"names":["hostCollectionKind","normalizeScreenSlug","duplicateResource","AI_TEMPLATE_SUBJECT_DEFINITIONS","AI_TEMPLATE_SUBJECTS","aiBindingTokensIn","aiTemplateAddressTokens","parseAiTemplateJobInputs","AI_STEP_TIERS","AI_ROUTING_TABLE","aiModelForStep","aiDoctrineTreeTool","runValidatedGeneration","walkTree","AI_PALETTE","aiTemplateExamplesSystemBlock","readSiteInventory","registerAiJobAdmission","aiJobDraftId","aiDraftAdmissionRefusal","aiDraftAllowanceRefusal","aiSiteSubdomain","readAiDraft","writeAiDraft","aiConfirmedPlan","aiDoctrineReview","aiGenerationSpent","aiJobBriefLine","aiLimitReview","aiModelNodeIds","aiPlanCreation","aiPlanReferenceLines","aiUnspentOutcome","aiPlanTemplateTokenViolations","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","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","aiTemplateBindingCheck","allowed","Set","tree","nodes","unknown","unknownAt","foreign","titles","titleBound","ancestors","rootId","strays","filter","has","add","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","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","template","copy","sourceId","uid","createdBy","ok","review","allowance","result","step","instructions","messages","role","content","tool","thinking","effort","bindingTokens","extend","spent","refused","runAiJobTemplateStep","registerAiTemplateJob"],"mappings":"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"}
@@ -71,9 +71,12 @@ export interface AiBuildPlanCreate {
71
71
  * A component: its props as `name:type`, an icon as `name:icon`
72
72
  * (AGL-3054). A layout: the regions it has, from the closed vocabulary
73
73
  * `AI_LAYOUT_REGIONS` holds (AGL-3024) — a layout has no props, and a
74
- * region named here is what the built layout is held to. A form or a
75
- * dataset: its field names. A theme change: the palette paths it adds or
76
- * changes.
74
+ * region named here is what the built layout is held to. A template: the
75
+ * binding tokens its page shows, from the subject's own closed catalog
76
+ * (`AI_TEMPLATE_SUBJECT_DEFINITIONS[subject].tokens`, AGL-3143) — the
77
+ * template's analogue of a layout's regions, and what the built template is
78
+ * held to. A form or a dataset: its field names. A theme change: the
79
+ * palette paths it adds or changes.
77
80
  */
78
81
  fields: string[];
79
82
  /**
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-build-plan.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { AiTool } from '../providers/contract'\n\n/**\n * The plan a generation job builds from (AGL-2935): what the job's plan step\n * (`jobs/ai-job-plan-step.ts`) returns, before a single node is generated.\n *\n * This module is the plan's MODEL — its shape, its tool schema and its\n * reader — and imports nothing at runtime, so the doctrine validators, the\n * plan step, the job document and the proposal can all read it without\n * importing each other.\n *\n * A plan is REFERENCES, never documents (AGL-2937). It names what the site\n * already has by inventory id, what the job will create by a short name, and\n * which screens it builds from which of those — so the doctrine can be held\n * against the plan while fixing it costs a sentence, not a regenerated page,\n * and the customer reads what the job will do before it spends a credit on\n * doing it.\n *\n * `new:<name>` is how an entry refers to something the plan itself creates;\n * any other reference is an id from the site inventory.\n */\n\nexport const AI_PLAN_NEW_REF_PREFIX = 'new:'\n\n/** Records a plan may reuse, by the inventory list they come from. */\nexport type AiBuildPlanReuseKind =\n | 'component'\n | 'layout'\n | 'template'\n | 'form'\n | 'dataset'\n | 'collection'\n | 'screen'\n | 'theme'\n\nexport const AI_BUILD_PLAN_REUSE_KINDS: readonly AiBuildPlanReuseKind[] = [\n 'component',\n 'layout',\n 'template',\n 'form',\n 'dataset',\n 'collection',\n 'screen',\n 'theme',\n]\n\n/** What a plan may create. A theme change is a draft through the theme door, never a literal. */\nexport type AiBuildPlanCreateKind =\n | 'component'\n | 'layout'\n | 'template'\n | 'form'\n | 'theme-change'\n | 'dataset'\n | 'email'\n\n/**\n * What each creation is called in a sentence, and where a member makes one.\n *\n * Total over the union deliberately: a refusal that cannot name a creation is\n * a refusal a member cannot act on, so widening `AiBuildPlanCreateKind`\n * without saying where the new thing is made does not compile.\n */\nexport const AI_BUILD_PLAN_CREATION_NOUNS: Record<\n AiBuildPlanCreateKind,\n { noun: string; where: string }\n> = {\n component: { noun: 'component', where: 'on the Components page' },\n form: { noun: 'form', where: 'on the Forms page' },\n layout: { noun: 'layout', where: 'on the Layouts page' },\n template: { noun: 'template', where: 'in the Templates library' },\n 'theme-change': { noun: 'theme change', where: 'in the Theme section' },\n dataset: { noun: 'dataset', where: 'on the Datasets page' },\n email: { noun: 'email design', where: 'in Emails → Templates' },\n}\n\nexport const AI_BUILD_PLAN_CREATE_KINDS: readonly AiBuildPlanCreateKind[] = [\n 'component',\n 'layout',\n 'template',\n 'form',\n 'theme-change',\n 'dataset',\n 'email',\n]\n\nexport interface AiBuildPlanReuse {\n kind: AiBuildPlanReuseKind\n /** The inventory id. */\n id: string\n /** What the plan uses it for, in a few words. */\n purpose: string\n}\n\nexport interface AiBuildPlanCreate {\n kind: AiBuildPlanCreateKind\n /** Unique within the plan; referenced as `new:<name>`. */\n name: string\n /** Why nothing the site has will do — shown in the proposal. */\n why: string\n /** The inventory id this starts from as a duplicate, or null. */\n duplicateOf: string | null\n /**\n * A component: its props as `name:type`, an icon as `name:icon`\n * (AGL-3054). A layout: the regions it has, from the closed vocabulary\n * `AI_LAYOUT_REGIONS` holds (AGL-3024) — a layout has no props, and a\n * region named here is what the built layout is held to. A form or a\n * dataset: its field names. A theme change: the palette paths it adds or\n * changes.\n */\n fields: string[]\n /**\n * The id the draft this creation becomes is written under, where a unit of\n * the job builds it (AGL-3079). Minted when the job keeps the plan, never\n * part of the model's answer.\n */\n id?: string\n}\n\nexport interface AiBuildPlanSection {\n /** A few words: \"hero\", \"services grid\", \"contact form\". */\n name: string\n /** What the section places: inventory ids or `new:<name>` references. */\n uses: string[]\n /** How many repeated items it shows (cards, rows, tiles); 0 when none. */\n items: number\n}\n\nexport interface AiBuildPlanScreen {\n title: string\n slug: string\n /** The layout it renders inside. */\n layout: string | null\n /** The template it applies, or null for a one-off page. */\n template: string | null\n /** The inventory screen it starts from as a duplicate, or null. */\n duplicateOf: string | null\n /** Whether the site navigation gains an entry for it. */\n nav: boolean\n seoTitle: string\n seoDescription: string\n sections: AiBuildPlanSection[]\n /**\n * The id the draft screen is written under, where a unit of the job builds\n * it (AGL-3079). Minted when the job keeps the plan, never part of the\n * model's answer.\n */\n id?: string\n}\n\nexport interface AiBuildPlan {\n reuse: AiBuildPlanReuse[]\n create: AiBuildPlanCreate[]\n screens: AiBuildPlanScreen[]\n}\n\n/**\n * How much one plan may hold. A job builds what one proposal can describe\n * and one person can read before confirming; a brief asking for more is\n * several jobs.\n */\nexport const AI_BUILD_PLAN_LIMITS = {\n reuse: 40,\n create: 20,\n screens: 12,\n sections: 16,\n uses: 8,\n fields: 30,\n text: 200,\n seoDescription: 320,\n items: 500,\n} as const\n\nconst string = (description: string) => ({ type: 'string', description })\nconst nullableString = (description: string) => ({\n anyOf: [{ type: 'string' }, { type: 'null' }],\n description,\n})\nconst strings = (description: string) => ({\n type: 'array',\n items: { type: 'string' },\n description,\n})\n\n/**\n * The ceiling `parseAiBuildPlan` cuts a field's text at, in words (AGL-3022).\n * The reader repairs an overlong value by cutting it, and a model never told\n * where the cut falls writes a rationale that stops mid-sentence there.\n */\nconst atMost = (limit: number = AI_BUILD_PLAN_LIMITS.text) => `at most ${limit} characters`\n\n/**\n * The plan as a strict tool's input schema. Only the keywords strict\n * structured output accepts: every object closes its properties and lists\n * every one as required, an optional value is a `null` branch, and no\n * length or count bound appears as a keyword — those are enforced by\n * `parseAiBuildPlan`, which every answer passes through anyway.\n *\n * A text ceiling is stated in words instead, on each field the model writes.\n * A reference is copied from the inventory or from a creation's name, so its\n * length is not the model's to choose. A search title and description are\n * held by rule 10 to ceilings far inside the reader's, which the doctrine\n * states, so naming the reader's here would contradict the rule.\n */\nexport const AI_BUILD_PLAN_TOOL: AiTool = {\n name: 'submit_build_plan',\n description:\n 'Submit the build plan: what the site already has that this job reuses, what it creates and why, and the screens it builds from them.',\n strict: true,\n inputSchema: {\n type: 'object',\n additionalProperties: false,\n required: ['reuse', 'create', 'screens'],\n properties: {\n reuse: {\n type: 'array',\n description: 'Records the site already has that this plan uses, by inventory id.',\n items: {\n type: 'object',\n additionalProperties: false,\n required: ['kind', 'id', 'purpose'],\n properties: {\n kind: { type: 'string', enum: [...AI_BUILD_PLAN_REUSE_KINDS] },\n id: string('The id from the site inventory.'),\n purpose: string(`What the plan uses it for, in a few words, ${atMost()}.`),\n },\n },\n },\n create: {\n type: 'array',\n description:\n 'What the plan creates. Creating is the exception: each entry says why nothing listed will do.',\n items: {\n type: 'object',\n additionalProperties: false,\n required: ['kind', 'name', 'why', 'duplicateOf', 'fields'],\n properties: {\n kind: { type: 'string', enum: [...AI_BUILD_PLAN_CREATE_KINDS] },\n name: string(\n `Unique within the plan, ${atMost()}; other entries refer to it as new:<name>.`,\n ),\n why: string(`Why nothing the site already has will do, in one sentence of ${atMost()}.`),\n duplicateOf: nullableString(\n 'The inventory id this starts from as a duplicate, or null.',\n ),\n fields: strings(\n `A component: its props as name:type, an icon as name:icon. A layout: the regions it has, each one of header, nav, sidebar, main, footer — list every region the finished layout has, not only the ones it gains. A form or dataset: its field names. A theme change: the palette paths it adds or changes. Each ${atMost()}.`,\n ),\n },\n },\n },\n screens: {\n type: 'array',\n description: 'The screens the job builds, each a new draft.',\n items: {\n type: 'object',\n additionalProperties: false,\n required: [\n 'title',\n 'slug',\n 'layout',\n 'template',\n 'duplicateOf',\n 'nav',\n 'seoTitle',\n 'seoDescription',\n 'sections',\n ],\n properties: {\n title: string(`The screen name, ${atMost()}.`),\n slug: string(\n `The path, ${atMost()}: lowercase words joined by hyphens, e.g. /services/roof-repair.`,\n ),\n layout: nullableString(\n 'The layout it renders inside: an inventory id or new:<name>.',\n ),\n template: nullableString(\n 'The template it applies (inventory id or new:<name>), or null for a one-off page.',\n ),\n duplicateOf: nullableString(\n 'The inventory screen it starts from as a duplicate, or null.',\n ),\n nav: {\n type: 'boolean',\n description: 'Whether the site navigation gains an entry for it.',\n },\n seoTitle: string('The search-result title.'),\n seoDescription: string('The search-result description.'),\n sections: {\n type: 'array',\n description: 'The sections, top to bottom.',\n items: {\n type: 'object',\n additionalProperties: false,\n required: ['name', 'uses', 'items'],\n properties: {\n name: string(`A few words, ${atMost()}: hero, services grid, contact form.`),\n uses: strings('What the section places: inventory ids or new:<name>.'),\n items: {\n type: 'integer',\n description: 'How many repeated items it shows; 0 when none.',\n },\n },\n },\n },\n },\n },\n },\n },\n },\n}\n\nexport type AiBuildPlanParse =\n | { ok: true; plan: AiBuildPlan; repairs: string[] }\n | { ok: false; error: string }\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\nclass PlanShapeError extends Error {}\n\n/**\n * Read a plan out of a tool call, or name what makes it unreadable.\n *\n * Structure is refused — a missing list, an unknown kind, more screens than\n * a job holds, two creations under one name — because a plan that reads\n * differently from what the model meant is worse than a re-ask. Length is\n * repaired: copy past a ceiling is cut and the cut is listed.\n */\nexport function parseAiBuildPlan(input: unknown): AiBuildPlanParse {\n const repairs: string[] = []\n const text = (\n value: unknown,\n path: string,\n limit: number = AI_BUILD_PLAN_LIMITS.text,\n ): string => {\n if (typeof value !== 'string') throw new PlanShapeError(`${path} is not text`)\n const trimmed = value.trim()\n if (trimmed.length > limit) {\n repairs.push(`${path} was over ${limit} characters; truncated`)\n // On the last word break inside the ceiling, not through a word: a\n // rationale is read by the member confirming the plan, and half a word\n // reads as a broken answer rather than a long one (AGL-3022). A run of\n // `limit` characters with no break in it still has to be cut where the\n // ceiling falls.\n const cut = trimmed.slice(0, limit)\n const brk = cut.search(/\\s\\S*$/)\n return (brk > 0 ? cut.slice(0, brk) : cut).trimEnd()\n }\n return trimmed\n }\n const nullable = (value: unknown, path: string): string | null =>\n value === null || value === undefined || value === '' ? null : text(value, path) || null\n const list = (value: unknown, path: string, limit: number): unknown[] => {\n if (!Array.isArray(value)) throw new PlanShapeError(`${path} is not a list`)\n if (value.length > limit) {\n throw new PlanShapeError(`${path} lists ${value.length}; one plan holds at most ${limit}`)\n }\n return value\n }\n const record = (value: unknown, path: string): Record<string, unknown> => {\n if (!isRecord(value)) throw new PlanShapeError(`${path} is not an object`)\n return value\n }\n const oneOf = <T extends string>(value: unknown, allowed: readonly T[], path: string): T => {\n if (typeof value !== 'string' || !(allowed as readonly string[]).includes(value)) {\n throw new PlanShapeError(`${path} is not one of ${allowed.join(', ')}`)\n }\n return value as T\n }\n\n try {\n const root = record(input, 'plan')\n const reuse = list(root['reuse'], 'reuse', AI_BUILD_PLAN_LIMITS.reuse).map((raw, index) => {\n const entry = record(raw, `reuse[${index}]`)\n const id = text(entry['id'], `reuse[${index}].id`)\n if (!id) throw new PlanShapeError(`reuse[${index}].id is empty`)\n return {\n kind: oneOf(entry['kind'], AI_BUILD_PLAN_REUSE_KINDS, `reuse[${index}].kind`),\n id,\n purpose: text(entry['purpose'], `reuse[${index}].purpose`),\n }\n })\n const names = new Set<string>()\n const create = list(root['create'], 'create', AI_BUILD_PLAN_LIMITS.create).map(\n (raw, index) => {\n const entry = record(raw, `create[${index}]`)\n const name = text(entry['name'], `create[${index}].name`)\n if (!name) throw new PlanShapeError(`create[${index}].name is empty`)\n const key = name.toLowerCase()\n if (names.has(key)) {\n throw new PlanShapeError(`create[${index}].name \"${name}\" is used twice`)\n }\n names.add(key)\n return {\n kind: oneOf(entry['kind'], AI_BUILD_PLAN_CREATE_KINDS, `create[${index}].kind`),\n name,\n why: text(entry['why'], `create[${index}].why`),\n duplicateOf: nullable(entry['duplicateOf'], `create[${index}].duplicateOf`),\n fields: list(entry['fields'] ?? [], `create[${index}].fields`, AI_BUILD_PLAN_LIMITS.fields)\n .map((field, fieldIndex) => text(field, `create[${index}].fields[${fieldIndex}]`))\n .filter(Boolean),\n }\n },\n )\n const screens = list(root['screens'], 'screens', AI_BUILD_PLAN_LIMITS.screens).map(\n (raw, index) => {\n const entry = record(raw, `screens[${index}]`)\n if (typeof entry['nav'] !== 'boolean') {\n throw new PlanShapeError(`screens[${index}].nav is not true or false`)\n }\n return {\n title: text(entry['title'], `screens[${index}].title`),\n slug: text(entry['slug'], `screens[${index}].slug`),\n layout: nullable(entry['layout'], `screens[${index}].layout`),\n template: nullable(entry['template'], `screens[${index}].template`),\n duplicateOf: nullable(entry['duplicateOf'], `screens[${index}].duplicateOf`),\n nav: entry['nav'],\n seoTitle: text(entry['seoTitle'], `screens[${index}].seoTitle`),\n seoDescription: text(\n entry['seoDescription'],\n `screens[${index}].seoDescription`,\n AI_BUILD_PLAN_LIMITS.seoDescription,\n ),\n sections: list(\n entry['sections'],\n `screens[${index}].sections`,\n AI_BUILD_PLAN_LIMITS.sections,\n ).map((rawSection, sectionIndex) => {\n const path = `screens[${index}].sections[${sectionIndex}]`\n const section = record(rawSection, path)\n const items = Number(section['items'] ?? 0)\n if (!Number.isInteger(items) || items < 0 || items > AI_BUILD_PLAN_LIMITS.items) {\n throw new PlanShapeError(`${path}.items is not a count`)\n }\n return {\n name: text(section['name'], `${path}.name`),\n uses: list(section['uses'] ?? [], `${path}.uses`, AI_BUILD_PLAN_LIMITS.uses)\n .map((ref, refIndex) => text(ref, `${path}.uses[${refIndex}]`))\n .filter(Boolean),\n items,\n }\n }),\n }\n },\n )\n return { ok: true, plan: { reuse, create, screens }, repairs }\n } catch (error) {\n if (error instanceof PlanShapeError) return { ok: false, error: error.message }\n throw error\n }\n}\n\n/** Whether a reference names something the plan creates. */\nexport function isAiPlanNewRef(ref: string | null | undefined): ref is string {\n return typeof ref === 'string' && ref.startsWith(AI_PLAN_NEW_REF_PREFIX)\n}\n\n/** The creation a `new:<name>` reference names, case-insensitively. */\nexport function aiPlanCreateFor(\n plan: AiBuildPlan,\n ref: string,\n): AiBuildPlanCreate | undefined {\n if (!isAiPlanNewRef(ref)) return undefined\n const name = ref.slice(AI_PLAN_NEW_REF_PREFIX.length).trim().toLowerCase()\n return plan.create.find((entry) => entry.name.toLowerCase() === name)\n}\n\n/**\n * A `new:<name>` reference that names no entry of the plan's create list: a\n * screen's own layout or template, or what one of its sections places, with\n * that section's index.\n */\nexport type AiPlanUndeclaredRef = {\n /** The name the reference gives the creation, as written after `new:`. */\n name: string\n /** Where the plan writes it: `screens[0].template`, `screens[0].sections[4].uses[0]`. */\n path: string\n /** The screen that writes it, by index. */\n screenIndex: number\n} & ({ field: 'layout' | 'template'; sectionIndex: null } | { field: 'uses'; sectionIndex: number })\n\n/**\n * Every `new:<name>` reference the plan's create list does not carry, in plan\n * order: each screen's layout, then its template, then what its sections\n * place. A plan that places a creation it never declares has nothing to build\n * it from, so the plan rules refuse each (AGL-3040), and the page and\n * scaffold doors still name one in a plan confirmed before they did.\n */\nexport function aiPlanUndeclaredRefs(plan: AiBuildPlan): AiPlanUndeclaredRef[] {\n const found: AiPlanUndeclaredRef[] = []\n const undeclared = (ref: string | null): ref is string =>\n isAiPlanNewRef(ref) && !aiPlanCreateFor(plan, ref)\n const nameOf = (ref: string) => ref.slice(AI_PLAN_NEW_REF_PREFIX.length).trim()\n plan.screens.forEach((screen, screenIndex) => {\n for (const field of ['layout', 'template'] as const) {\n const ref = screen[field]\n if (!undeclared(ref)) continue\n found.push({\n name: nameOf(ref),\n path: `screens[${screenIndex}].${field}`,\n screenIndex,\n field,\n sectionIndex: null,\n })\n }\n screen.sections.forEach((section, sectionIndex) => {\n section.uses.forEach((ref, useIndex) => {\n if (!undeclared(ref)) return\n found.push({\n name: nameOf(ref),\n path: `screens[${screenIndex}].sections[${sectionIndex}].uses[${useIndex}]`,\n screenIndex,\n field: 'uses',\n sectionIndex,\n })\n })\n })\n })\n return found\n}\n"],"names":["AI_PLAN_NEW_REF_PREFIX","AI_BUILD_PLAN_REUSE_KINDS","AI_BUILD_PLAN_CREATION_NOUNS","component","noun","where","form","layout","template","dataset","email","AI_BUILD_PLAN_CREATE_KINDS","AI_BUILD_PLAN_LIMITS","reuse","create","screens","sections","uses","fields","text","seoDescription","items","string","description","type","nullableString","anyOf","strings","atMost","limit","AI_BUILD_PLAN_TOOL","name","strict","inputSchema","additionalProperties","required","properties","kind","enum","id","purpose","why","duplicateOf","title","slug","nav","seoTitle","isRecord","value","Array","isArray","PlanShapeError","Error","parseAiBuildPlan","input","repairs","path","trimmed","trim","length","push","cut","slice","brk","search","trimEnd","nullable","undefined","list","record","oneOf","allowed","includes","join","root","map","raw","index","entry","names","Set","key","toLowerCase","has","add","field","fieldIndex","filter","Boolean","rawSection","sectionIndex","section","Number","isInteger","ref","refIndex","ok","plan","error","message","isAiPlanNewRef","startsWith","aiPlanCreateFor","find","aiPlanUndeclaredRefs","found","undeclared","nameOf","forEach","screen","screenIndex","useIndex"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-build-plan.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { AiTool } from '../providers/contract'\n\n/**\n * The plan a generation job builds from (AGL-2935): what the job's plan step\n * (`jobs/ai-job-plan-step.ts`) returns, before a single node is generated.\n *\n * This module is the plan's MODEL — its shape, its tool schema and its\n * reader — and imports nothing at runtime, so the doctrine validators, the\n * plan step, the job document and the proposal can all read it without\n * importing each other.\n *\n * A plan is REFERENCES, never documents (AGL-2937). It names what the site\n * already has by inventory id, what the job will create by a short name, and\n * which screens it builds from which of those — so the doctrine can be held\n * against the plan while fixing it costs a sentence, not a regenerated page,\n * and the customer reads what the job will do before it spends a credit on\n * doing it.\n *\n * `new:<name>` is how an entry refers to something the plan itself creates;\n * any other reference is an id from the site inventory.\n */\n\nexport const AI_PLAN_NEW_REF_PREFIX = 'new:'\n\n/** Records a plan may reuse, by the inventory list they come from. */\nexport type AiBuildPlanReuseKind =\n | 'component'\n | 'layout'\n | 'template'\n | 'form'\n | 'dataset'\n | 'collection'\n | 'screen'\n | 'theme'\n\nexport const AI_BUILD_PLAN_REUSE_KINDS: readonly AiBuildPlanReuseKind[] = [\n 'component',\n 'layout',\n 'template',\n 'form',\n 'dataset',\n 'collection',\n 'screen',\n 'theme',\n]\n\n/** What a plan may create. A theme change is a draft through the theme door, never a literal. */\nexport type AiBuildPlanCreateKind =\n | 'component'\n | 'layout'\n | 'template'\n | 'form'\n | 'theme-change'\n | 'dataset'\n | 'email'\n\n/**\n * What each creation is called in a sentence, and where a member makes one.\n *\n * Total over the union deliberately: a refusal that cannot name a creation is\n * a refusal a member cannot act on, so widening `AiBuildPlanCreateKind`\n * without saying where the new thing is made does not compile.\n */\nexport const AI_BUILD_PLAN_CREATION_NOUNS: Record<\n AiBuildPlanCreateKind,\n { noun: string; where: string }\n> = {\n component: { noun: 'component', where: 'on the Components page' },\n form: { noun: 'form', where: 'on the Forms page' },\n layout: { noun: 'layout', where: 'on the Layouts page' },\n template: { noun: 'template', where: 'in the Templates library' },\n 'theme-change': { noun: 'theme change', where: 'in the Theme section' },\n dataset: { noun: 'dataset', where: 'on the Datasets page' },\n email: { noun: 'email design', where: 'in Emails → Templates' },\n}\n\nexport const AI_BUILD_PLAN_CREATE_KINDS: readonly AiBuildPlanCreateKind[] = [\n 'component',\n 'layout',\n 'template',\n 'form',\n 'theme-change',\n 'dataset',\n 'email',\n]\n\nexport interface AiBuildPlanReuse {\n kind: AiBuildPlanReuseKind\n /** The inventory id. */\n id: string\n /** What the plan uses it for, in a few words. */\n purpose: string\n}\n\nexport interface AiBuildPlanCreate {\n kind: AiBuildPlanCreateKind\n /** Unique within the plan; referenced as `new:<name>`. */\n name: string\n /** Why nothing the site has will do — shown in the proposal. */\n why: string\n /** The inventory id this starts from as a duplicate, or null. */\n duplicateOf: string | null\n /**\n * A component: its props as `name:type`, an icon as `name:icon`\n * (AGL-3054). A layout: the regions it has, from the closed vocabulary\n * `AI_LAYOUT_REGIONS` holds (AGL-3024) — a layout has no props, and a\n * region named here is what the built layout is held to. A template: the\n * binding tokens its page shows, from the subject's own closed catalog\n * (`AI_TEMPLATE_SUBJECT_DEFINITIONS[subject].tokens`, AGL-3143) — the\n * template's analogue of a layout's regions, and what the built template is\n * held to. A form or a dataset: its field names. A theme change: the\n * palette paths it adds or changes.\n */\n fields: string[]\n /**\n * The id the draft this creation becomes is written under, where a unit of\n * the job builds it (AGL-3079). Minted when the job keeps the plan, never\n * part of the model's answer.\n */\n id?: string\n}\n\nexport interface AiBuildPlanSection {\n /** A few words: \"hero\", \"services grid\", \"contact form\". */\n name: string\n /** What the section places: inventory ids or `new:<name>` references. */\n uses: string[]\n /** How many repeated items it shows (cards, rows, tiles); 0 when none. */\n items: number\n}\n\nexport interface AiBuildPlanScreen {\n title: string\n slug: string\n /** The layout it renders inside. */\n layout: string | null\n /** The template it applies, or null for a one-off page. */\n template: string | null\n /** The inventory screen it starts from as a duplicate, or null. */\n duplicateOf: string | null\n /** Whether the site navigation gains an entry for it. */\n nav: boolean\n seoTitle: string\n seoDescription: string\n sections: AiBuildPlanSection[]\n /**\n * The id the draft screen is written under, where a unit of the job builds\n * it (AGL-3079). Minted when the job keeps the plan, never part of the\n * model's answer.\n */\n id?: string\n}\n\nexport interface AiBuildPlan {\n reuse: AiBuildPlanReuse[]\n create: AiBuildPlanCreate[]\n screens: AiBuildPlanScreen[]\n}\n\n/**\n * How much one plan may hold. A job builds what one proposal can describe\n * and one person can read before confirming; a brief asking for more is\n * several jobs.\n */\nexport const AI_BUILD_PLAN_LIMITS = {\n reuse: 40,\n create: 20,\n screens: 12,\n sections: 16,\n uses: 8,\n fields: 30,\n text: 200,\n seoDescription: 320,\n items: 500,\n} as const\n\nconst string = (description: string) => ({ type: 'string', description })\nconst nullableString = (description: string) => ({\n anyOf: [{ type: 'string' }, { type: 'null' }],\n description,\n})\nconst strings = (description: string) => ({\n type: 'array',\n items: { type: 'string' },\n description,\n})\n\n/**\n * The ceiling `parseAiBuildPlan` cuts a field's text at, in words (AGL-3022).\n * The reader repairs an overlong value by cutting it, and a model never told\n * where the cut falls writes a rationale that stops mid-sentence there.\n */\nconst atMost = (limit: number = AI_BUILD_PLAN_LIMITS.text) => `at most ${limit} characters`\n\n/**\n * The plan as a strict tool's input schema. Only the keywords strict\n * structured output accepts: every object closes its properties and lists\n * every one as required, an optional value is a `null` branch, and no\n * length or count bound appears as a keyword — those are enforced by\n * `parseAiBuildPlan`, which every answer passes through anyway.\n *\n * A text ceiling is stated in words instead, on each field the model writes.\n * A reference is copied from the inventory or from a creation's name, so its\n * length is not the model's to choose. A search title and description are\n * held by rule 10 to ceilings far inside the reader's, which the doctrine\n * states, so naming the reader's here would contradict the rule.\n */\nexport const AI_BUILD_PLAN_TOOL: AiTool = {\n name: 'submit_build_plan',\n description:\n 'Submit the build plan: what the site already has that this job reuses, what it creates and why, and the screens it builds from them.',\n strict: true,\n inputSchema: {\n type: 'object',\n additionalProperties: false,\n required: ['reuse', 'create', 'screens'],\n properties: {\n reuse: {\n type: 'array',\n description: 'Records the site already has that this plan uses, by inventory id.',\n items: {\n type: 'object',\n additionalProperties: false,\n required: ['kind', 'id', 'purpose'],\n properties: {\n kind: { type: 'string', enum: [...AI_BUILD_PLAN_REUSE_KINDS] },\n id: string('The id from the site inventory.'),\n purpose: string(`What the plan uses it for, in a few words, ${atMost()}.`),\n },\n },\n },\n create: {\n type: 'array',\n description:\n 'What the plan creates. Creating is the exception: each entry says why nothing listed will do.',\n items: {\n type: 'object',\n additionalProperties: false,\n required: ['kind', 'name', 'why', 'duplicateOf', 'fields'],\n properties: {\n kind: { type: 'string', enum: [...AI_BUILD_PLAN_CREATE_KINDS] },\n name: string(\n `Unique within the plan, ${atMost()}; other entries refer to it as new:<name>.`,\n ),\n why: string(`Why nothing the site already has will do, in one sentence of ${atMost()}.`),\n duplicateOf: nullableString(\n 'The inventory id this starts from as a duplicate, or null.',\n ),\n fields: strings(\n `A component: its props as name:type, an icon as name:icon. A layout: the regions it has, each one of header, nav, sidebar, main, footer — list every region the finished layout has, not only the ones it gains. A form or dataset: its field names. A theme change: the palette paths it adds or changes. Each ${atMost()}.`,\n ),\n },\n },\n },\n screens: {\n type: 'array',\n description: 'The screens the job builds, each a new draft.',\n items: {\n type: 'object',\n additionalProperties: false,\n required: [\n 'title',\n 'slug',\n 'layout',\n 'template',\n 'duplicateOf',\n 'nav',\n 'seoTitle',\n 'seoDescription',\n 'sections',\n ],\n properties: {\n title: string(`The screen name, ${atMost()}.`),\n slug: string(\n `The path, ${atMost()}: lowercase words joined by hyphens, e.g. /services/roof-repair.`,\n ),\n layout: nullableString(\n 'The layout it renders inside: an inventory id or new:<name>.',\n ),\n template: nullableString(\n 'The template it applies (inventory id or new:<name>), or null for a one-off page.',\n ),\n duplicateOf: nullableString(\n 'The inventory screen it starts from as a duplicate, or null.',\n ),\n nav: {\n type: 'boolean',\n description: 'Whether the site navigation gains an entry for it.',\n },\n seoTitle: string('The search-result title.'),\n seoDescription: string('The search-result description.'),\n sections: {\n type: 'array',\n description: 'The sections, top to bottom.',\n items: {\n type: 'object',\n additionalProperties: false,\n required: ['name', 'uses', 'items'],\n properties: {\n name: string(`A few words, ${atMost()}: hero, services grid, contact form.`),\n uses: strings('What the section places: inventory ids or new:<name>.'),\n items: {\n type: 'integer',\n description: 'How many repeated items it shows; 0 when none.',\n },\n },\n },\n },\n },\n },\n },\n },\n },\n}\n\nexport type AiBuildPlanParse =\n | { ok: true; plan: AiBuildPlan; repairs: string[] }\n | { ok: false; error: string }\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\nclass PlanShapeError extends Error {}\n\n/**\n * Read a plan out of a tool call, or name what makes it unreadable.\n *\n * Structure is refused — a missing list, an unknown kind, more screens than\n * a job holds, two creations under one name — because a plan that reads\n * differently from what the model meant is worse than a re-ask. Length is\n * repaired: copy past a ceiling is cut and the cut is listed.\n */\nexport function parseAiBuildPlan(input: unknown): AiBuildPlanParse {\n const repairs: string[] = []\n const text = (\n value: unknown,\n path: string,\n limit: number = AI_BUILD_PLAN_LIMITS.text,\n ): string => {\n if (typeof value !== 'string') throw new PlanShapeError(`${path} is not text`)\n const trimmed = value.trim()\n if (trimmed.length > limit) {\n repairs.push(`${path} was over ${limit} characters; truncated`)\n // On the last word break inside the ceiling, not through a word: a\n // rationale is read by the member confirming the plan, and half a word\n // reads as a broken answer rather than a long one (AGL-3022). A run of\n // `limit` characters with no break in it still has to be cut where the\n // ceiling falls.\n const cut = trimmed.slice(0, limit)\n const brk = cut.search(/\\s\\S*$/)\n return (brk > 0 ? cut.slice(0, brk) : cut).trimEnd()\n }\n return trimmed\n }\n const nullable = (value: unknown, path: string): string | null =>\n value === null || value === undefined || value === '' ? null : text(value, path) || null\n const list = (value: unknown, path: string, limit: number): unknown[] => {\n if (!Array.isArray(value)) throw new PlanShapeError(`${path} is not a list`)\n if (value.length > limit) {\n throw new PlanShapeError(`${path} lists ${value.length}; one plan holds at most ${limit}`)\n }\n return value\n }\n const record = (value: unknown, path: string): Record<string, unknown> => {\n if (!isRecord(value)) throw new PlanShapeError(`${path} is not an object`)\n return value\n }\n const oneOf = <T extends string>(value: unknown, allowed: readonly T[], path: string): T => {\n if (typeof value !== 'string' || !(allowed as readonly string[]).includes(value)) {\n throw new PlanShapeError(`${path} is not one of ${allowed.join(', ')}`)\n }\n return value as T\n }\n\n try {\n const root = record(input, 'plan')\n const reuse = list(root['reuse'], 'reuse', AI_BUILD_PLAN_LIMITS.reuse).map((raw, index) => {\n const entry = record(raw, `reuse[${index}]`)\n const id = text(entry['id'], `reuse[${index}].id`)\n if (!id) throw new PlanShapeError(`reuse[${index}].id is empty`)\n return {\n kind: oneOf(entry['kind'], AI_BUILD_PLAN_REUSE_KINDS, `reuse[${index}].kind`),\n id,\n purpose: text(entry['purpose'], `reuse[${index}].purpose`),\n }\n })\n const names = new Set<string>()\n const create = list(root['create'], 'create', AI_BUILD_PLAN_LIMITS.create).map(\n (raw, index) => {\n const entry = record(raw, `create[${index}]`)\n const name = text(entry['name'], `create[${index}].name`)\n if (!name) throw new PlanShapeError(`create[${index}].name is empty`)\n const key = name.toLowerCase()\n if (names.has(key)) {\n throw new PlanShapeError(`create[${index}].name \"${name}\" is used twice`)\n }\n names.add(key)\n return {\n kind: oneOf(entry['kind'], AI_BUILD_PLAN_CREATE_KINDS, `create[${index}].kind`),\n name,\n why: text(entry['why'], `create[${index}].why`),\n duplicateOf: nullable(entry['duplicateOf'], `create[${index}].duplicateOf`),\n fields: list(entry['fields'] ?? [], `create[${index}].fields`, AI_BUILD_PLAN_LIMITS.fields)\n .map((field, fieldIndex) => text(field, `create[${index}].fields[${fieldIndex}]`))\n .filter(Boolean),\n }\n },\n )\n const screens = list(root['screens'], 'screens', AI_BUILD_PLAN_LIMITS.screens).map(\n (raw, index) => {\n const entry = record(raw, `screens[${index}]`)\n if (typeof entry['nav'] !== 'boolean') {\n throw new PlanShapeError(`screens[${index}].nav is not true or false`)\n }\n return {\n title: text(entry['title'], `screens[${index}].title`),\n slug: text(entry['slug'], `screens[${index}].slug`),\n layout: nullable(entry['layout'], `screens[${index}].layout`),\n template: nullable(entry['template'], `screens[${index}].template`),\n duplicateOf: nullable(entry['duplicateOf'], `screens[${index}].duplicateOf`),\n nav: entry['nav'],\n seoTitle: text(entry['seoTitle'], `screens[${index}].seoTitle`),\n seoDescription: text(\n entry['seoDescription'],\n `screens[${index}].seoDescription`,\n AI_BUILD_PLAN_LIMITS.seoDescription,\n ),\n sections: list(\n entry['sections'],\n `screens[${index}].sections`,\n AI_BUILD_PLAN_LIMITS.sections,\n ).map((rawSection, sectionIndex) => {\n const path = `screens[${index}].sections[${sectionIndex}]`\n const section = record(rawSection, path)\n const items = Number(section['items'] ?? 0)\n if (!Number.isInteger(items) || items < 0 || items > AI_BUILD_PLAN_LIMITS.items) {\n throw new PlanShapeError(`${path}.items is not a count`)\n }\n return {\n name: text(section['name'], `${path}.name`),\n uses: list(section['uses'] ?? [], `${path}.uses`, AI_BUILD_PLAN_LIMITS.uses)\n .map((ref, refIndex) => text(ref, `${path}.uses[${refIndex}]`))\n .filter(Boolean),\n items,\n }\n }),\n }\n },\n )\n return { ok: true, plan: { reuse, create, screens }, repairs }\n } catch (error) {\n if (error instanceof PlanShapeError) return { ok: false, error: error.message }\n throw error\n }\n}\n\n/** Whether a reference names something the plan creates. */\nexport function isAiPlanNewRef(ref: string | null | undefined): ref is string {\n return typeof ref === 'string' && ref.startsWith(AI_PLAN_NEW_REF_PREFIX)\n}\n\n/** The creation a `new:<name>` reference names, case-insensitively. */\nexport function aiPlanCreateFor(\n plan: AiBuildPlan,\n ref: string,\n): AiBuildPlanCreate | undefined {\n if (!isAiPlanNewRef(ref)) return undefined\n const name = ref.slice(AI_PLAN_NEW_REF_PREFIX.length).trim().toLowerCase()\n return plan.create.find((entry) => entry.name.toLowerCase() === name)\n}\n\n/**\n * A `new:<name>` reference that names no entry of the plan's create list: a\n * screen's own layout or template, or what one of its sections places, with\n * that section's index.\n */\nexport type AiPlanUndeclaredRef = {\n /** The name the reference gives the creation, as written after `new:`. */\n name: string\n /** Where the plan writes it: `screens[0].template`, `screens[0].sections[4].uses[0]`. */\n path: string\n /** The screen that writes it, by index. */\n screenIndex: number\n} & ({ field: 'layout' | 'template'; sectionIndex: null } | { field: 'uses'; sectionIndex: number })\n\n/**\n * Every `new:<name>` reference the plan's create list does not carry, in plan\n * order: each screen's layout, then its template, then what its sections\n * place. A plan that places a creation it never declares has nothing to build\n * it from, so the plan rules refuse each (AGL-3040), and the page and\n * scaffold doors still name one in a plan confirmed before they did.\n */\nexport function aiPlanUndeclaredRefs(plan: AiBuildPlan): AiPlanUndeclaredRef[] {\n const found: AiPlanUndeclaredRef[] = []\n const undeclared = (ref: string | null): ref is string =>\n isAiPlanNewRef(ref) && !aiPlanCreateFor(plan, ref)\n const nameOf = (ref: string) => ref.slice(AI_PLAN_NEW_REF_PREFIX.length).trim()\n plan.screens.forEach((screen, screenIndex) => {\n for (const field of ['layout', 'template'] as const) {\n const ref = screen[field]\n if (!undeclared(ref)) continue\n found.push({\n name: nameOf(ref),\n path: `screens[${screenIndex}].${field}`,\n screenIndex,\n field,\n sectionIndex: null,\n })\n }\n screen.sections.forEach((section, sectionIndex) => {\n section.uses.forEach((ref, useIndex) => {\n if (!undeclared(ref)) return\n found.push({\n name: nameOf(ref),\n path: `screens[${screenIndex}].sections[${sectionIndex}].uses[${useIndex}]`,\n screenIndex,\n field: 'uses',\n sectionIndex,\n })\n })\n })\n })\n return found\n}\n"],"names":["AI_PLAN_NEW_REF_PREFIX","AI_BUILD_PLAN_REUSE_KINDS","AI_BUILD_PLAN_CREATION_NOUNS","component","noun","where","form","layout","template","dataset","email","AI_BUILD_PLAN_CREATE_KINDS","AI_BUILD_PLAN_LIMITS","reuse","create","screens","sections","uses","fields","text","seoDescription","items","string","description","type","nullableString","anyOf","strings","atMost","limit","AI_BUILD_PLAN_TOOL","name","strict","inputSchema","additionalProperties","required","properties","kind","enum","id","purpose","why","duplicateOf","title","slug","nav","seoTitle","isRecord","value","Array","isArray","PlanShapeError","Error","parseAiBuildPlan","input","repairs","path","trimmed","trim","length","push","cut","slice","brk","search","trimEnd","nullable","undefined","list","record","oneOf","allowed","includes","join","root","map","raw","index","entry","names","Set","key","toLowerCase","has","add","field","fieldIndex","filter","Boolean","rawSection","sectionIndex","section","Number","isInteger","ref","refIndex","ok","plan","error","message","isAiPlanNewRef","startsWith","aiPlanCreateFor","find","aiPlanUndeclaredRefs","found","undeclared","nameOf","forEach","screen","screenIndex","useIndex"],"mappings":"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"}
@@ -62,7 +62,7 @@ export const AI_PRODUCT_CATEGORIES_MAX_TOKENS = 2000;
62
62
  'Tags are words or short phrases a shopper filters by.',
63
63
  'Choose categories only from the list you are given, by id, and none when none fits.',
64
64
  'Give one name per option the product has, in order, keeping a name that is already clear.',
65
- 'When a photo is attached, use only what it plainly shows, and prefer the product’s own text where the two differ.',
65
+ 'When a photo is attached, use only what it plainly shows, and prefer the product’s own text where the two differ. What it shows — a color, a finish, a shape, a part — is a fact you have: write it, rather than marking it in brackets.',
66
66
  'Write in the language of the product’s name and text.'
67
67
  ].map((rule)=>`- ${rule}`).join('\n'),
68
68
  cacheBreakpoint: true
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/runtime/ai-products-generation.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n AI_CATALOG_OPTION_VALUES_MAX,\n AI_PRODUCT_CATEGORY_LIST_MAX,\n aiProductMerchantWords,\n type AiCatalogProduct,\n type AiCategoriesProposal,\n type AiProductCategory,\n type AiProductCopy,\n type AiProductFacts,\n} from '../model/ai-products'\nimport type { AiStepKind } from '../providers/catalog'\nimport type { AiImagePart, AiMessagePart, AiProvider } from '../providers/contract'\nimport { AI_ROUTING_TABLE, type AiPluginSettings } from '../providers/routing'\nimport {\n AI_CATALOG_TOOL,\n AI_CATALOG_TOOL_NAME,\n AI_CATEGORIES_TOOL,\n AI_CATEGORIES_TOOL_NAME,\n AI_PRODUCT_COPY_TOOL,\n AI_PRODUCT_COPY_TOOL_NAME,\n checkAiCatalog,\n checkAiCategories,\n checkAiProductCopy,\n} from '../tools/ai-products-tool'\nimport { runValidatedGeneration, type AiValidatedGeneration } from './ai-doctrine'\nimport type { AiSystemBlock } from './ai-runtime'\n\n/**\n * COMMERCE BY AI (AGL-2916): the three generations a `products` job makes, each\n * through the building doctrine's generation call (`runValidatedGeneration`):\n * the doctrine's cached block, which carries the acceptable-use rules, then the\n * generation's own rules, cached after it, and one strict tool. The store's\n * name, a product's facts, its categories, its photo and the brief ride in the\n * user turn, so no byte of a store sits inside the cached prefix, and no site\n * inventory is sent: product copy is written from the product.\n *\n * Every answer is held by a check in `tools/ai-products-tool.ts`, which is\n * where storefront copy safety is enforced — no health, financial or legal\n * claim, no certification or endorsement the merchant did not state, and no\n * price. The rules below say the same, so the model is never refused for a\n * rule it was not told.\n */\n\n/** The step kind every generation of a `products` job is routed as. */\nexport const AI_PRODUCTS_STEP: AiStepKind = 'job.products'\n\n/**\n * Each generation's answer ceiling. A catalog is the largest answer the step\n * asks for, so it takes the routing table's; one product's copy and a set of\n * categories with discounts are far smaller, and asking them for less keeps a\n * pass's least time short enough for a bulk job to run several a beat.\n * `ai-job-products-step.spec.ts` measures the largest answer each tool accepts\n * against its ceiling.\n */\nexport const AI_CATALOG_MAX_TOKENS = AI_ROUTING_TABLE[AI_PRODUCTS_STEP].maxTokens\nexport const AI_PRODUCT_COPY_MAX_TOKENS = 1_500\nexport const AI_PRODUCT_CATEGORIES_MAX_TOKENS = 2_000\n\n/**\n * Rules every storefront generation states, because its check refuses an\n * answer for each of them.\n */\nconst STOREFRONT_RULES = [\n 'Never write a price, a discount, a shipping promise or a stock level: the store owner sets those.',\n 'Never say a product cures, treats, heals or prevents anything, and never promise a health, financial or legal result.',\n 'Never claim a certification, an award, a license, an endorsement or a best seller that the owner’s own words do not state.',\n 'Write plain text: no markup, no formatting marks, no emoji, no links.',\n]\n\n/** The rules for one product's copy. Byte-identical on every request. */\nexport const AI_PRODUCT_COPY_INSTRUCTIONS: AiSystemBlock[] = [\n {\n text:\n 'You write the storefront copy for one product of an online store: its description, its search listing, its tags, the store categories it belongs in and clear names for its options.\\n\\n' +\n `Answer by calling ${AI_PRODUCT_COPY_TOOL_NAME} exactly once. A reply in prose cannot be used.\\n\\n` +\n 'Rules:\\n' +\n [\n 'Write only from what you are given: the product’s name, its current text, its tags, its options and its photo. Never invent a material, a size, a quantity, an origin or a feature. Where a shopper would need a fact you do not have, mark it in square brackets, like [material], in the description only.',\n 'Keep every fact the current text states, in your own words.',\n ...STOREFRONT_RULES,\n 'The description is short paragraphs a shopper reads. The search title names the product plainly; the search description is one or two sentences; neither marks a missing fact.',\n 'Tags are words or short phrases a shopper filters by.',\n 'Choose categories only from the list you are given, by id, and none when none fits.',\n 'Give one name per option the product has, in order, keeping a name that is already clear.',\n 'When a photo is attached, use only what it plainly shows, and prefer the product’s own text where the two differ.',\n 'Write in the language of the product’s name and text.',\n ]\n .map((rule) => `- ${rule}`)\n .join('\\n'),\n cacheBreakpoint: true,\n },\n]\n\n/** The rules for a store's first products. Byte-identical on every request. */\nexport const AI_CATALOG_INSTRUCTIONS: AiSystemBlock[] = [\n {\n text:\n 'You propose the first products of an online store from its owner’s brief. Each is a draft the owner completes: a name, a type, a description, tags, options, a search listing and what its photo should show.\\n\\n' +\n `Answer by calling ${AI_CATALOG_TOOL_NAME} exactly once. A reply in prose cannot be used.\\n\\n` +\n 'Rules:\\n' +\n [\n 'Propose 6 to 12 products that fit the brief, unless the brief says how many.',\n 'Write only from the brief. Never invent a material, a size, an origin or a feature it does not give; where a shopper would need a fact the brief does not give, mark it in square brackets, like [material], in the description only.',\n ...STOREFRONT_RULES,\n `Options are only the choices a shopper makes, such as a size or a color, with at most ${AI_CATALOG_OPTION_VALUES_MAX} values each; a product with no choice has none.`,\n 'The photo says in one sentence what a photo of the product should show, so the owner can take or pick it. Never a link.',\n 'The search title names the product plainly, and neither it nor the search description marks a missing fact.',\n 'Write in the language of the brief.',\n ]\n .map((rule) => `- ${rule}`)\n .join('\\n'),\n cacheBreakpoint: true,\n },\n]\n\n/** The rules for categories and discounts. Byte-identical on every request. */\nexport const AI_CATEGORIES_INSTRUCTIONS: AiSystemBlock[] = [\n {\n text:\n 'You propose store categories and a first set of discounts from an online store owner’s brief. The owner creates the ones they accept, and every discount starts switched off.\\n\\n' +\n `Answer by calling ${AI_CATEGORIES_TOOL_NAME} exactly once. A reply in prose cannot be used.\\n\\n` +\n 'Rules:\\n' +\n [\n 'Categories group the store’s products the way a shopper browses them. Never repeat a category the store already has.',\n 'A discount is a percentage off, an amount off in dollars, or free shipping, with an optional minimum order, and a code a shopper types or no code when it applies on its own. Keep them few and modest, and in line with the brief.',\n 'Each category and each discount says in one sentence why it helps this store.',\n 'Never promise savings, earnings or results beyond the discount itself, and never make a health, financial or legal claim.',\n 'Write plain text: no markup, no emoji, no links.',\n 'Propose nothing the brief gives no reason for; an empty list is an answer.',\n 'Write in the language of the brief.',\n ]\n .map((rule) => `- ${rule}`)\n .join('\\n'),\n cacheBreakpoint: true,\n },\n]\n\nexport interface AiProductCopyPromptInput {\n /** The store's name, as its visitors see it. */\n store: string\n product: AiProductFacts\n categories: readonly AiProductCategory[]\n /** Whether a photo rides with the request. */\n photo: boolean\n}\n\n/** The user turn's text: the store, the product as it stands, and the categories to choose from. */\nexport function aiProductCopyPrompt(input: AiProductCopyPromptInput): string {\n const { product } = input\n const lines = [\n `Store: ${input.store || 'untitled store'}`,\n `Product: ${product.name}`,\n `Type: ${product.type}`,\n 'Current text:',\n product.text.trim() || '(none)',\n `Current tags: ${product.tags.length ? product.tags.join(', ') : '(none)'}`,\n ]\n if (product.options.length) {\n lines.push('Options:', ...product.options.map((option) => `- ${option.name}: ${option.values.join(', ')}`))\n } else {\n lines.push('Options: (none)')\n }\n if (product.seoTitle || product.seoDescription) {\n lines.push(\n `Current search title: ${product.seoTitle || '(none)'}`,\n `Current search description: ${product.seoDescription || '(none)'}`,\n )\n }\n const categories = input.categories.slice(0, AI_PRODUCT_CATEGORY_LIST_MAX)\n lines.push(\n categories.length ? 'Categories to choose from:' : 'Categories to choose from: (none)',\n ...categories.map((category) => `- ${category.id}: ${category.name}`),\n `Photo: ${input.photo ? 'attached' : 'none'}`,\n )\n return lines.join('\\n')\n}\n\ninterface AiProductsGenerationBase {\n settings?: AiPluginSettings\n model?: string\n maxTokens?: number\n signal?: AbortSignal\n provider?: AiProvider\n}\n\nexport interface GenerateAiProductCopyInput extends AiProductsGenerationBase, AiProductCopyPromptInput {\n /** The product's photo, when the model reads pictures and the photo was read. */\n image?: AiImagePart | null\n}\n\n/**\n * Propose one product's copy. It asks the routing table's `job.products` model\n * through the doctrine's generation call and resolves to `ok` with the copy,\n * `needs_input` when the answer could not be held to the rules after one\n * re-ask, or `refused`. It writes nothing.\n */\nexport function generateAiProductCopy(input: GenerateAiProductCopyInput): Promise<AiValidatedGeneration<AiProductCopy>> {\n const image = input.photo ? (input.image ?? null) : null\n const content: AiMessagePart[] = [\n ...(image ? [image] : []),\n { type: 'text', text: aiProductCopyPrompt({ ...input, photo: Boolean(image) }) },\n ]\n return runValidatedGeneration('product copy', {\n step: AI_PRODUCTS_STEP,\n ...(input.settings ? { settings: input.settings } : {}),\n ...(input.model ? { model: input.model } : {}),\n instructions: AI_PRODUCT_COPY_INSTRUCTIONS,\n messages: [{ role: 'user', content: image ? content : aiProductCopyPrompt({ ...input, photo: false }) }],\n tool: AI_PRODUCT_COPY_TOOL,\n maxTokens: input.maxTokens ?? AI_PRODUCT_COPY_MAX_TOKENS,\n ...thinkingOf(),\n ...(input.signal ? { signal: input.signal } : {}),\n ...(input.provider ? { provider: input.provider } : {}),\n check: (answer) =>\n checkAiProductCopy(answer, {\n categoryIds: input.categories.slice(0, AI_PRODUCT_CATEGORY_LIST_MAX).map((category) => category.id),\n optionCount: input.product.options.length,\n merchantWords: aiProductMerchantWords(input.product),\n }),\n })\n}\n\nexport interface GenerateAiCatalogInput extends AiProductsGenerationBase {\n store: string\n brief: string\n}\n\n/** The user turn for a catalog: the store and the brief. */\nexport function aiCatalogPrompt(input: Pick<GenerateAiCatalogInput, 'store' | 'brief'>): string {\n return [`Store: ${input.store || 'untitled store'}`, 'Brief:', input.brief.trim()].join('\\n')\n}\n\n/** Propose a store's first products from a brief. It writes nothing. */\nexport function generateAiCatalog(input: GenerateAiCatalogInput & { maxTokens: number }): Promise<AiValidatedGeneration<AiCatalogProduct[]>> {\n return runValidatedGeneration('catalog', {\n step: AI_PRODUCTS_STEP,\n ...(input.settings ? { settings: input.settings } : {}),\n ...(input.model ? { model: input.model } : {}),\n instructions: AI_CATALOG_INSTRUCTIONS,\n messages: [{ role: 'user', content: aiCatalogPrompt(input) }],\n tool: AI_CATALOG_TOOL,\n maxTokens: input.maxTokens,\n ...thinkingOf(),\n ...(input.signal ? { signal: input.signal } : {}),\n ...(input.provider ? { provider: input.provider } : {}),\n check: (answer) => checkAiCatalog(answer, { merchantWords: input.brief }),\n })\n}\n\nexport interface GenerateAiCategoriesInput extends AiProductsGenerationBase {\n store: string\n brief: string\n /** The names of the categories the store already has. */\n existingCategoryNames: readonly string[]\n}\n\n/** The user turn for categories and discounts: the store, what it has, and the brief. */\nexport function aiCategoriesPrompt(input: Pick<GenerateAiCategoriesInput, 'store' | 'brief' | 'existingCategoryNames'>): string {\n const existing = input.existingCategoryNames.slice(0, AI_PRODUCT_CATEGORY_LIST_MAX)\n return [\n `Store: ${input.store || 'untitled store'}`,\n `Categories the store already has: ${existing.length ? existing.join(', ') : '(none)'}`,\n 'Brief:',\n input.brief.trim(),\n ].join('\\n')\n}\n\n/** Propose categories and discounts from a brief. It writes nothing. */\nexport function generateAiCategories(\n input: GenerateAiCategoriesInput & { maxTokens: number },\n): Promise<AiValidatedGeneration<Pick<AiCategoriesProposal, 'categories' | 'discounts'>>> {\n return runValidatedGeneration('store categories and discounts', {\n step: AI_PRODUCTS_STEP,\n ...(input.settings ? { settings: input.settings } : {}),\n ...(input.model ? { model: input.model } : {}),\n instructions: AI_CATEGORIES_INSTRUCTIONS,\n messages: [{ role: 'user', content: aiCategoriesPrompt(input) }],\n tool: AI_CATEGORIES_TOOL,\n maxTokens: input.maxTokens,\n ...thinkingOf(),\n ...(input.signal ? { signal: input.signal } : {}),\n ...(input.provider ? { provider: input.provider } : {}),\n check: (answer) =>\n checkAiCategories(answer, {\n existingCategoryNames: input.existingCategoryNames,\n merchantWords: input.brief,\n }),\n })\n}\n\n/** The routing row's thinking and effort, sent only where the row names one. */\nfunction thinkingOf() {\n const row = AI_ROUTING_TABLE[AI_PRODUCTS_STEP]\n return {\n ...(row.thinking ? { thinking: row.thinking } : {}),\n ...(row.effort ? { effort: row.effort } : {}),\n }\n}\n"],"names":["AI_CATALOG_OPTION_VALUES_MAX","AI_PRODUCT_CATEGORY_LIST_MAX","aiProductMerchantWords","AI_ROUTING_TABLE","AI_CATALOG_TOOL","AI_CATALOG_TOOL_NAME","AI_CATEGORIES_TOOL","AI_CATEGORIES_TOOL_NAME","AI_PRODUCT_COPY_TOOL","AI_PRODUCT_COPY_TOOL_NAME","checkAiCatalog","checkAiCategories","checkAiProductCopy","runValidatedGeneration","AI_PRODUCTS_STEP","AI_CATALOG_MAX_TOKENS","maxTokens","AI_PRODUCT_COPY_MAX_TOKENS","AI_PRODUCT_CATEGORIES_MAX_TOKENS","STOREFRONT_RULES","AI_PRODUCT_COPY_INSTRUCTIONS","text","map","rule","join","cacheBreakpoint","AI_CATALOG_INSTRUCTIONS","AI_CATEGORIES_INSTRUCTIONS","aiProductCopyPrompt","input","product","lines","store","name","type","trim","tags","length","options","push","option","values","seoTitle","seoDescription","categories","slice","category","id","photo","generateAiProductCopy","image","content","Boolean","step","settings","model","instructions","messages","role","tool","thinkingOf","signal","provider","check","answer","categoryIds","optionCount","merchantWords","aiCatalogPrompt","brief","generateAiCatalog","aiCategoriesPrompt","existing","existingCategoryNames","generateAiCategories","row","thinking","effort"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/runtime/ai-products-generation.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n AI_CATALOG_OPTION_VALUES_MAX,\n AI_PRODUCT_CATEGORY_LIST_MAX,\n aiProductMerchantWords,\n type AiCatalogProduct,\n type AiCategoriesProposal,\n type AiProductCategory,\n type AiProductCopy,\n type AiProductFacts,\n} from '../model/ai-products'\nimport type { AiStepKind } from '../providers/catalog'\nimport type { AiImagePart, AiMessagePart, AiProvider } from '../providers/contract'\nimport { AI_ROUTING_TABLE, type AiPluginSettings } from '../providers/routing'\nimport {\n AI_CATALOG_TOOL,\n AI_CATALOG_TOOL_NAME,\n AI_CATEGORIES_TOOL,\n AI_CATEGORIES_TOOL_NAME,\n AI_PRODUCT_COPY_TOOL,\n AI_PRODUCT_COPY_TOOL_NAME,\n checkAiCatalog,\n checkAiCategories,\n checkAiProductCopy,\n} from '../tools/ai-products-tool'\nimport { runValidatedGeneration, type AiValidatedGeneration } from './ai-doctrine'\nimport type { AiSystemBlock } from './ai-runtime'\n\n/**\n * COMMERCE BY AI (AGL-2916): the three generations a `products` job makes, each\n * through the building doctrine's generation call (`runValidatedGeneration`):\n * the doctrine's cached block, which carries the acceptable-use rules, then the\n * generation's own rules, cached after it, and one strict tool. The store's\n * name, a product's facts, its categories, its photo and the brief ride in the\n * user turn, so no byte of a store sits inside the cached prefix, and no site\n * inventory is sent: product copy is written from the product.\n *\n * Every answer is held by a check in `tools/ai-products-tool.ts`, which is\n * where storefront copy safety is enforced — no health, financial or legal\n * claim, no certification or endorsement the merchant did not state, and no\n * price. The rules below say the same, so the model is never refused for a\n * rule it was not told.\n */\n\n/** The step kind every generation of a `products` job is routed as. */\nexport const AI_PRODUCTS_STEP: AiStepKind = 'job.products'\n\n/**\n * Each generation's answer ceiling. A catalog is the largest answer the step\n * asks for, so it takes the routing table's; one product's copy and a set of\n * categories with discounts are far smaller, and asking them for less keeps a\n * pass's least time short enough for a bulk job to run several a beat.\n * `ai-job-products-step.spec.ts` measures the largest answer each tool accepts\n * against its ceiling.\n */\nexport const AI_CATALOG_MAX_TOKENS = AI_ROUTING_TABLE[AI_PRODUCTS_STEP].maxTokens\nexport const AI_PRODUCT_COPY_MAX_TOKENS = 1_500\nexport const AI_PRODUCT_CATEGORIES_MAX_TOKENS = 2_000\n\n/**\n * Rules every storefront generation states, because its check refuses an\n * answer for each of them.\n */\nconst STOREFRONT_RULES = [\n 'Never write a price, a discount, a shipping promise or a stock level: the store owner sets those.',\n 'Never say a product cures, treats, heals or prevents anything, and never promise a health, financial or legal result.',\n 'Never claim a certification, an award, a license, an endorsement or a best seller that the owner’s own words do not state.',\n 'Write plain text: no markup, no formatting marks, no emoji, no links.',\n]\n\n/** The rules for one product's copy. Byte-identical on every request. */\nexport const AI_PRODUCT_COPY_INSTRUCTIONS: AiSystemBlock[] = [\n {\n text:\n 'You write the storefront copy for one product of an online store: its description, its search listing, its tags, the store categories it belongs in and clear names for its options.\\n\\n' +\n `Answer by calling ${AI_PRODUCT_COPY_TOOL_NAME} exactly once. A reply in prose cannot be used.\\n\\n` +\n 'Rules:\\n' +\n [\n 'Write only from what you are given: the product’s name, its current text, its tags, its options and its photo. Never invent a material, a size, a quantity, an origin or a feature. Where a shopper would need a fact you do not have, mark it in square brackets, like [material], in the description only.',\n 'Keep every fact the current text states, in your own words.',\n ...STOREFRONT_RULES,\n 'The description is short paragraphs a shopper reads. The search title names the product plainly; the search description is one or two sentences; neither marks a missing fact.',\n 'Tags are words or short phrases a shopper filters by.',\n 'Choose categories only from the list you are given, by id, and none when none fits.',\n 'Give one name per option the product has, in order, keeping a name that is already clear.',\n 'When a photo is attached, use only what it plainly shows, and prefer the product’s own text where the two differ. What it shows — a color, a finish, a shape, a part — is a fact you have: write it, rather than marking it in brackets.',\n 'Write in the language of the product’s name and text.',\n ]\n .map((rule) => `- ${rule}`)\n .join('\\n'),\n cacheBreakpoint: true,\n },\n]\n\n/** The rules for a store's first products. Byte-identical on every request. */\nexport const AI_CATALOG_INSTRUCTIONS: AiSystemBlock[] = [\n {\n text:\n 'You propose the first products of an online store from its owner’s brief. Each is a draft the owner completes: a name, a type, a description, tags, options, a search listing and what its photo should show.\\n\\n' +\n `Answer by calling ${AI_CATALOG_TOOL_NAME} exactly once. A reply in prose cannot be used.\\n\\n` +\n 'Rules:\\n' +\n [\n 'Propose 6 to 12 products that fit the brief, unless the brief says how many.',\n 'Write only from the brief. Never invent a material, a size, an origin or a feature it does not give; where a shopper would need a fact the brief does not give, mark it in square brackets, like [material], in the description only.',\n ...STOREFRONT_RULES,\n `Options are only the choices a shopper makes, such as a size or a color, with at most ${AI_CATALOG_OPTION_VALUES_MAX} values each; a product with no choice has none.`,\n 'The photo says in one sentence what a photo of the product should show, so the owner can take or pick it. Never a link.',\n 'The search title names the product plainly, and neither it nor the search description marks a missing fact.',\n 'Write in the language of the brief.',\n ]\n .map((rule) => `- ${rule}`)\n .join('\\n'),\n cacheBreakpoint: true,\n },\n]\n\n/** The rules for categories and discounts. Byte-identical on every request. */\nexport const AI_CATEGORIES_INSTRUCTIONS: AiSystemBlock[] = [\n {\n text:\n 'You propose store categories and a first set of discounts from an online store owner’s brief. The owner creates the ones they accept, and every discount starts switched off.\\n\\n' +\n `Answer by calling ${AI_CATEGORIES_TOOL_NAME} exactly once. A reply in prose cannot be used.\\n\\n` +\n 'Rules:\\n' +\n [\n 'Categories group the store’s products the way a shopper browses them. Never repeat a category the store already has.',\n 'A discount is a percentage off, an amount off in dollars, or free shipping, with an optional minimum order, and a code a shopper types or no code when it applies on its own. Keep them few and modest, and in line with the brief.',\n 'Each category and each discount says in one sentence why it helps this store.',\n 'Never promise savings, earnings or results beyond the discount itself, and never make a health, financial or legal claim.',\n 'Write plain text: no markup, no emoji, no links.',\n 'Propose nothing the brief gives no reason for; an empty list is an answer.',\n 'Write in the language of the brief.',\n ]\n .map((rule) => `- ${rule}`)\n .join('\\n'),\n cacheBreakpoint: true,\n },\n]\n\nexport interface AiProductCopyPromptInput {\n /** The store's name, as its visitors see it. */\n store: string\n product: AiProductFacts\n categories: readonly AiProductCategory[]\n /** Whether a photo rides with the request. */\n photo: boolean\n}\n\n/** The user turn's text: the store, the product as it stands, and the categories to choose from. */\nexport function aiProductCopyPrompt(input: AiProductCopyPromptInput): string {\n const { product } = input\n const lines = [\n `Store: ${input.store || 'untitled store'}`,\n `Product: ${product.name}`,\n `Type: ${product.type}`,\n 'Current text:',\n product.text.trim() || '(none)',\n `Current tags: ${product.tags.length ? product.tags.join(', ') : '(none)'}`,\n ]\n if (product.options.length) {\n lines.push('Options:', ...product.options.map((option) => `- ${option.name}: ${option.values.join(', ')}`))\n } else {\n lines.push('Options: (none)')\n }\n if (product.seoTitle || product.seoDescription) {\n lines.push(\n `Current search title: ${product.seoTitle || '(none)'}`,\n `Current search description: ${product.seoDescription || '(none)'}`,\n )\n }\n const categories = input.categories.slice(0, AI_PRODUCT_CATEGORY_LIST_MAX)\n lines.push(\n categories.length ? 'Categories to choose from:' : 'Categories to choose from: (none)',\n ...categories.map((category) => `- ${category.id}: ${category.name}`),\n `Photo: ${input.photo ? 'attached' : 'none'}`,\n )\n return lines.join('\\n')\n}\n\ninterface AiProductsGenerationBase {\n settings?: AiPluginSettings\n model?: string\n maxTokens?: number\n signal?: AbortSignal\n provider?: AiProvider\n}\n\nexport interface GenerateAiProductCopyInput extends AiProductsGenerationBase, AiProductCopyPromptInput {\n /** The product's photo, when the model reads pictures and the photo was read. */\n image?: AiImagePart | null\n}\n\n/**\n * Propose one product's copy. It asks the routing table's `job.products` model\n * through the doctrine's generation call and resolves to `ok` with the copy,\n * `needs_input` when the answer could not be held to the rules after one\n * re-ask, or `refused`. It writes nothing.\n */\nexport function generateAiProductCopy(input: GenerateAiProductCopyInput): Promise<AiValidatedGeneration<AiProductCopy>> {\n const image = input.photo ? (input.image ?? null) : null\n const content: AiMessagePart[] = [\n ...(image ? [image] : []),\n { type: 'text', text: aiProductCopyPrompt({ ...input, photo: Boolean(image) }) },\n ]\n return runValidatedGeneration('product copy', {\n step: AI_PRODUCTS_STEP,\n ...(input.settings ? { settings: input.settings } : {}),\n ...(input.model ? { model: input.model } : {}),\n instructions: AI_PRODUCT_COPY_INSTRUCTIONS,\n messages: [{ role: 'user', content: image ? content : aiProductCopyPrompt({ ...input, photo: false }) }],\n tool: AI_PRODUCT_COPY_TOOL,\n maxTokens: input.maxTokens ?? AI_PRODUCT_COPY_MAX_TOKENS,\n ...thinkingOf(),\n ...(input.signal ? { signal: input.signal } : {}),\n ...(input.provider ? { provider: input.provider } : {}),\n check: (answer) =>\n checkAiProductCopy(answer, {\n categoryIds: input.categories.slice(0, AI_PRODUCT_CATEGORY_LIST_MAX).map((category) => category.id),\n optionCount: input.product.options.length,\n merchantWords: aiProductMerchantWords(input.product),\n }),\n })\n}\n\nexport interface GenerateAiCatalogInput extends AiProductsGenerationBase {\n store: string\n brief: string\n}\n\n/** The user turn for a catalog: the store and the brief. */\nexport function aiCatalogPrompt(input: Pick<GenerateAiCatalogInput, 'store' | 'brief'>): string {\n return [`Store: ${input.store || 'untitled store'}`, 'Brief:', input.brief.trim()].join('\\n')\n}\n\n/** Propose a store's first products from a brief. It writes nothing. */\nexport function generateAiCatalog(input: GenerateAiCatalogInput & { maxTokens: number }): Promise<AiValidatedGeneration<AiCatalogProduct[]>> {\n return runValidatedGeneration('catalog', {\n step: AI_PRODUCTS_STEP,\n ...(input.settings ? { settings: input.settings } : {}),\n ...(input.model ? { model: input.model } : {}),\n instructions: AI_CATALOG_INSTRUCTIONS,\n messages: [{ role: 'user', content: aiCatalogPrompt(input) }],\n tool: AI_CATALOG_TOOL,\n maxTokens: input.maxTokens,\n ...thinkingOf(),\n ...(input.signal ? { signal: input.signal } : {}),\n ...(input.provider ? { provider: input.provider } : {}),\n check: (answer) => checkAiCatalog(answer, { merchantWords: input.brief }),\n })\n}\n\nexport interface GenerateAiCategoriesInput extends AiProductsGenerationBase {\n store: string\n brief: string\n /** The names of the categories the store already has. */\n existingCategoryNames: readonly string[]\n}\n\n/** The user turn for categories and discounts: the store, what it has, and the brief. */\nexport function aiCategoriesPrompt(input: Pick<GenerateAiCategoriesInput, 'store' | 'brief' | 'existingCategoryNames'>): string {\n const existing = input.existingCategoryNames.slice(0, AI_PRODUCT_CATEGORY_LIST_MAX)\n return [\n `Store: ${input.store || 'untitled store'}`,\n `Categories the store already has: ${existing.length ? existing.join(', ') : '(none)'}`,\n 'Brief:',\n input.brief.trim(),\n ].join('\\n')\n}\n\n/** Propose categories and discounts from a brief. It writes nothing. */\nexport function generateAiCategories(\n input: GenerateAiCategoriesInput & { maxTokens: number },\n): Promise<AiValidatedGeneration<Pick<AiCategoriesProposal, 'categories' | 'discounts'>>> {\n return runValidatedGeneration('store categories and discounts', {\n step: AI_PRODUCTS_STEP,\n ...(input.settings ? { settings: input.settings } : {}),\n ...(input.model ? { model: input.model } : {}),\n instructions: AI_CATEGORIES_INSTRUCTIONS,\n messages: [{ role: 'user', content: aiCategoriesPrompt(input) }],\n tool: AI_CATEGORIES_TOOL,\n maxTokens: input.maxTokens,\n ...thinkingOf(),\n ...(input.signal ? { signal: input.signal } : {}),\n ...(input.provider ? { provider: input.provider } : {}),\n check: (answer) =>\n checkAiCategories(answer, {\n existingCategoryNames: input.existingCategoryNames,\n merchantWords: input.brief,\n }),\n })\n}\n\n/** The routing row's thinking and effort, sent only where the row names one. */\nfunction thinkingOf() {\n const row = AI_ROUTING_TABLE[AI_PRODUCTS_STEP]\n return {\n ...(row.thinking ? { thinking: row.thinking } : {}),\n ...(row.effort ? { effort: row.effort } : {}),\n }\n}\n"],"names":["AI_CATALOG_OPTION_VALUES_MAX","AI_PRODUCT_CATEGORY_LIST_MAX","aiProductMerchantWords","AI_ROUTING_TABLE","AI_CATALOG_TOOL","AI_CATALOG_TOOL_NAME","AI_CATEGORIES_TOOL","AI_CATEGORIES_TOOL_NAME","AI_PRODUCT_COPY_TOOL","AI_PRODUCT_COPY_TOOL_NAME","checkAiCatalog","checkAiCategories","checkAiProductCopy","runValidatedGeneration","AI_PRODUCTS_STEP","AI_CATALOG_MAX_TOKENS","maxTokens","AI_PRODUCT_COPY_MAX_TOKENS","AI_PRODUCT_CATEGORIES_MAX_TOKENS","STOREFRONT_RULES","AI_PRODUCT_COPY_INSTRUCTIONS","text","map","rule","join","cacheBreakpoint","AI_CATALOG_INSTRUCTIONS","AI_CATEGORIES_INSTRUCTIONS","aiProductCopyPrompt","input","product","lines","store","name","type","trim","tags","length","options","push","option","values","seoTitle","seoDescription","categories","slice","category","id","photo","generateAiProductCopy","image","content","Boolean","step","settings","model","instructions","messages","role","tool","thinkingOf","signal","provider","check","answer","categoryIds","optionCount","merchantWords","aiCatalogPrompt","brief","generateAiCatalog","aiCategoriesPrompt","existing","existingCategoryNames","generateAiCategories","row","thinking","effort"],"mappings":"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"}
@@ -131,10 +131,32 @@ export interface OrgAiUsageByUserMonth {
131
131
  export declare function readOrgAiUsageByUser(firestore: FirebaseFirestore.Firestore, orgId: string, month: string): Promise<OrgAiUsageByUserMonth>;
132
132
  /**
133
133
  * One person's months in one org, newest first — the member card and the
134
- * staff page. Ordered on the document id, which IS the month key and so
135
- * needs no field every writer must remember.
134
+ * staff page. The document id IS the month key, so no field every writer must
135
+ * remember carries the order.
136
+ *
137
+ * ## Asked for by id, never ordered (AGL-3143 §13)
138
+ *
139
+ * This read `.orderBy(documentId(), 'desc').limit(n)`, which looks
140
+ * like the obvious way to say "newest first" and **500s in production**:
141
+ * Firestore indexes `__name__` ASCENDING automatically and descending NOT at
142
+ * all, so the query answers `FAILED_PRECONDITION: the query requires an
143
+ * index`. `GET /api/ai/usage?uid=…` returned 500 on every call and the member
144
+ * card read "Could not read this member's AI usage — a failed read, not zero
145
+ * usage", while the org-wide table beside it, which orders on a real field,
146
+ * was fine.
147
+ *
148
+ * ⛔ The fix is deliberately NOT the index the error offers. A promotion ships
149
+ * Vercel and nothing else — it deploys no rules and no indexes — so a fix that
150
+ * needs one would land green, ship, and stay broken until someone remembered a
151
+ * separate deploy. There is nothing to remember here.
152
+ *
153
+ * The whole key space is knowable without a query: `aiUsageMonthKeys` is the
154
+ * retention window and the TTL reaps everything older, so the thirteen ids are
155
+ * asked for directly and the ones that exist come back in the order they were
156
+ * asked. Bounded at thirteen point reads, and it cannot return a month outside
157
+ * retention, which is what the door promises its callers.
136
158
  */
137
- export declare function readUserAiUsageMonths(firestore: FirebaseFirestore.Firestore, orgId: string, uid: string, limit?: number): Promise<AiUsageByUserMonth[]>;
159
+ export declare function readUserAiUsageMonths(firestore: FirebaseFirestore.Firestore, orgId: string, uid: string, limit?: number, now?: Date): Promise<AiUsageByUserMonth[]>;
138
160
  export interface EraseUserAiUsageResult {
139
161
  /** Orgs whose `aiUsageByUser/{uid}` subtree was deleted by path. */
140
162
  orgs: number;
@@ -17,8 +17,8 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
17
17
  */ // Straight from the SDK, not the admin lib, for the reason `assist-usage.ts`
18
18
  // gives at its head: the statics need no app, and the admin lib would drag the
19
19
  // default-app initialization into every unit test that touches a counter.
20
- import { FieldPath, FieldValue } from "firebase-admin/firestore";
21
- import { AI_USAGE_BY_USER_COLLECTION, AI_USAGE_BY_USER_MONTHS_COLLECTION, AI_USAGE_BY_USER_RETENTION_MONTHS, aiUsageByUserExpiry, aiUsageByUserMonthFrom, aiUsageShare } from "../model/ai-usage-by-user.js";
20
+ import { FieldValue } from "firebase-admin/firestore";
21
+ import { AI_USAGE_BY_USER_COLLECTION, AI_USAGE_BY_USER_MONTHS_COLLECTION, AI_USAGE_BY_USER_RETENTION_MONTHS, aiUsageByUserExpiry, aiUsageByUserMonthFrom, aiUsageMonthKeys, aiUsageShare } from "../model/ai-usage-by-user.js";
22
22
  import { AI_USAGE_TOKENS_FIELD } from "../model/ai-tokens.js";
23
23
  import { assistCreditsFromUsd } from "@aglyn/aglyn/app-utils/assist-credits";
24
24
  /**
@@ -163,11 +163,35 @@ export function userAiUsageMonthRef(orgRef, uid, month) {
163
163
  }
164
164
  /**
165
165
  * One person's months in one org, newest first — the member card and the
166
- * staff page. Ordered on the document id, which IS the month key and so
167
- * needs no field every writer must remember.
168
- */ export async function readUserAiUsageMonths(firestore, orgId, uid, limit = AI_USAGE_BY_USER_RETENTION_MONTHS) {
169
- const snapshot = await firestore.collection('orgs').doc(orgId).collection(AI_USAGE_BY_USER_COLLECTION).doc(uid).collection(AI_USAGE_BY_USER_MONTHS_COLLECTION).orderBy(FieldPath.documentId(), 'desc').limit(limit).get();
170
- return snapshot.docs.map((doc)=>aiUsageByUserMonthFrom(doc.data(), uid, doc.id));
166
+ * staff page. The document id IS the month key, so no field every writer must
167
+ * remember carries the order.
168
+ *
169
+ * ## Asked for by id, never ordered (AGL-3143 §13)
170
+ *
171
+ * This read `.orderBy(documentId(), 'desc').limit(n)`, which looks
172
+ * like the obvious way to say "newest first" and **500s in production**:
173
+ * Firestore indexes `__name__` ASCENDING automatically and descending NOT at
174
+ * all, so the query answers `FAILED_PRECONDITION: the query requires an
175
+ * index`. `GET /api/ai/usage?uid=…` returned 500 on every call and the member
176
+ * card read "Could not read this member's AI usage — a failed read, not zero
177
+ * usage", while the org-wide table beside it, which orders on a real field,
178
+ * was fine.
179
+ *
180
+ * ⛔ The fix is deliberately NOT the index the error offers. A promotion ships
181
+ * Vercel and nothing else — it deploys no rules and no indexes — so a fix that
182
+ * needs one would land green, ship, and stay broken until someone remembered a
183
+ * separate deploy. There is nothing to remember here.
184
+ *
185
+ * The whole key space is knowable without a query: `aiUsageMonthKeys` is the
186
+ * retention window and the TTL reaps everything older, so the thirteen ids are
187
+ * asked for directly and the ones that exist come back in the order they were
188
+ * asked. Bounded at thirteen point reads, and it cannot return a month outside
189
+ * retention, which is what the door promises its callers.
190
+ */ export async function readUserAiUsageMonths(firestore, orgId, uid, limit = AI_USAGE_BY_USER_RETENTION_MONTHS, now = new Date()) {
191
+ const months = firestore.collection('orgs').doc(orgId).collection(AI_USAGE_BY_USER_COLLECTION).doc(uid).collection(AI_USAGE_BY_USER_MONTHS_COLLECTION);
192
+ const wanted = aiUsageMonthKeys(now);
193
+ const snapshots = await firestore.getAll(...wanted.map((month)=>months.doc(month)));
194
+ return snapshots.filter((doc)=>doc.exists).slice(0, Math.max(1, limit)).map((doc)=>aiUsageByUserMonthFrom(doc.data(), uid, doc.id));
171
195
  }
172
196
  /**
173
197
  * Remove a person's usage documents on account erasure.