@aglyn/plugins-ai 1.0.0-beta.288 → 1.0.0-beta.290
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.
- package/package.json +17 -17
- package/src/lib/activity/ai-activity-actions.d.ts +3 -2
- package/src/lib/activity/ai-activity-actions.js +3 -0
- package/src/lib/activity/ai-activity-actions.js.map +1 -1
- package/src/lib/components/ai-job-requests.d.ts +2 -0
- package/src/lib/components/ai-job-requests.js +2 -0
- package/src/lib/components/ai-job-requests.js.map +1 -1
- package/src/lib/components/ai-site-build-page.component.js +4 -0
- package/src/lib/components/ai-site-build-page.component.js.map +1 -1
- package/src/lib/components/ai-site-import-job.component.d.ts +6 -1
- package/src/lib/components/ai-site-import-job.component.js +124 -17
- package/src/lib/components/ai-site-import-job.component.js.map +1 -1
- package/src/lib/components/ai-site-import-start.component.d.ts +6 -0
- package/src/lib/components/ai-site-import-start.component.js +18 -4
- package/src/lib/components/ai-site-import-start.component.js.map +1 -1
- package/src/lib/components/ai-site-start-card.component.js +2 -1
- package/src/lib/components/ai-site-start-card.component.js.map +1 -1
- package/src/lib/jobs/ai-image-reads.d.ts +20 -2
- package/src/lib/jobs/ai-image-reads.js +47 -0
- package/src/lib/jobs/ai-image-reads.js.map +1 -1
- package/src/lib/jobs/ai-job-import-step.d.ts +53 -3
- package/src/lib/jobs/ai-job-import-step.js +386 -64
- package/src/lib/jobs/ai-job-import-step.js.map +1 -1
- package/src/lib/jobs/ai-job-layout-language.d.ts +33 -0
- package/src/lib/jobs/ai-job-layout-language.js +75 -1
- package/src/lib/jobs/ai-job-layout-language.js.map +1 -1
- package/src/lib/jobs/ai-job-layout-step.js +8 -3
- package/src/lib/jobs/ai-job-layout-step.js.map +1 -1
- package/src/lib/jobs/ai-job-page-language.d.ts +29 -0
- package/src/lib/jobs/ai-job-page-language.js +178 -2
- package/src/lib/jobs/ai-job-page-language.js.map +1 -1
- package/src/lib/jobs/ai-job-page-step.js +3 -0
- package/src/lib/jobs/ai-job-page-step.js.map +1 -1
- package/src/lib/jobs/ai-site-import-document.d.ts +117 -0
- package/src/lib/jobs/ai-site-import-document.js +710 -0
- package/src/lib/jobs/ai-site-import-document.js.map +1 -0
- package/src/lib/jobs/ai-site-import-look.d.ts +112 -0
- package/src/lib/jobs/ai-site-import-look.js +402 -0
- package/src/lib/jobs/ai-site-import-look.js.map +1 -0
- package/src/lib/jobs/ai-site-import-publish.d.ts +41 -0
- package/src/lib/jobs/ai-site-import-publish.js +92 -0
- package/src/lib/jobs/ai-site-import-publish.js.map +1 -0
- package/src/lib/jobs/ai-theme-brand-inputs.d.ts +27 -2
- package/src/lib/jobs/ai-theme-brand-inputs.js +37 -8
- package/src/lib/jobs/ai-theme-brand-inputs.js.map +1 -1
- package/src/lib/layout-language/ai-layout-frame.d.ts +17 -0
- package/src/lib/layout-language/ai-layout-frame.js +73 -2
- package/src/lib/layout-language/ai-layout-frame.js.map +1 -1
- package/src/lib/layout-language/ai-layout-language.d.ts +43 -0
- package/src/lib/layout-language/ai-layout-language.js +199 -0
- package/src/lib/layout-language/ai-layout-language.js.map +1 -1
- package/src/lib/model/ai-credit-estimate.d.ts +15 -0
- package/src/lib/model/ai-credit-estimate.js +14 -1
- package/src/lib/model/ai-credit-estimate.js.map +1 -1
- package/src/lib/model/ai-jobs.types.d.ts +13 -1
- package/src/lib/model/ai-jobs.types.js.map +1 -1
- package/src/lib/model/ai-site-import-view.d.ts +47 -3
- package/src/lib/model/ai-site-import-view.js +122 -20
- package/src/lib/model/ai-site-import-view.js.map +1 -1
- package/src/lib/model/ai-site-import.d.ts +232 -7
- package/src/lib/model/ai-site-import.js +365 -35
- package/src/lib/model/ai-site-import.js.map +1 -1
- package/src/lib/runtime/ai-doctrine-validators.d.ts +18 -0
- package/src/lib/runtime/ai-doctrine-validators.js +16 -5
- package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
- package/src/lib/runtime/ai-palette.generated.js +31 -0
- package/src/lib/runtime/ai-palette.generated.js.map +1 -1
- package/src/lib/server/ai-image-reads-route.js +24 -1
- package/src/lib/server/ai-image-reads-route.js.map +1 -1
- package/src/lib/server/ai-jobs-resume.js +7 -1
- package/src/lib/server/ai-jobs-resume.js.map +1 -1
- package/src/lib/site-import/fixtures/aglyn-com-recordings.d.ts +57 -0
- package/src/lib/site-import/fixtures/aglyn-com-recordings.js +172 -0
- package/src/lib/site-import/fixtures/aglyn-com-recordings.js.map +1 -0
- package/src/lib/site-import/site-import-crawl.d.ts +32 -7
- package/src/lib/site-import/site-import-crawl.js +133 -44
- package/src/lib/site-import/site-import-crawl.js.map +1 -1
- package/src/lib/site-import/site-import-edge-server.d.ts +31 -0
- package/src/lib/site-import/site-import-edge-server.js +31 -0
- package/src/lib/site-import/site-import-edge-server.js.map +1 -0
- package/src/lib/site-import/site-import-edge.d.ts +44 -0
- package/src/lib/site-import/site-import-edge.js +108 -0
- package/src/lib/site-import/site-import-edge.js.map +1 -0
- package/src/lib/site-import/site-import-html.d.ts +73 -3
- package/src/lib/site-import/site-import-html.js +332 -45
- package/src/lib/site-import/site-import-html.js.map +1 -1
- package/src/lib/site-import/site-import-images.d.ts +22 -0
- package/src/lib/site-import/site-import-images.js +78 -2
- package/src/lib/site-import/site-import-images.js.map +1 -1
- package/src/lib/site-import/site-import-page.d.ts +44 -2
- package/src/lib/site-import/site-import-page.js +116 -16
- package/src/lib/site-import/site-import-page.js.map +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/layout-language/ai-layout-language.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { AiTool } from '../providers/contract'\nimport { AI_ICON_WORDS } from '../runtime/ai-icon-library'\n\n/**\n * The compact layout language (AGL-3660): what a model designs a page, and a\n * site's header and footer, in — and the one place its shape is declared.\n *\n * ── Why a language, not a tree ───────────────────────────────────────────\n *\n * A page used to be asked for as a raw element tree, packed as escaped JSON\n * text inside a tool call and held to the doctrine's building rules: Grid\n * items sized as strings, every child attached, one h1, a destination on\n * every button, headings under their ceiling. A real model kept about half\n * of a page's sections on the first try, and a Free section was cut off at\n * its token ceiling. The founder chose a language over a library of section\n * templates (10/7): every page is designed for its own request, so no two\n * sites come out alike, and the rules hold because CODE writes the tree.\n *\n * So the model says WHAT each section is and how it is arranged, in a few\n * words of structure, and writes every word a visitor reads; the compiler\n * (`ai-layout-compiler.ts`) turns that into the element tree with the\n * palette's elements and the theme's tokens, by construction.\n *\n * ── The shape ────────────────────────────────────────────────────────────\n *\n * A page is its plan's sections, in order. A SECTION is\n * - `band` — its background: `plain` (the page), `soft` (a paper band),\n * `brand` (the brand color) or `dark` (always dark);\n * - `cols` — a row of columns by relative width, as `[7, 5]` or `[1, 1, 1]`,\n * up to four; absent, one column;\n * - `align` — `start` or `center`;\n * - `blocks` — what it shows, top to bottom.\n *\n * A BLOCK is one thing a visitor reads or uses. Its `kind` is one of\n * `AI_LAYOUT_BLOCK_KINDS`; `col` places it in a column of the row (0 first),\n * and a block with no `col` spans the section — above the row when it comes\n * before the first placed block, below it when it comes after. `text` is its\n * words, `to` where it goes, `style` its emphasis and `icon` a word from the\n * icon library. A group — `list`, `cards`, `steps`, `stats`, `quotes`,\n * `faq` — carries its `items`, each a `title`, a `text`, and optionally its\n * own `to` and `icon`.\n *\n * Where a block goes (`to`): `page:<id>` for a page of this site (its label\n * works too), `#<n>` for section n of this page, `form` for the site's form,\n * `phone` or `email` for the phone number or email the brief gives (a link\n * that calls or writes, AGL-3979), a form's or a component's id for a `form`\n * or `component` block, or an `https:` address the brief gave.\n *\n * A site's header and footer are two sections in the same language\n * (`AI_LAYOUT_FRAME_TOOL`): the header's brand and navigation are the\n * site's own and written by the compiler, so the model gives the header its\n * band, its alignment and an optional call-to-action button; the footer is\n * a section like any other.\n *\n * ── Cheap by shape ───────────────────────────────────────────────────────\n *\n * The schema is a structured object in the tool input (never JSON text in a\n * string), small enough for a strict tool (`tool-schema-limits.spec.ts`), and\n * the language's instructions ride in the cached prefix. A section is a few\n * dozen words of structure beside its copy, so a whole page is one answer.\n */\n\n/** Every kind of block, in the order the schema lists them. */\nexport const AI_LAYOUT_BLOCK_KINDS = [\n 'eyebrow',\n 'heading',\n 'lede',\n 'text',\n 'note',\n 'list',\n 'button',\n 'image',\n 'cards',\n 'steps',\n 'stats',\n 'quotes',\n 'faq',\n 'form',\n 'component',\n] as const\n\nexport type AiLayoutBlockKind = (typeof AI_LAYOUT_BLOCK_KINDS)[number]\n\n/** The kinds that carry `items`. */\nexport const AI_LAYOUT_GROUP_KINDS: ReadonlySet<AiLayoutBlockKind> = new Set([\n 'list',\n 'cards',\n 'steps',\n 'stats',\n 'quotes',\n 'faq',\n])\n\nexport const AI_LAYOUT_BANDS = ['plain', 'soft', 'brand', 'dark'] as const\nexport type AiLayoutBand = (typeof AI_LAYOUT_BANDS)[number]\n\nexport const AI_LAYOUT_ALIGNS = ['start', 'center'] as const\nexport type AiLayoutAlign = (typeof AI_LAYOUT_ALIGNS)[number]\n\nexport const AI_LAYOUT_STYLES = [\n 'primary',\n 'secondary',\n 'quiet',\n 'large',\n] as const\nexport type AiLayoutStyle = (typeof AI_LAYOUT_STYLES)[number]\n\n/** The most columns a row holds; a fifth is folded into the fourth. */\nexport const AI_LAYOUT_MAX_COLS = 4\n\n/**\n * The most items a group shows: one under the eight copies rule 8 reads as a\n * list typed out by hand, which belongs in a dataset.\n */\nexport const AI_LAYOUT_MAX_ITEMS = 7\n\n/** The most quotes a group shows: each is a gap the owner fills with a real one. */\nexport const AI_LAYOUT_MAX_QUOTES = 3\n\nexport interface AiLayoutItem {\n title: string\n text: string\n to?: string\n icon?: string\n}\n\nexport interface AiLayoutBlock {\n kind: AiLayoutBlockKind\n col?: number\n text?: string\n to?: string\n style?: AiLayoutStyle\n icon?: string\n items?: AiLayoutItem[]\n}\n\nexport interface AiLayoutSection {\n band?: AiLayoutBand\n cols?: number[]\n align?: AiLayoutAlign\n blocks: AiLayoutBlock[]\n}\n\n/** A page: its plan's sections, in order. */\nexport interface AiLayoutPageDoc {\n sections: AiLayoutSection[]\n}\n\n/** A site's frame: the header and the footer every page renders between. */\nexport interface AiLayoutFrameDoc {\n header: AiLayoutSection\n footer: AiLayoutSection\n}\n\nconst ITEM_SCHEMA = {\n type: 'object',\n additionalProperties: false,\n required: ['title', 'text', 'to', 'icon'],\n properties: {\n title: { type: 'string' },\n text: { type: 'string' },\n to: { type: 'string' },\n icon: { type: 'string' },\n },\n} as const\n\nconst BLOCK_SCHEMA = {\n type: 'object',\n additionalProperties: false,\n required: ['kind', 'col', 'text', 'to', 'style', 'icon', 'items'],\n properties: {\n kind: { type: 'string', enum: [...AI_LAYOUT_BLOCK_KINDS] },\n col: { type: 'integer' },\n text: { type: 'string' },\n to: { type: 'string' },\n style: { type: 'string', enum: [...AI_LAYOUT_STYLES, 'none'] },\n icon: { type: 'string' },\n items: { type: 'array', items: ITEM_SCHEMA },\n },\n} as const\n\nconst SECTION_SCHEMA = {\n type: 'object',\n additionalProperties: false,\n required: ['band', 'cols', 'align', 'blocks'],\n properties: {\n band: { type: 'string', enum: [...AI_LAYOUT_BANDS] },\n cols: { type: 'array', items: { type: 'integer' } },\n align: { type: 'string', enum: [...AI_LAYOUT_ALIGNS] },\n blocks: { type: 'array', items: BLOCK_SCHEMA },\n },\n} as const\n\n/** The page tool: every section of one page, in plan order, in one answer. */\nexport const AI_LAYOUT_PAGE_TOOL: AiTool = {\n name: 'submit_page',\n description:\n 'Submit the page: one entry in sections for each planned section, in order, in the layout language.',\n strict: true,\n inputSchema: {\n type: 'object',\n additionalProperties: false,\n required: ['sections'],\n properties: { sections: { type: 'array', items: SECTION_SCHEMA } },\n },\n}\n\n/** The frame tool: the site's header and footer, in the same language. */\nexport const AI_LAYOUT_FRAME_TOOL: AiTool = {\n name: 'submit_frame',\n description:\n \"Submit the site's header and footer, each as a section in the layout language.\",\n strict: true,\n inputSchema: {\n type: 'object',\n additionalProperties: false,\n required: ['header', 'footer'],\n properties: { header: SECTION_SCHEMA, footer: SECTION_SCHEMA },\n },\n}\n\n/**\n * The language as a model is taught it: the cached instructions both doors\n * share. Every word of copy is the model's; this says only how a design is\n * written down and what each word becomes.\n */\nexport const AI_LAYOUT_LANGUAGE_TEXT = [\n \"You design web pages in a compact layout language. You decide the structure and write every word; the platform turns your design into the page, with the site's theme, spacing and type, so never describe styling beyond the words below.\",\n '',\n 'A page is a list of sections, one for each section the plan names, in that order. A section has:',\n '- band: plain (the page background), soft (a light band), brand (the brand color) or dark (always dark). Vary the bands down a page so neighboring sections read apart; keep brand and dark to one or two sections a page.',\n '- cols: relative column widths for a row, such as [1, 1] for halves, [7, 5] for a wide and a narrow column, or [1, 1, 1]; at most 4. [] for one column.',\n '- align: start or center.',\n '- blocks: what the section shows, top to bottom. A block with col 0 or more goes in that column of the row (0 is the first); a block with col -1 spans the whole section, above the row when it comes first and below it when it comes last.',\n 'Every field is always written: a block that does not use one writes \"\" (text, to, icon), \"none\" (style), -1 (col) or [] (items); an item that does not use one writes \"\".',\n '',\n 'Block kinds:',\n \"- eyebrow: a short label above a heading. heading: a heading; the first heading of the page's first section is the page's title, and the first heading of every other section is that section's heading. lede: the larger sentence under a heading. text: a paragraph. note: small print.\",\n '- button: text is the label and to says where it goes; style primary, secondary or quiet. Two buttons in a row read as a pair.',\n '- image: text describes the picture to place, which becomes its alt text; the owner adds the picture. icon names a drawing to show until then. to lightbox opens it large when pressed.',\n '- list: items are the lines (title only; text may be empty).',\n \"- cards, steps, stats, faq: items, each with a title and a text. cards are features or services; steps are numbered; stats are a figure (title) with its label (text), only for figures the brief gives; faq items are a question (title) and its answer (text). Use no quotes block: the site has no customer's words to show.\",\n \"- form: places a saved form by its id in to. component: places a reusable component by its id in to; its items fill the component's props, title the prop name and text the value.\",\n '- style large on a heading makes the page title display-sized. icon (on a block or an item) is one of: ' +\n AI_ICON_WORDS.join(', ') +\n '.',\n '',\n \"Where a link goes (to): page:<page id> for a page of this site; #<n> for section n of this page (counted from 1); form for the site's form; phone or email to call or write to the phone number or email the brief gives; or an https: address the brief gives. A button the page has nowhere to send is left out. A to value is never written into words: in text, name a page by its title.\",\n '',\n \"Writing: plain, specific copy in the site's voice, from the brief, the business and its audience, never filler. Keep a page title under 70 characters and a section heading under 80. The site is published exactly as you write it, so where the brief leaves out a fact such as a price, a figure, a name, a phone number, an email, an address or opening hours, leave it out: write the sentence without it, and make no list line, card or contact row for it. Never write a placeholder or a gap in square brackets, and never invent the fact. Never invent a customer, a review, an award or a statistic.\",\n '',\n 'Design each page for this business and this request: choose the bands, the rows and the blocks that tell its story best, so no two sites read alike.',\n].join('\\n')\n\n// ── Reading an answer ────────────────────────────────────────────────────\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\nfunction text(value: unknown): string | undefined {\n return typeof value === 'string' && value.trim() ? value : undefined\n}\n\nfunction oneOf<T extends string>(\n value: unknown,\n values: readonly T[],\n): T | undefined {\n return typeof value === 'string' &&\n (values as readonly string[]).includes(value)\n ? (value as T)\n : undefined\n}\n\n/** What the reader settled on its own, for the job's record and the specs. */\nexport interface AiLayoutSettlement {\n /** Where: `sections[2].blocks[1]`, `header`. */\n at: string\n what: string\n}\n\nfunction readItem(raw: unknown): AiLayoutItem | null {\n if (!isRecord(raw)) return null\n const item: AiLayoutItem = {\n title: typeof raw['title'] === 'string' ? raw['title'] : '',\n text: typeof raw['text'] === 'string' ? raw['text'] : '',\n }\n if (!item.title.trim() && !item.text.trim()) return null\n const to = text(raw['to'])\n const icon = text(raw['icon'])\n return { ...item, ...(to ? { to } : {}), ...(icon ? { icon } : {}) }\n}\n\nfunction readBlock(\n raw: unknown,\n at: string,\n settled: AiLayoutSettlement[],\n): AiLayoutBlock | null {\n if (!isRecord(raw)) {\n settled.push({ at, what: 'not a block; left out' })\n return null\n }\n const kind = oneOf(raw['kind'], AI_LAYOUT_BLOCK_KINDS)\n if (!kind) {\n settled.push({\n at,\n what: `unknown kind ${JSON.stringify(raw['kind'] ?? null)}; left out`,\n })\n return null\n }\n const block: AiLayoutBlock = { kind }\n if (\n typeof raw['col'] === 'number' &&\n Number.isInteger(raw['col']) &&\n raw['col'] >= 0\n )\n block.col = raw['col']\n const words = text(raw['text'])\n if (words) block.text = words\n const to = text(raw['to'])\n if (to) block.to = to.trim()\n const style = oneOf(raw['style'], AI_LAYOUT_STYLES)\n if (style) block.style = style\n const icon = text(raw['icon'])\n if (icon) block.icon = icon.trim().toLowerCase()\n if (AI_LAYOUT_GROUP_KINDS.has(kind) || kind === 'component') {\n const items = (Array.isArray(raw['items']) ? raw['items'] : [])\n .map(readItem)\n .filter((item): item is AiLayoutItem => !!item)\n const most =\n kind === 'quotes'\n ? AI_LAYOUT_MAX_QUOTES\n : kind === 'component'\n ? Infinity\n : AI_LAYOUT_MAX_ITEMS\n if (items.length > most)\n settled.push({\n at,\n what: `${items.length} items; the first ${most} kept`,\n })\n if (items.length) block.items = items.slice(0, most)\n }\n // A block with nothing to show is no block: a group with no items, words\n // with no words. A form or a component may name its record alone.\n const empty = AI_LAYOUT_GROUP_KINDS.has(kind)\n ? !block.items?.length\n : kind === 'form' || kind === 'component'\n ? false\n : !block.text\n if (empty) {\n settled.push({ at, what: `an empty ${kind}; left out` })\n return null\n }\n return block\n}\n\n/** A section read from an answer, or `null` with why it could not be. */\nexport function aiReadLayoutSection(\n raw: unknown,\n at: string,\n settled: AiLayoutSettlement[],\n): AiLayoutSection | null {\n if (!isRecord(raw)) return null\n const section: AiLayoutSection = { blocks: [] }\n const band = oneOf(raw['band'], AI_LAYOUT_BANDS)\n if (band) section.band = band\n const align = oneOf(raw['align'], AI_LAYOUT_ALIGNS)\n if (align) section.align = align\n if (Array.isArray(raw['cols'])) {\n const cols = raw['cols'].filter(\n (value): value is number =>\n typeof value === 'number' && Number.isFinite(value) && value > 0,\n )\n if (cols.length > AI_LAYOUT_MAX_COLS) {\n settled.push({\n at: `${at}.cols`,\n what: `${cols.length} columns; the first ${AI_LAYOUT_MAX_COLS} kept`,\n })\n }\n if (cols.length >= 2)\n section.cols = cols\n .slice(0, AI_LAYOUT_MAX_COLS)\n .map((value) => Math.min(12, Math.max(1, Math.round(value))))\n }\n const blocks = Array.isArray(raw['blocks']) ? raw['blocks'] : []\n blocks.forEach((entry, index) => {\n const block = readBlock(entry, `${at}.blocks[${index}]`, settled)\n if (!block) return\n if (\n block.col !== undefined &&\n section.cols &&\n block.col >= section.cols.length\n ) {\n settled.push({\n at: `${at}.blocks[${index}]`,\n what: `column ${block.col} of ${section.cols.length}; placed in the last`,\n })\n block.col = section.cols.length - 1\n }\n if (block.col !== undefined && !section.cols) delete block.col\n section.blocks.push(block)\n })\n return section.blocks.length ? section : null\n}\n\n/** A page answer read: the sections it gave, by plan position, and the positions it could not. */\nexport interface AiLayoutPageReading {\n /** One entry per planned section; `null` where the answer gave nothing usable. */\n sections: Array<AiLayoutSection | null>\n settled: AiLayoutSettlement[]\n}\n\n/** What a planned section is, as the reader matches an answer's sections to it. */\nexport interface AiLayoutPlannedSection {\n name: string\n items: number\n /** What the plan places in it: a form it binds there is one of these. */\n uses?: readonly string[]\n}\n\n/** A planned section's name that asks for a form: a contact, an inquiry, a booking. */\nconst FORM_SECTION =\n /\\b(form|contact|get in touch|reach|write|message|inquir(?:y|ies|e)|enquir(?:y|ies|e)|book(?:ing|ings)?|reserv(?:e|ation|ations)|quote|appointments?|rsvp|sign ?up|subscribe|newsletter|apply|application|register)\\b/i\n\n/** Whether a planned section is one a form may fill: it names one, or the plan binds a form there. */\nfunction takesForm(planned: AiLayoutPlannedSection): boolean {\n return FORM_SECTION.test(planned.name) || (planned.uses ?? []).some((ref) => /\\bform\\b/i.test(ref))\n}\n\n/** What a form's section scores against a planned section that asks for no form: never chosen while another fits. */\nconst AI_LAYOUT_FORM_MISFIT = -100\n\n/**\n * A word as a heading and a plan's name are compared: letters in any script,\n * with their accents (AGL-3791). As ASCII, \"Menú\" was \"men\", which every\n * \"mensajes\" began with, and \"galería\" was \"galer\" and \"a\".\n */\nconst WORD = /[\\p{L}\\p{M}\\p{N}]+/gu\nconst STOP = new Set(['a', 'an', 'and', 'the', 'of', 'for', 'to', 'our', 'your', 'with', 'in', 'on', 'we', 'us', 'you'])\nconst wordsOf = (text: string) => new Set((text.toLowerCase().match(WORD) ?? []).filter((word) => !STOP.has(word)))\n\n/** The kinds that show a section's repeated items. */\nconst ITEM_KINDS = new Set(['list', 'cards', 'steps', 'stats', 'quotes', 'faq', 'component'])\n\n/**\n * How well an answer's section fits a planned one: the words its headings\n * share with the plan's name, and whether it shows items where the plan\n * asked for them and none where it asked for none.\n *\n * A shared heading word outweighs the items (AGL-3660): a live portfolio's\n * plan gave \"Works Gallery\" no items, and its \"Selected works\" cards lost the\n * place to an inquiry form that shared no word with it. A section holding a\n * form fills only a planned section that asks for one — by its name, or a\n * form the plan binds there — so the form is never a page's stand-in.\n */\nfunction fit(raw: unknown, planned: AiLayoutPlannedSection): number {\n const blocks = isRecord(raw) && Array.isArray(raw['blocks']) ? raw['blocks'].filter(isRecord) : []\n const said = wordsOf(\n blocks\n .filter((block) => block['kind'] === 'heading' || block['kind'] === 'eyebrow')\n .map((block) => String(block['text'] ?? ''))\n .join(' '),\n )\n const named = wordsOf(planned.name)\n let shared = 0\n for (const word of named) if (said.has(word) || [...said].some((other) => other.startsWith(word) || word.startsWith(other))) shared += 1\n const showsItems = blocks.some((block) => ITEM_KINDS.has(String(block['kind'])) && (block['kind'] === 'component' || (Array.isArray(block['items']) && block['items'].length > 0)))\n const wantsItems = planned.items > 0\n const holdsForm = blocks.some((block) => block['kind'] === 'form')\n if (holdsForm && !takesForm(planned)) return AI_LAYOUT_FORM_MISFIT\n return shared * 2 + (wantsItems === showsItems ? 0.5 : -0.5)\n}\n\n/**\n * The answer's sections that best match the plan, in order, where it gave\n * more than the plan names (AGL-3660): a model that opened a page with a\n * hero of its own, or closed it with a call to action, shifted every planned\n * section by one, and the cut fell on the plan's last sections — the ones\n * that carried its items. Each planned section takes the answer's section\n * that fits it best, keeping the answer's order.\n */\nfunction alignSections(given: readonly unknown[], planned: readonly AiLayoutPlannedSection[]): number[] {\n const n = given.length\n const m = planned.length\n // best[i][j]: the best score placing plan sections j.. on answer sections i..\n const best: number[][] = Array.from({ length: n + 1 }, () => Array(m + 1).fill(-Infinity))\n for (let i = 0; i <= n; i += 1) best[i][m] = 0\n for (let i = n - 1; i >= 0; i -= 1) {\n for (let j = m - 1; j >= 0; j -= 1) {\n if (n - i < m - j) continue\n best[i][j] = Math.max(best[i + 1][j], fit(given[i], planned[j]) + best[i + 1][j + 1])\n }\n }\n const picked: number[] = []\n let i = 0\n for (let j = 0; j < m; j += 1) {\n while (i < n && best[i][j] !== fit(given[i], planned[j]) + best[i + 1][j + 1]) i += 1\n picked.push(i)\n i += 1\n }\n return picked\n}\n\n/**\n * The page an answer describes, against the sections its plan names: each\n * planned section takes the answer's section at its position — or, where the\n * answer gave more sections than the plan names and the plan's sections are\n * given, the one that fits it best (`alignSections`) — an answer's extra\n * sections are left out, and a planned section the answer left empty or out\n * is `null` — the one thing a re-ask asks for again.\n */\nexport function aiReadLayoutPage(\n raw: unknown,\n planned: number,\n plan?: readonly AiLayoutPlannedSection[],\n): AiLayoutPageReading {\n const settled: AiLayoutSettlement[] = []\n const given =\n isRecord(raw) && Array.isArray(raw['sections']) ? raw['sections'] : []\n const aligned =\n given.length > planned && plan && plan.length === planned\n ? alignSections(given, plan)\n : Array.from({ length: planned }, (_, index) => index)\n if (given.length > planned) {\n settled.push({\n at: 'sections',\n what: `${given.length} sections for a plan of ${planned}; ${\n aligned.every((at, index) => at === index) ? 'the extra left out' : `sections ${aligned.map((at) => at + 1).join(', ')} kept as the plan's`\n }`,\n })\n }\n const sections = aligned.map((at, index) =>\n aiReadLayoutSection(given[at], `sections[${index}]`, settled),\n )\n return { sections, settled }\n}\n\n/** A frame answer read: a header and a footer, either `null` where it was not usable. */\nexport function aiReadLayoutFrame(raw: unknown): {\n header: AiLayoutSection | null\n footer: AiLayoutSection | null\n settled: AiLayoutSettlement[]\n} {\n const settled: AiLayoutSettlement[] = []\n const record = isRecord(raw) ? raw : {}\n // A header needs no block of its own: its brand and navigation are the\n // site's, so an empty one is a plain bar.\n const header = isRecord(record['header'])\n ? (aiReadLayoutSection(record['header'], 'header', settled) ??\n readBare(record['header']))\n : null\n const footer = aiReadLayoutSection(record['footer'], 'footer', settled)\n return { header, footer, settled }\n}\n\n/** A section with no usable block, keeping only its band and alignment. */\nfunction readBare(raw: Record<string, unknown>): AiLayoutSection {\n const band = oneOf(raw['band'], AI_LAYOUT_BANDS)\n const align = oneOf(raw['align'], AI_LAYOUT_ALIGNS)\n return { ...(band ? { band } : {}), ...(align ? { align } : {}), blocks: [] }\n}\n"],"names":["AI_ICON_WORDS","AI_LAYOUT_BLOCK_KINDS","AI_LAYOUT_GROUP_KINDS","Set","AI_LAYOUT_BANDS","AI_LAYOUT_ALIGNS","AI_LAYOUT_STYLES","AI_LAYOUT_MAX_COLS","AI_LAYOUT_MAX_ITEMS","AI_LAYOUT_MAX_QUOTES","ITEM_SCHEMA","type","additionalProperties","required","properties","title","text","to","icon","BLOCK_SCHEMA","kind","enum","col","style","items","SECTION_SCHEMA","band","cols","align","blocks","AI_LAYOUT_PAGE_TOOL","name","description","strict","inputSchema","sections","AI_LAYOUT_FRAME_TOOL","header","footer","AI_LAYOUT_LANGUAGE_TEXT","join","isRecord","value","Array","isArray","trim","undefined","oneOf","values","includes","readItem","raw","item","readBlock","at","settled","block","push","what","JSON","stringify","Number","isInteger","words","toLowerCase","has","map","filter","most","Infinity","length","slice","empty","aiReadLayoutSection","section","isFinite","Math","min","max","round","forEach","entry","index","FORM_SECTION","takesForm","planned","test","uses","some","ref","AI_LAYOUT_FORM_MISFIT","WORD","STOP","wordsOf","match","word","ITEM_KINDS","fit","said","String","named","shared","other","startsWith","showsItems","wantsItems","holdsForm","alignSections","given","n","m","best","from","fill","i","j","picked","aiReadLayoutPage","plan","aligned","_","every","aiReadLayoutFrame","record","readBare"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/layout-language/ai-layout-language.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { AiTool } from '../providers/contract'\nimport { AI_ICON_WORDS } from '../runtime/ai-icon-library'\n\n/**\n * The compact layout language (AGL-3660): what a model designs a page, and a\n * site's header and footer, in — and the one place its shape is declared.\n *\n * ── Why a language, not a tree ───────────────────────────────────────────\n *\n * A page used to be asked for as a raw element tree, packed as escaped JSON\n * text inside a tool call and held to the doctrine's building rules: Grid\n * items sized as strings, every child attached, one h1, a destination on\n * every button, headings under their ceiling. A real model kept about half\n * of a page's sections on the first try, and a Free section was cut off at\n * its token ceiling. The founder chose a language over a library of section\n * templates (10/7): every page is designed for its own request, so no two\n * sites come out alike, and the rules hold because CODE writes the tree.\n *\n * So the model says WHAT each section is and how it is arranged, in a few\n * words of structure, and writes every word a visitor reads; the compiler\n * (`ai-layout-compiler.ts`) turns that into the element tree with the\n * palette's elements and the theme's tokens, by construction.\n *\n * ── The shape ────────────────────────────────────────────────────────────\n *\n * A page is its plan's sections, in order. A SECTION is\n * - `band` — its background: `plain` (the page), `soft` (a paper band),\n * `brand` (the brand color) or `dark` (always dark);\n * - `cols` — a row of columns by relative width, as `[7, 5]` or `[1, 1, 1]`,\n * up to four; absent, one column;\n * - `align` — `start` or `center`;\n * - `blocks` — what it shows, top to bottom.\n *\n * A BLOCK is one thing a visitor reads or uses. Its `kind` is one of\n * `AI_LAYOUT_BLOCK_KINDS`; `col` places it in a column of the row (0 first),\n * and a block with no `col` spans the section — above the row when it comes\n * before the first placed block, below it when it comes after. `text` is its\n * words, `to` where it goes, `style` its emphasis and `icon` a word from the\n * icon library. A group — `list`, `cards`, `steps`, `stats`, `quotes`,\n * `faq` — carries its `items`, each a `title`, a `text`, and optionally its\n * own `to` and `icon`.\n *\n * Where a block goes (`to`): `page:<id>` for a page of this site (its label\n * works too), `#<n>` for section n of this page, `form` for the site's form,\n * `phone` or `email` for the phone number or email the brief gives (a link\n * that calls or writes, AGL-3979), a form's or a component's id for a `form`\n * or `component` block, or an `https:` address the brief gave.\n *\n * A site's header and footer are two sections in the same language\n * (`AI_LAYOUT_FRAME_TOOL`): the header's brand and navigation are the\n * site's own and written by the compiler, so the model gives the header its\n * band, its alignment and an optional call-to-action button; the footer is\n * a section like any other.\n *\n * ── Cheap by shape ───────────────────────────────────────────────────────\n *\n * The schema is a structured object in the tool input (never JSON text in a\n * string), small enough for a strict tool (`tool-schema-limits.spec.ts`), and\n * the language's instructions ride in the cached prefix. A section is a few\n * dozen words of structure beside its copy, so a whole page is one answer.\n */\n\n/** Every kind of block, in the order the schema lists them. */\nexport const AI_LAYOUT_BLOCK_KINDS = [\n 'eyebrow',\n 'heading',\n 'lede',\n 'text',\n 'note',\n 'list',\n 'button',\n 'image',\n 'cards',\n 'steps',\n 'stats',\n 'quotes',\n 'faq',\n 'form',\n 'component',\n] as const\n\nexport type AiLayoutBlockKind = (typeof AI_LAYOUT_BLOCK_KINDS)[number]\n\n/** The kinds that carry `items`. */\nexport const AI_LAYOUT_GROUP_KINDS: ReadonlySet<AiLayoutBlockKind> = new Set([\n 'list',\n 'cards',\n 'steps',\n 'stats',\n 'quotes',\n 'faq',\n])\n\nexport const AI_LAYOUT_BANDS = ['plain', 'soft', 'brand', 'dark'] as const\nexport type AiLayoutBand = (typeof AI_LAYOUT_BANDS)[number]\n\nexport const AI_LAYOUT_ALIGNS = ['start', 'center'] as const\nexport type AiLayoutAlign = (typeof AI_LAYOUT_ALIGNS)[number]\n\nexport const AI_LAYOUT_STYLES = [\n 'primary',\n 'secondary',\n 'quiet',\n 'large',\n] as const\nexport type AiLayoutStyle = (typeof AI_LAYOUT_STYLES)[number]\n\n/** The most columns a row holds; a fifth is folded into the fourth. */\nexport const AI_LAYOUT_MAX_COLS = 4\n\n/**\n * The most items a group shows: one under the eight copies rule 8 reads as a\n * list typed out by hand, which belongs in a dataset.\n */\nexport const AI_LAYOUT_MAX_ITEMS = 7\n\n/** The most quotes a group shows: each is a gap the owner fills with a real one. */\nexport const AI_LAYOUT_MAX_QUOTES = 3\n\nexport interface AiLayoutItem {\n title: string\n text: string\n to?: string\n icon?: string\n}\n\nexport interface AiLayoutBlock {\n kind: AiLayoutBlockKind\n col?: number\n text?: string\n to?: string\n style?: AiLayoutStyle\n icon?: string\n items?: AiLayoutItem[]\n}\n\nexport interface AiLayoutSection {\n band?: AiLayoutBand\n cols?: number[]\n align?: AiLayoutAlign\n blocks: AiLayoutBlock[]\n}\n\n/** A page: its plan's sections, in order. */\nexport interface AiLayoutPageDoc {\n sections: AiLayoutSection[]\n}\n\n/** A site's frame: the header and the footer every page renders between. */\nexport interface AiLayoutFrameDoc {\n header: AiLayoutSection\n footer: AiLayoutSection\n}\n\nconst ITEM_SCHEMA = {\n type: 'object',\n additionalProperties: false,\n required: ['title', 'text', 'to', 'icon'],\n properties: {\n title: { type: 'string' },\n text: { type: 'string' },\n to: { type: 'string' },\n icon: { type: 'string' },\n },\n} as const\n\nconst BLOCK_SCHEMA = {\n type: 'object',\n additionalProperties: false,\n required: ['kind', 'col', 'text', 'to', 'style', 'icon', 'items'],\n properties: {\n kind: { type: 'string', enum: [...AI_LAYOUT_BLOCK_KINDS] },\n col: { type: 'integer' },\n text: { type: 'string' },\n to: { type: 'string' },\n style: { type: 'string', enum: [...AI_LAYOUT_STYLES, 'none'] },\n icon: { type: 'string' },\n items: { type: 'array', items: ITEM_SCHEMA },\n },\n} as const\n\nconst SECTION_SCHEMA = {\n type: 'object',\n additionalProperties: false,\n required: ['band', 'cols', 'align', 'blocks'],\n properties: {\n band: { type: 'string', enum: [...AI_LAYOUT_BANDS] },\n cols: { type: 'array', items: { type: 'integer' } },\n align: { type: 'string', enum: [...AI_LAYOUT_ALIGNS] },\n blocks: { type: 'array', items: BLOCK_SCHEMA },\n },\n} as const\n\n/** The page tool: every section of one page, in plan order, in one answer. */\nexport const AI_LAYOUT_PAGE_TOOL: AiTool = {\n name: 'submit_page',\n description:\n 'Submit the page: one entry in sections for each planned section, in order, in the layout language.',\n strict: true,\n inputSchema: {\n type: 'object',\n additionalProperties: false,\n required: ['sections'],\n properties: { sections: { type: 'array', items: SECTION_SCHEMA } },\n },\n}\n\n/** The frame tool: the site's header and footer, in the same language. */\nexport const AI_LAYOUT_FRAME_TOOL: AiTool = {\n name: 'submit_frame',\n description:\n \"Submit the site's header and footer, each as a section in the layout language.\",\n strict: true,\n inputSchema: {\n type: 'object',\n additionalProperties: false,\n required: ['header', 'footer'],\n properties: { header: SECTION_SCHEMA, footer: SECTION_SCHEMA },\n },\n}\n\n/**\n * The language as a model is taught it: the cached instructions both doors\n * share. Every word of copy is the model's; this says only how a design is\n * written down and what each word becomes.\n */\nexport const AI_LAYOUT_LANGUAGE_TEXT = [\n \"You design web pages in a compact layout language. You decide the structure and write every word; the platform turns your design into the page, with the site's theme, spacing and type, so never describe styling beyond the words below.\",\n '',\n 'A page is a list of sections, one for each section the plan names, in that order. A section has:',\n '- band: plain (the page background), soft (a light band), brand (the brand color) or dark (always dark). Vary the bands down a page so neighboring sections read apart; keep brand and dark to one or two sections a page.',\n '- cols: relative column widths for a row, such as [1, 1] for halves, [7, 5] for a wide and a narrow column, or [1, 1, 1]; at most 4. [] for one column.',\n '- align: start or center.',\n '- blocks: what the section shows, top to bottom. A block with col 0 or more goes in that column of the row (0 is the first); a block with col -1 spans the whole section, above the row when it comes first and below it when it comes last.',\n 'Every field is always written: a block that does not use one writes \"\" (text, to, icon), \"none\" (style), -1 (col) or [] (items); an item that does not use one writes \"\".',\n '',\n 'Block kinds:',\n \"- eyebrow: a short label above a heading. heading: a heading; the first heading of the page's first section is the page's title, and the first heading of every other section is that section's heading. lede: the larger sentence under a heading. text: a paragraph. note: small print.\",\n '- button: text is the label and to says where it goes; style primary, secondary or quiet. Two buttons in a row read as a pair.',\n '- image: text describes the picture to place, which becomes its alt text; the owner adds the picture. icon names a drawing to show until then. to lightbox opens it large when pressed.',\n '- list: items are the lines (title only; text may be empty).',\n \"- cards, steps, stats, faq: items, each with a title and a text. cards are features or services; steps are numbered; stats are a figure (title) with its label (text), only for figures the brief gives; faq items are a question (title) and its answer (text). Use no quotes block: the site has no customer's words to show.\",\n \"- form: places a saved form by its id in to. component: places a reusable component by its id in to; its items fill the component's props, title the prop name and text the value.\",\n '- style large on a heading makes the page title display-sized. icon (on a block or an item) is one of: ' +\n AI_ICON_WORDS.join(', ') +\n '.',\n '',\n \"Where a link goes (to): page:<page id> for a page of this site; #<n> for section n of this page (counted from 1); form for the site's form; phone or email to call or write to the phone number or email the brief gives; or an https: address the brief gives. A button the page has nowhere to send is left out. A to value is never written into words: in text, name a page by its title.\",\n '',\n \"Writing: plain, specific copy in the site's voice, from the brief, the business and its audience, never filler. Keep a page title under 70 characters and a section heading under 80. The site is published exactly as you write it, so where the brief leaves out a fact such as a price, a figure, a name, a phone number, an email, an address or opening hours, leave it out: write the sentence without it, and make no list line, card or contact row for it. Never write a placeholder or a gap in square brackets, and never invent the fact. Never invent a customer, a review, an award or a statistic.\",\n '',\n 'Design each page for this business and this request: choose the bands, the rows and the blocks that tell its story best, so no two sites read alike.',\n].join('\\n')\n\n// ── Reading an answer ────────────────────────────────────────────────────\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\nfunction text(value: unknown): string | undefined {\n return typeof value === 'string' && value.trim() ? value : undefined\n}\n\nfunction oneOf<T extends string>(\n value: unknown,\n values: readonly T[],\n): T | undefined {\n return typeof value === 'string' &&\n (values as readonly string[]).includes(value)\n ? (value as T)\n : undefined\n}\n\n/** What the reader settled on its own, for the job's record and the specs. */\nexport interface AiLayoutSettlement {\n /** Where: `sections[2].blocks[1]`, `header`. */\n at: string\n what: string\n}\n\nfunction readItem(raw: unknown): AiLayoutItem | null {\n if (!isRecord(raw)) return null\n const item: AiLayoutItem = {\n title: typeof raw['title'] === 'string' ? raw['title'] : '',\n text: typeof raw['text'] === 'string' ? raw['text'] : '',\n }\n if (!item.title.trim() && !item.text.trim()) return null\n const to = text(raw['to'])\n const icon = text(raw['icon'])\n return { ...item, ...(to ? { to } : {}), ...(icon ? { icon } : {}) }\n}\n\nfunction readBlock(\n raw: unknown,\n at: string,\n settled: AiLayoutSettlement[],\n): AiLayoutBlock | null {\n if (!isRecord(raw)) {\n settled.push({ at, what: 'not a block; left out' })\n return null\n }\n const kind = oneOf(raw['kind'], AI_LAYOUT_BLOCK_KINDS)\n if (!kind) {\n settled.push({\n at,\n what: `unknown kind ${JSON.stringify(raw['kind'] ?? null)}; left out`,\n })\n return null\n }\n const block: AiLayoutBlock = { kind }\n if (\n typeof raw['col'] === 'number' &&\n Number.isInteger(raw['col']) &&\n raw['col'] >= 0\n )\n block.col = raw['col']\n const words = text(raw['text'])\n if (words) block.text = words\n const to = text(raw['to'])\n if (to) block.to = to.trim()\n const style = oneOf(raw['style'], AI_LAYOUT_STYLES)\n if (style) block.style = style\n const icon = text(raw['icon'])\n if (icon) block.icon = icon.trim().toLowerCase()\n if (AI_LAYOUT_GROUP_KINDS.has(kind) || kind === 'component') {\n const items = (Array.isArray(raw['items']) ? raw['items'] : [])\n .map(readItem)\n .filter((item): item is AiLayoutItem => !!item)\n const most =\n kind === 'quotes'\n ? AI_LAYOUT_MAX_QUOTES\n : kind === 'component'\n ? Infinity\n : AI_LAYOUT_MAX_ITEMS\n if (items.length > most)\n settled.push({\n at,\n what: `${items.length} items; the first ${most} kept`,\n })\n if (items.length) block.items = items.slice(0, most)\n }\n // A block with nothing to show is no block: a group with no items, words\n // with no words. A form or a component may name its record alone.\n const empty = AI_LAYOUT_GROUP_KINDS.has(kind)\n ? !block.items?.length\n : kind === 'form' || kind === 'component'\n ? false\n : !block.text\n if (empty) {\n settled.push({ at, what: `an empty ${kind}; left out` })\n return null\n }\n return block\n}\n\n/** A section read from an answer, or `null` with why it could not be. */\nexport function aiReadLayoutSection(\n raw: unknown,\n at: string,\n settled: AiLayoutSettlement[],\n): AiLayoutSection | null {\n if (!isRecord(raw)) return null\n const section: AiLayoutSection = { blocks: [] }\n const band = oneOf(raw['band'], AI_LAYOUT_BANDS)\n if (band) section.band = band\n const align = oneOf(raw['align'], AI_LAYOUT_ALIGNS)\n if (align) section.align = align\n if (Array.isArray(raw['cols'])) {\n const cols = raw['cols'].filter(\n (value): value is number =>\n typeof value === 'number' && Number.isFinite(value) && value > 0,\n )\n if (cols.length > AI_LAYOUT_MAX_COLS) {\n settled.push({\n at: `${at}.cols`,\n what: `${cols.length} columns; the first ${AI_LAYOUT_MAX_COLS} kept`,\n })\n }\n if (cols.length >= 2)\n section.cols = cols\n .slice(0, AI_LAYOUT_MAX_COLS)\n .map((value) => Math.min(12, Math.max(1, Math.round(value))))\n }\n const blocks = Array.isArray(raw['blocks']) ? raw['blocks'] : []\n blocks.forEach((entry, index) => {\n const block = readBlock(entry, `${at}.blocks[${index}]`, settled)\n if (!block) return\n if (\n block.col !== undefined &&\n section.cols &&\n block.col >= section.cols.length\n ) {\n settled.push({\n at: `${at}.blocks[${index}]`,\n what: `column ${block.col} of ${section.cols.length}; placed in the last`,\n })\n block.col = section.cols.length - 1\n }\n if (block.col !== undefined && !section.cols) delete block.col\n section.blocks.push(block)\n })\n return section.blocks.length ? section : null\n}\n\n/** A page answer read: the sections it gave, by plan position, and the positions it could not. */\nexport interface AiLayoutPageReading {\n /** One entry per planned section; `null` where the answer gave nothing usable. */\n sections: Array<AiLayoutSection | null>\n settled: AiLayoutSettlement[]\n}\n\n/** What a planned section is, as the reader matches an answer's sections to it. */\nexport interface AiLayoutPlannedSection {\n name: string\n items: number\n /** What the plan places in it: a form it binds there is one of these. */\n uses?: readonly string[]\n}\n\n/** A planned section's name that asks for a form: a contact, an inquiry, a booking. */\nconst FORM_SECTION =\n /\\b(form|contact|get in touch|reach|write|message|inquir(?:y|ies|e)|enquir(?:y|ies|e)|book(?:ing|ings)?|reserv(?:e|ation|ations)|quote|appointments?|rsvp|sign ?up|subscribe|newsletter|apply|application|register)\\b/i\n\n/** Whether a planned section is one a form may fill: it names one, or the plan binds a form there. */\nfunction takesForm(planned: AiLayoutPlannedSection): boolean {\n return FORM_SECTION.test(planned.name) || (planned.uses ?? []).some((ref) => /\\bform\\b/i.test(ref))\n}\n\n/** What a form's section scores against a planned section that asks for no form: never chosen while another fits. */\nconst AI_LAYOUT_FORM_MISFIT = -100\n\n/**\n * A word as a heading and a plan's name are compared: letters in any script,\n * with their accents (AGL-3791). As ASCII, \"Menú\" was \"men\", which every\n * \"mensajes\" began with, and \"galería\" was \"galer\" and \"a\".\n */\nconst WORD = /[\\p{L}\\p{M}\\p{N}]+/gu\nconst STOP = new Set(['a', 'an', 'and', 'the', 'of', 'for', 'to', 'our', 'your', 'with', 'in', 'on', 'we', 'us', 'you'])\nconst wordsOf = (text: string) => new Set((text.toLowerCase().match(WORD) ?? []).filter((word) => !STOP.has(word)))\n\n/** The kinds that show a section's repeated items. */\nconst ITEM_KINDS = new Set(['list', 'cards', 'steps', 'stats', 'quotes', 'faq', 'component'])\n\n/**\n * How well an answer's section fits a planned one: the words its headings\n * share with the plan's name, and whether it shows items where the plan\n * asked for them and none where it asked for none.\n *\n * A shared heading word outweighs the items (AGL-3660): a live portfolio's\n * plan gave \"Works Gallery\" no items, and its \"Selected works\" cards lost the\n * place to an inquiry form that shared no word with it. A section holding a\n * form fills only a planned section that asks for one — by its name, or a\n * form the plan binds there — so the form is never a page's stand-in.\n */\nfunction fit(raw: unknown, planned: AiLayoutPlannedSection): number {\n const blocks = isRecord(raw) && Array.isArray(raw['blocks']) ? raw['blocks'].filter(isRecord) : []\n const said = wordsOf(\n blocks\n .filter((block) => block['kind'] === 'heading' || block['kind'] === 'eyebrow')\n .map((block) => String(block['text'] ?? ''))\n .join(' '),\n )\n const named = wordsOf(planned.name)\n let shared = 0\n for (const word of named) if (said.has(word) || [...said].some((other) => other.startsWith(word) || word.startsWith(other))) shared += 1\n const showsItems = blocks.some((block) => ITEM_KINDS.has(String(block['kind'])) && (block['kind'] === 'component' || (Array.isArray(block['items']) && block['items'].length > 0)))\n const wantsItems = planned.items > 0\n const holdsForm = blocks.some((block) => block['kind'] === 'form')\n if (holdsForm && !takesForm(planned)) return AI_LAYOUT_FORM_MISFIT\n return shared * 2 + (wantsItems === showsItems ? 0.5 : -0.5)\n}\n\n/**\n * The answer's sections that best match the plan, in order, where it gave\n * more than the plan names (AGL-3660): a model that opened a page with a\n * hero of its own, or closed it with a call to action, shifted every planned\n * section by one, and the cut fell on the plan's last sections — the ones\n * that carried its items. Each planned section takes the answer's section\n * that fits it best, keeping the answer's order.\n */\nfunction alignSections(given: readonly unknown[], planned: readonly AiLayoutPlannedSection[]): number[] {\n const n = given.length\n const m = planned.length\n // best[i][j]: the best score placing plan sections j.. on answer sections i..\n const best: number[][] = Array.from({ length: n + 1 }, () => Array(m + 1).fill(-Infinity))\n for (let i = 0; i <= n; i += 1) best[i][m] = 0\n for (let i = n - 1; i >= 0; i -= 1) {\n for (let j = m - 1; j >= 0; j -= 1) {\n if (n - i < m - j) continue\n best[i][j] = Math.max(best[i + 1][j], fit(given[i], planned[j]) + best[i + 1][j + 1])\n }\n }\n const picked: number[] = []\n let i = 0\n for (let j = 0; j < m; j += 1) {\n while (i < n && best[i][j] !== fit(given[i], planned[j]) + best[i + 1][j + 1]) i += 1\n picked.push(i)\n i += 1\n }\n return picked\n}\n\n/**\n * The page an answer describes, against the sections its plan names: each\n * planned section takes the answer's section at its position — or, where the\n * answer gave more sections than the plan names and the plan's sections are\n * given, the one that fits it best (`alignSections`) — an answer's extra\n * sections are left out, and a planned section the answer left empty or out\n * is `null` — the one thing a re-ask asks for again.\n */\nexport function aiReadLayoutPage(\n raw: unknown,\n planned: number,\n plan?: readonly AiLayoutPlannedSection[],\n): AiLayoutPageReading {\n const settled: AiLayoutSettlement[] = []\n const given =\n isRecord(raw) && Array.isArray(raw['sections']) ? raw['sections'] : []\n const aligned =\n given.length > planned && plan && plan.length === planned\n ? alignSections(given, plan)\n : Array.from({ length: planned }, (_, index) => index)\n if (given.length > planned) {\n settled.push({\n at: 'sections',\n what: `${given.length} sections for a plan of ${planned}; ${\n aligned.every((at, index) => at === index) ? 'the extra left out' : `sections ${aligned.map((at) => at + 1).join(', ')} kept as the plan's`\n }`,\n })\n }\n const sections = aligned.map((at, index) =>\n aiReadLayoutSection(given[at], `sections[${index}]`, settled),\n )\n return { sections, settled }\n}\n\n/** A frame answer read: a header and a footer, either `null` where it was not usable. */\nexport function aiReadLayoutFrame(raw: unknown): {\n header: AiLayoutSection | null\n footer: AiLayoutSection | null\n settled: AiLayoutSettlement[]\n} {\n const settled: AiLayoutSettlement[] = []\n const record = isRecord(raw) ? raw : {}\n // A header needs no block of its own: its brand and navigation are the\n // site's, so an empty one is a plain bar.\n const header = isRecord(record['header'])\n ? (aiReadLayoutSection(record['header'], 'header', settled) ??\n readBare(record['header']))\n : null\n const footer = aiReadLayoutSection(record['footer'], 'footer', settled)\n return { header, footer, settled }\n}\n\n/** A section with no usable block, keeping only its band and alignment. */\nfunction readBare(raw: Record<string, unknown>): AiLayoutSection {\n const band = oneOf(raw['band'], AI_LAYOUT_BANDS)\n const align = oneOf(raw['align'], AI_LAYOUT_ALIGNS)\n return { ...(band ? { band } : {}), ...(align ? { align } : {}), blocks: [] }\n}\n\n// ── Arranging a page brought in (AGL-4084) ───────────────────────────────\n\n/**\n * A page brought in from its owner's own website is ARRANGED, never written\n * (AGL-4084): its words are the source's, numbered line by line, and the\n * answer says only how each section is laid out and which numbered lines go\n * in which kind of block. Code substitutes every line exactly as the source\n * wrote it (`jobs/ai-site-import-document.ts`), so the schema below has no\n * field a model could put a word of copy in: a rewording has nowhere to go.\n */\n\n/** The kinds of block an arranged line may go in. */\nexport const AI_LAYOUT_ARRANGE_KINDS = [\n 'eyebrow',\n 'heading',\n 'lede',\n 'text',\n 'note',\n 'list',\n 'button',\n 'image',\n 'cards',\n 'steps',\n 'stats',\n 'faq',\n] as const\n\nexport type AiLayoutArrangeKind = (typeof AI_LAYOUT_ARRANGE_KINDS)[number]\n\n/** The arranged kinds whose lines are items: each item's `from` is its title line, then its text lines. */\nexport const AI_LAYOUT_ARRANGE_ITEM_KINDS: ReadonlySet<AiLayoutArrangeKind> = new Set(['cards', 'steps', 'stats', 'faq'])\n\n/** The bands an arranged section may take: a brand band's words need a contrast the source never chose. */\nexport const AI_LAYOUT_ARRANGE_BANDS = ['plain', 'soft', 'dark'] as const\nexport type AiLayoutArrangeBand = (typeof AI_LAYOUT_ARRANGE_BANDS)[number]\n\nexport interface AiLayoutArrangedBlock {\n kind: AiLayoutArrangeKind\n col?: number\n /** The numbered source lines it shows, in order. */\n from: number[]\n style?: AiLayoutStyle\n /** For a group: each item's lines, its title line first. */\n items?: number[][]\n}\n\nexport interface AiLayoutArrangedSection {\n band?: AiLayoutArrangeBand\n cols?: number[]\n align?: AiLayoutAlign\n blocks: AiLayoutArrangedBlock[]\n}\n\n/** An arrangement answer read: one entry per source section, `null` where it gave none. */\nexport interface AiLayoutArrangement {\n sections: Array<AiLayoutArrangedSection | null>\n}\n\nconst LINE_LIST_SCHEMA = { type: 'array', items: { type: 'integer' } } as const\n\nconst ARRANGE_ITEM_SCHEMA = {\n type: 'object',\n additionalProperties: false,\n required: ['from'],\n properties: { from: LINE_LIST_SCHEMA },\n} as const\n\nconst ARRANGE_BLOCK_SCHEMA = {\n type: 'object',\n additionalProperties: false,\n required: ['kind', 'col', 'from', 'style', 'items'],\n properties: {\n kind: { type: 'string', enum: [...AI_LAYOUT_ARRANGE_KINDS] },\n col: { type: 'integer' },\n from: LINE_LIST_SCHEMA,\n style: { type: 'string', enum: [...AI_LAYOUT_STYLES, 'none'] },\n items: { type: 'array', items: ARRANGE_ITEM_SCHEMA },\n },\n} as const\n\nconst ARRANGE_SECTION_SCHEMA = {\n type: 'object',\n additionalProperties: false,\n required: ['band', 'cols', 'align', 'blocks'],\n properties: {\n band: { type: 'string', enum: [...AI_LAYOUT_ARRANGE_BANDS] },\n cols: { type: 'array', items: { type: 'integer' } },\n align: { type: 'string', enum: [...AI_LAYOUT_ALIGNS] },\n blocks: { type: 'array', items: ARRANGE_BLOCK_SCHEMA },\n },\n} as const\n\n/** The arrangement tool: every section of a page brought in, in order, by its numbered lines. */\nexport const AI_LAYOUT_ARRANGE_TOOL: AiTool = {\n name: 'submit_arrangement',\n description:\n \"Submit the page's arrangement: one entry in sections for each section of the page, in order, placing its numbered lines in blocks by from.\",\n strict: true,\n inputSchema: {\n type: 'object',\n additionalProperties: false,\n required: ['sections'],\n properties: { sections: { type: 'array', items: ARRANGE_SECTION_SCHEMA } },\n },\n}\n\n/** Line numbers as an answer gives them: whole, positive, each once. */\nfunction lineNumbers(value: unknown): number[] {\n if (!Array.isArray(value)) return []\n const out: number[] = []\n for (const entry of value) {\n if (typeof entry === 'number' && Number.isInteger(entry) && entry > 0 && !out.includes(entry)) out.push(entry)\n }\n return out\n}\n\nfunction readArrangedBlock(raw: unknown): AiLayoutArrangedBlock | null {\n if (!isRecord(raw)) return null\n const kind = oneOf(raw['kind'], AI_LAYOUT_ARRANGE_KINDS)\n if (!kind) return null\n // Only line numbers are read: any words an answer carried are ignored.\n const block: AiLayoutArrangedBlock = { kind, from: lineNumbers(raw['from']) }\n if (typeof raw['col'] === 'number' && Number.isInteger(raw['col']) && raw['col'] >= 0) block.col = raw['col']\n const style = oneOf(raw['style'], AI_LAYOUT_STYLES)\n if (style) block.style = style\n if (AI_LAYOUT_ARRANGE_ITEM_KINDS.has(kind)) {\n const items = (Array.isArray(raw['items']) ? raw['items'] : [])\n .map((item) => (isRecord(item) ? lineNumbers(item['from']) : []))\n .filter((lines) => lines.length > 0)\n if (items.length) block.items = items\n }\n // A block that names no line is no block.\n return block.from.length || block.items?.length ? block : null\n}\n\nfunction readArrangedSection(raw: unknown): AiLayoutArrangedSection | null {\n if (!isRecord(raw)) return null\n const section: AiLayoutArrangedSection = { blocks: [] }\n const band = oneOf(raw['band'], AI_LAYOUT_ARRANGE_BANDS)\n if (band) section.band = band\n const align = oneOf(raw['align'], AI_LAYOUT_ALIGNS)\n if (align) section.align = align\n if (Array.isArray(raw['cols'])) {\n const cols = raw['cols'].filter((value): value is number => typeof value === 'number' && Number.isFinite(value) && value > 0)\n if (cols.length >= 2) section.cols = cols.slice(0, AI_LAYOUT_MAX_COLS).map((value) => Math.min(12, Math.max(1, Math.round(value))))\n }\n for (const entry of Array.isArray(raw['blocks']) ? raw['blocks'] : []) {\n const block = readArrangedBlock(entry)\n if (!block) continue\n if (block.col !== undefined && (!section.cols || block.col >= section.cols.length)) {\n if (section.cols) block.col = section.cols.length - 1\n else delete block.col\n }\n section.blocks.push(block)\n }\n return section.blocks.length ? section : null\n}\n\n/**\n * An arrangement answer read (AGL-4084): each source section's layout, by\n * position, and `null` for one it gave nothing usable for — that section is\n * then laid out as the source has it. Words are never read from an answer.\n */\nexport function aiReadLayoutArrangement(raw: unknown, sections: number): AiLayoutArrangement {\n const given = isRecord(raw) && Array.isArray(raw['sections']) ? raw['sections'] : []\n return { sections: Array.from({ length: Math.max(0, sections) }, (_, index) => readArrangedSection(given[index])) }\n}\n"],"names":["AI_ICON_WORDS","AI_LAYOUT_BLOCK_KINDS","AI_LAYOUT_GROUP_KINDS","Set","AI_LAYOUT_BANDS","AI_LAYOUT_ALIGNS","AI_LAYOUT_STYLES","AI_LAYOUT_MAX_COLS","AI_LAYOUT_MAX_ITEMS","AI_LAYOUT_MAX_QUOTES","ITEM_SCHEMA","type","additionalProperties","required","properties","title","text","to","icon","BLOCK_SCHEMA","kind","enum","col","style","items","SECTION_SCHEMA","band","cols","align","blocks","AI_LAYOUT_PAGE_TOOL","name","description","strict","inputSchema","sections","AI_LAYOUT_FRAME_TOOL","header","footer","AI_LAYOUT_LANGUAGE_TEXT","join","isRecord","value","Array","isArray","trim","undefined","oneOf","values","includes","readItem","raw","item","readBlock","at","settled","block","push","what","JSON","stringify","Number","isInteger","words","toLowerCase","has","map","filter","most","Infinity","length","slice","empty","aiReadLayoutSection","section","isFinite","Math","min","max","round","forEach","entry","index","FORM_SECTION","takesForm","planned","test","uses","some","ref","AI_LAYOUT_FORM_MISFIT","WORD","STOP","wordsOf","match","word","ITEM_KINDS","fit","said","String","named","shared","other","startsWith","showsItems","wantsItems","holdsForm","alignSections","given","n","m","best","from","fill","i","j","picked","aiReadLayoutPage","plan","aligned","_","every","aiReadLayoutFrame","record","readBare","AI_LAYOUT_ARRANGE_KINDS","AI_LAYOUT_ARRANGE_ITEM_KINDS","AI_LAYOUT_ARRANGE_BANDS","LINE_LIST_SCHEMA","ARRANGE_ITEM_SCHEMA","ARRANGE_BLOCK_SCHEMA","ARRANGE_SECTION_SCHEMA","AI_LAYOUT_ARRANGE_TOOL","lineNumbers","out","readArrangedBlock","lines","readArrangedSection","aiReadLayoutArrangement"],"mappings":"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"}
|
|
@@ -159,6 +159,21 @@ export declare const AI_MEASURED_IMPORT_CREDITS: {
|
|
|
159
159
|
readonly median: 29;
|
|
160
160
|
readonly p90: 29;
|
|
161
161
|
};
|
|
162
|
+
/**
|
|
163
|
+
* A main page Aglyn AI arranges from its old page's own lines (AGL-4083):
|
|
164
|
+
* one frame-sized structured answer, no prose, and its listing.
|
|
165
|
+
* PROVISIONAL until the first live import measures it; it prices nothing
|
|
166
|
+
* until the step marks a page `arranged`.
|
|
167
|
+
*/
|
|
168
|
+
readonly arranged: {
|
|
169
|
+
readonly median: 12;
|
|
170
|
+
readonly p90: 20;
|
|
171
|
+
};
|
|
172
|
+
/** A page copied as it is (AGL-4083): its listing pass alone. Provisional, as `arranged`. */
|
|
173
|
+
readonly copied: {
|
|
174
|
+
readonly median: 1;
|
|
175
|
+
readonly p90: 2;
|
|
176
|
+
};
|
|
162
177
|
};
|
|
163
178
|
/**
|
|
164
179
|
* When the table above was read, and from when its jobs ran. Both tables
|
|
@@ -147,7 +147,20 @@
|
|
|
147
147
|
* them: their `page` rows' `creditsSpent`, as the paid table is read.
|
|
148
148
|
*/ export const AI_MEASURED_IMPORT_CREDITS = {
|
|
149
149
|
page: AI_MEASURED_PAID_SITE_CREDITS.page,
|
|
150
|
-
frame: AI_MEASURED_PAID_SITE_CREDITS.layout
|
|
150
|
+
frame: AI_MEASURED_PAID_SITE_CREDITS.layout,
|
|
151
|
+
/**
|
|
152
|
+
* A main page Aglyn AI arranges from its old page's own lines (AGL-4083):
|
|
153
|
+
* one frame-sized structured answer, no prose, and its listing.
|
|
154
|
+
* PROVISIONAL until the first live import measures it; it prices nothing
|
|
155
|
+
* until the step marks a page `arranged`.
|
|
156
|
+
*/ arranged: {
|
|
157
|
+
median: 12,
|
|
158
|
+
p90: 20
|
|
159
|
+
},
|
|
160
|
+
/** A page copied as it is (AGL-4083): its listing pass alone. Provisional, as `arranged`. */ copied: {
|
|
161
|
+
median: 1,
|
|
162
|
+
p90: 2
|
|
163
|
+
}
|
|
151
164
|
};
|
|
152
165
|
/**
|
|
153
166
|
* When the table above was read, and from when its jobs ran. Both tables
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-credit-estimate.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * WHAT A JOB REALLY COSTS, NEXT TO WHAT IT CAN COST (AGL-3722).\n *\n * Every job's cost used to be quoted at its CEILING: each pass at the\n * `AI_SITE_PASS_CREDITS` (50) a pass is reserved against. A Free Assist\n * build of two pages, a quote form and a layout was quoted 600 credits and\n * refused with 164 left, when builds like it measure about a fifth of that.\n *\n * So a job is now quoted as a RANGE: what it is likely to cost (the sum of\n * the measured medians of what it builds), what it costs nine times in ten\n * (the measured p90s), and its ceiling (the old figure, unchanged). The\n * meter still charges actual use, and the reservation each pass takes is\n * still the ceiling's 50 — only the words and the Free admission move:\n * a Free job is admitted outright when its p90 fits what is left, and asked\n * to be confirmed when it does not.\n *\n * This module is pure: the console, the doors and the specs read one table.\n */\n\n/** One measured cost: the median and the 90th percentile, in credits. */\nexport interface AiMeasuredCredits {\n median: number\n p90: number\n}\n\n/**\n * MEASURED IN PRODUCTION, credits per unit, over every `done` AI job since\n * 2026-10-01 (read 2026-10-09; a whole Free site job came to 148–160):\n *\n * page item 30 / 40 (a page of about `AI_MEASURED_PAGE_SECTIONS` sections, its listing included)\n * form 29 / 31\n * layout 23 / 24 (header and footer, one answer)\n * theme 4 / 8 (a site start's look)\n * plan step 21 / 70\n *\n * THE FAST TIER'S PART, REPRICED (AGL-3781). A site start's look, a Free\n * start's plan and every page's listing run on the fast tier, measured above\n * on Claude Haiku 4.5 at its cost. They run on Claude Haiku 5.5 at the\n * platform's markup now, so those parts are repriced from what production\n * sent, at the new rates and about 30% more tokens: over October 1–14 a look\n * 4 / 7 → 1 / 2, a Free start's plan 15 / 22 → 3 / 5, a listing 3 / 5 → 1 / 1\n * (2 less on every page), and a whole two-page Free site 148 → 121 at the\n * median. The other rows ran on the balanced tier and do not move.\n *\n * HOW TO RE-MEASURE: a read-only collection-group read of `aiJobs` in\n * production where `status == 'done'` and `createdAt` is in the window; group\n * each ledger row's (`items[]`) `creditsSpent` by its `op` (the site start's\n * look is the `theme` row), take the plan step's `steps[].creditsSpent` for\n * `plan`, and each job's `creditsSpent` for the whole-site band. Put each\n * group's median and 90th percentile here, and the dates in\n * `AI_MEASURED_AT`. `ai-credit-estimate.spec.ts` holds the table, the page\n * scaling and the measured whole-site band together, so a re-measure that\n * moves one moves the spec with it.\n */\nexport const AI_MEASURED_UNIT_CREDITS = {\n // 30 / 40 with its listing at Haiku 4.5's cost.\n page: { median: 28, p90: 38 },\n form: { median: 29, p90: 31 },\n layout: { median: 23, p90: 24 },\n // 4 / 8 on Haiku 4.5.\n theme: { median: 1, p90: 2 },\n // 21 / 70: the median is a Free start's plan, on the fast tier; the p90 a\n // plan on the balanced tier, which does not move.\n plan: { median: 4, p90: 70 },\n} as const satisfies Record<string, AiMeasuredCredits>\n\n/**\n * MEASURED IN PRODUCTION, credits per unit of a PAID site start, whose plan\n * and parts run on the paid routing (Sonnet) rather than a Free start's\n * (Haiku) the table above is mostly made of: every `done` paid site job\n * since 2026-10-01, read 2026-10-12 (6 jobs; products from the 4 stores; AGL-3739).\n *\n * plan step 81 / 86 (a Free start's plan: 20 / 42 on Haiku 4.5, 3 / 5 repriced)\n * page item 29 / 49 (about `AI_MEASURED_PAGE_SECTIONS` sections; 27 / 47 with its listing repriced)\n * form 32 / 39\n * layout 29 / 29\n * theme 4 / 4 (1 / 1 repriced)\n * email 49 / 51\n * products 64 / 66 (a store's first products, one answer)\n *\n * A 5-page store with its welcome email was quoted about 277 off the table\n * above: its plan at a Free start's 21 where paid plans run about 81, and its\n * first products not counted at all where they run about 64. Those stores\n * came to 413-448; this table quotes one at about 404.\n *\n * Re-measure as the table above says, keeping only jobs whose plan step's\n * `tokens.model` is the paid routing's.\n */\nexport const AI_MEASURED_PAID_SITE_CREDITS = {\n plan: { median: 81, p90: 86 },\n page: { median: 27, p90: 47 },\n form: { median: 32, p90: 39 },\n layout: { median: 29, p90: 29 },\n theme: { median: 1, p90: 1 },\n email: { median: 49, p90: 51 },\n products: { median: 64, p90: 66 },\n} as const satisfies Record<string, AiMeasuredCredits>\n\n/**\n * A page brought in from a person's own website (AGL-3976), priced as an AI\n * page: a paid site start's page as measured above, since an imported page is\n * one answer in the layout language and its listing, written inside one job\n * the way a site start writes its pages. Its site's header and footer, built\n * once in an import's first wave, is that table's layout.\n *\n * Re-measure from done `import` jobs' item ledgers once production has run\n * them: their `page` rows' `creditsSpent`, as the paid table is read.\n */\nexport const AI_MEASURED_IMPORT_CREDITS = {\n page: AI_MEASURED_PAID_SITE_CREDITS.page,\n frame: AI_MEASURED_PAID_SITE_CREDITS.layout,\n} as const satisfies Record<string, AiMeasuredCredits>\n\n/**\n * When the table above was read, and from when its jobs ran. Both tables\n * were measured before AGL-3781 asked a paid site's plan at medium effort and\n * the layout language's pages and frames at low. A lower effort draws less,\n * so their plan, page and layout figures stand as the higher reading until\n * the next re-measure: a quote is never under what is drawn.\n */\nexport const AI_MEASURED_AT = { read: '2026-10-09', since: '2026-10-01' } as const\n\n/**\n * What a whole Free two-page site job measured at, in credits, in the same\n * window: 148–160 with its plan, look and listings at Haiku 4.5's cost, 27\n * less at the median with them repriced (AGL-3781).\n */\nexport const AI_MEASURED_FREE_SITE_CREDITS = { low: 121, high: 133 } as const\n\n/** The sections the measured page item is about: a page is scaled around it. */\nexport const AI_MEASURED_PAGE_SECTIONS = 5\n\n/**\n * What a page of this many sections is likely to cost: the measured page\n * scaled by its sections around the ~5-section median — about 6 credits a\n * section, 8 at the p90.\n */\nexport function aiMeasuredPageCredits(sections: number): AiMeasuredCredits {\n const count = Math.max(1, Math.floor(Number.isFinite(sections) ? sections : AI_MEASURED_PAGE_SECTIONS))\n const { median, p90 } = AI_MEASURED_UNIT_CREDITS.page\n return {\n median: Math.ceil((median * count) / AI_MEASURED_PAGE_SECTIONS),\n p90: Math.ceil((p90 * count) / AI_MEASURED_PAGE_SECTIONS),\n }\n}\n\n/**\n * What a page costs when it is written ON ITS OWN — a page an Assist build or\n * a page job writes, rather than one of a site start's pages (AGL-3722).\n *\n * The table's page item is a site start's: its pages are written one after\n * another inside one job, the later ones reading the prompt the first one\n * paid to write, so a 2-section second page measured 19–28. A page written on\n * its own pays that prompt itself and reads the site it lands on, and costs\n * what a site start's FIRST page costs: a 5-section home page measured 39–78\n * (median 40, p90 78), standalone page jobs' writing step 82–105 for a\n * 2-section page, and the Assist build pages of 2026-10-09 22–89 each. Quoting\n * a build's pages at the site start's scaled median told a Free workspace\n * \"about 48 credits\" for two 4-section pages (AGL-3722, 2026-10-10).\n *\n * So it is priced as a fixed cost — the prompt, the site, the listing — and a\n * cost per section: a 4-section page about 38 (p90 76), a 5-section 44 (p90 88).\n * The fixed part was 16 / 30 with its listing at Haiku 4.5's cost (AGL-3781).\n */\nexport const AI_MEASURED_STANDALONE_PAGE = {\n fixed: { median: 14, p90: 28 },\n perSection: { median: 6, p90: 12 },\n} as const satisfies Record<string, AiMeasuredCredits>\n\n/** What a page of this many sections written on its own is likely to cost. */\nexport function aiMeasuredStandalonePageCredits(sections: number): AiMeasuredCredits {\n const count = Math.max(1, Math.floor(Number.isFinite(sections) ? sections : AI_MEASURED_PAGE_SECTIONS))\n const { fixed, perSection } = AI_MEASURED_STANDALONE_PAGE\n return {\n median: fixed.median + perSection.median * count,\n p90: fixed.p90 + perSection.p90 * count,\n }\n}\n\n/**\n * What a creation costs when it is written ON ITS OWN (AGL-3722), where it\n * measured apart from a site start's: an Assist build's layout came to 24–28\n * (2026-10-09/10), against a site start's 23/24 written in its warm prompt.\n * A form measured the same either way (25–30 on its own, 29/31 in a site\n * start), so it keeps the table's figure.\n */\nexport const AI_MEASURED_STANDALONE_CREATION_CREDITS = {\n layout: { median: 26, p90: 30 },\n} as const satisfies Record<string, AiMeasuredCredits>\n\n/**\n * What one pass of a kind with no measurement of its own is likely to cost\n * (a component, an email design, a template, an item another runner\n * builds), written on its own: a component job's writing step measured\n * 52–65, a form job's 37, a layout job's 63, and the Assist build of\n * 2026-10-09 a welcome email at 46 and a products step at 60. Until\n * 2026-10-10 this was a site start's form (29/31), which is written inside a\n * site job's warm prompt and is the cheapest single answer there is.\n */\nexport const AI_MEASURED_PASS_CREDITS: AiMeasuredCredits = { median: 50, p90: 65 }\n\n/** A job's cost as it is quoted: likely, nine times in ten, and at its worst. */\nexport interface AiCreditRange {\n /** The sum of the measured medians: what the job is likely to spend. */\n likely: number\n /** The sum of the measured p90s: what a Free job is admitted on. */\n p90: number\n /** Every pass at its reserve: what it can spend at most. */\n ceiling: number\n}\n\nexport const AI_CREDIT_RANGE_ZERO: AiCreditRange = { likely: 0, p90: 0, ceiling: 0 }\n\n/** Two ranges added. */\nexport function aiCreditRangeAdd(a: AiCreditRange, b: AiCreditRange): AiCreditRange {\n return { likely: a.likely + b.likely, p90: a.p90 + b.p90, ceiling: a.ceiling + b.ceiling }\n}\n\n/** A measured cost as a range with its own ceiling, the likely and p90 never above it. */\nexport function aiCreditRangeOf(measured: AiMeasuredCredits, ceiling: number): AiCreditRange {\n const top = Math.max(0, Math.ceil(ceiling))\n return { likely: Math.min(measured.median, top), p90: Math.min(measured.p90, top), ceiling: top }\n}\n\n/** A range whose likely and p90 are each at least the last, and at most the ceiling. */\nexport function aiCreditRangeOrdered(range: AiCreditRange): AiCreditRange {\n const ceiling = Math.max(0, Math.ceil(range.ceiling))\n const likely = Math.min(Math.max(0, Math.ceil(range.likely)), ceiling)\n const p90 = Math.min(Math.max(likely, Math.ceil(range.p90)), ceiling)\n return { likely, p90, ceiling }\n}\n\nconst n = (value: number) => value.toLocaleString('en-US')\n\n/**\n * The range as every surface quotes it: \"About 106 credits (up to 650)\".\n * A job whose likely figure is its ceiling says one number.\n */\nexport function aiCreditRangeText(range: AiCreditRange): string {\n if (range.likely >= range.ceiling) return `About ${n(range.ceiling)} credits`\n return `About ${n(range.likely)} credits (up to ${n(range.ceiling)})`\n}\n\n/**\n * Whether a Free job of this range needs the person's go-ahead before it\n * starts (AGL-3722): its p90 is more than what is left. A job whose p90\n * fits starts with no prompt; one that does not starts only when confirmed,\n * builds as much as what is left pays for, and pauses with Resume.\n */\nexport function aiCreditsNeedConfirm(range: Pick<AiCreditRange, 'p90'>, left: number): boolean {\n return Number.isFinite(left) && range.p90 > Math.max(0, Math.floor(left))\n}\n\n/** The smaller first build a Free job that does not fit is offered (AGL-3722). */\nexport interface AiCreditsSmaller {\n /** \"Build the home page first\". */\n label: string\n likely: number\n p90: number\n ceiling: number\n}\n\n/**\n * What a Free job that does not fit what is left says before it starts, and\n * the choices it offers: build what fits, a smaller first build, or Upgrade.\n * The door that refuses an unconfirmed start answers with it (status 409,\n * `code: AI_CREDITS_CONFIRM_CODE`), and the plan card and the guided start\n * draw it from the same figures.\n */\nexport interface AiCreditsPrompt extends AiCreditRange {\n left: number\n resetsOn: string\n smaller: AiCreditsSmaller | null\n}\n\n/** The `code` a door answers an unconfirmed Free start with. */\nexport const AI_CREDITS_CONFIRM_CODE = 'credits-confirm'\n\n/** \"Build what fits\": the go-ahead for a Free job past what is left. */\nexport const AI_CREDITS_BUILD_WHAT_FITS = 'Build what fits'\n\n/** The smaller first build's button: \"Build the home page first (about 64 credits)\". */\nexport function aiCreditsSmallerText(smaller: Pick<AiCreditsSmaller, 'label' | 'likely'>): string {\n return `${smaller.label} (about ${n(smaller.likely)} credits)`\n}\n\n/** Which way on a prompt offers, in the order it offers them. */\nexport type AiCreditsChoiceKey = 'fits' | 'smaller' | 'upgrade'\n\n/** One way on, as its option button reads it: a short label and the figure under it (AGL-3722). */\nexport interface AiCreditsChoice {\n key: AiCreditsChoiceKey\n label: string\n detail: string\n /** The one choice drawn as the primary button. */\n recommended: boolean\n}\n\nconst credit = (count: number) => `${n(count)} ${count === 1 ? 'credit' : 'credits'}`\n\n/**\n * The prompt's ways on, as its three option buttons read them (Zach,\n * 2026-10-10: \"more appealing and consistent\"): build what fits, the smaller\n * first build when there is one, and Upgrade when it can be offered — each a\n * short sentence-case label with its figure under it. The smaller build is\n * the recommended one when its p90 fits what is left, since it then finishes;\n * otherwise building what fits is.\n */\nexport function aiCreditsChoices(\n prompt: Pick<AiCreditsPrompt, 'left' | 'smaller'>,\n offers: { smaller: boolean; upgrade: boolean },\n): AiCreditsChoice[] {\n const left = Math.max(0, Math.floor(prompt.left))\n const smaller = offers.smaller ? prompt.smaller : null\n const smallerFits = smaller !== null && smaller.p90 <= left\n const choices: AiCreditsChoice[] = [\n {\n key: 'fits',\n label: AI_CREDITS_BUILD_WHAT_FITS,\n detail: `Uses your ${credit(left)} left, then pauses`,\n recommended: !smallerFits,\n },\n ]\n if (smaller) {\n choices.push({\n key: 'smaller',\n label: smaller.label,\n detail: smallerFits ? `About ${credit(smaller.likely)}, fits what you have left` : `About ${credit(smaller.likely)}`,\n recommended: smallerFits,\n })\n }\n if (offers.upgrade) choices.push({ key: 'upgrade', label: 'Upgrade', detail: 'Get more credits', recommended: false })\n return choices\n}\n\n/** A reset day as a person reads it: \"November 1\". */\nfunction resetLabel(resetsOn: string): string {\n const at = new Date(`${resetsOn}T00:00:00.000Z`)\n return Number.isFinite(at.getTime())\n ? at.toLocaleDateString('en-US', { month: 'long', day: 'numeric', timeZone: 'UTC' })\n : 'the first of next month'\n}\n\n/**\n * The plain sentence (Zach, 2026-10-09): \"This build is about N credits (up\n * to M). You have L left, so it will build as much as it can and pause when\n * your credits run out. You can upgrade or resume when they renew.\"\n */\nexport function aiCreditsPromptText(prompt: AiCreditsPrompt, noun: 'build' | 'site' = 'build'): string {\n const size =\n prompt.likely >= prompt.ceiling\n ? `about ${n(prompt.ceiling)} credits`\n : `about ${n(prompt.likely)} credits (up to ${n(prompt.ceiling)})`\n const left = Math.max(0, Math.floor(prompt.left))\n return (\n `This ${noun} is ${size}. You have ${n(left)} left, so it will build as much as it can and pause ` +\n `when your credits run out. You can upgrade or resume when they renew on ${resetLabel(prompt.resetsOn)}.`\n )\n}\n\n/**\n * The record a confirmed Free start keeps (AGL-3722): who said go, when,\n * what was left then and the range they were shown.\n */\nexport interface AiJobCreditsConfirmation extends AiCreditRange {\n by: string\n at: Date | string\n left: number\n}\n\n/** The confirmation a door keeps on the job for the prompt the person confirmed, or `null` when there was none. */\nexport function aiCreditsConfirmation(\n prompt: AiCreditsPrompt | null,\n by: string,\n at: Date,\n): AiJobCreditsConfirmation | null {\n if (!prompt) return null\n return { by, at, left: prompt.left, likely: prompt.likely, p90: prompt.p90, ceiling: prompt.ceiling }\n}\n\n/** The prompt a range needs against what is left, or `null` when it fits. */\nexport function aiCreditsPromptFor(\n range: AiCreditRange,\n credits: { left: number; resetsOn: string } | null | undefined,\n smaller: AiCreditsSmaller | null = null,\n): AiCreditsPrompt | null {\n if (!credits || !aiCreditsNeedConfirm(range, credits.left)) return null\n return { ...range, left: Math.max(0, Math.floor(credits.left)), resetsOn: credits.resetsOn, smaller }\n}\n"],"names":["AI_MEASURED_UNIT_CREDITS","page","median","p90","form","layout","theme","plan","AI_MEASURED_PAID_SITE_CREDITS","email","products","AI_MEASURED_IMPORT_CREDITS","frame","AI_MEASURED_AT","read","since","AI_MEASURED_FREE_SITE_CREDITS","low","high","AI_MEASURED_PAGE_SECTIONS","aiMeasuredPageCredits","sections","count","Math","max","floor","Number","isFinite","ceil","AI_MEASURED_STANDALONE_PAGE","fixed","perSection","aiMeasuredStandalonePageCredits","AI_MEASURED_STANDALONE_CREATION_CREDITS","AI_MEASURED_PASS_CREDITS","AI_CREDIT_RANGE_ZERO","likely","ceiling","aiCreditRangeAdd","a","b","aiCreditRangeOf","measured","top","min","aiCreditRangeOrdered","range","n","value","toLocaleString","aiCreditRangeText","aiCreditsNeedConfirm","left","AI_CREDITS_CONFIRM_CODE","AI_CREDITS_BUILD_WHAT_FITS","aiCreditsSmallerText","smaller","label","credit","aiCreditsChoices","prompt","offers","smallerFits","choices","key","detail","recommended","push","upgrade","resetLabel","resetsOn","at","Date","getTime","toLocaleDateString","month","day","timeZone","aiCreditsPromptText","noun","size","aiCreditsConfirmation","by","aiCreditsPromptFor","credits"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-credit-estimate.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * WHAT A JOB REALLY COSTS, NEXT TO WHAT IT CAN COST (AGL-3722).\n *\n * Every job's cost used to be quoted at its CEILING: each pass at the\n * `AI_SITE_PASS_CREDITS` (50) a pass is reserved against. A Free Assist\n * build of two pages, a quote form and a layout was quoted 600 credits and\n * refused with 164 left, when builds like it measure about a fifth of that.\n *\n * So a job is now quoted as a RANGE: what it is likely to cost (the sum of\n * the measured medians of what it builds), what it costs nine times in ten\n * (the measured p90s), and its ceiling (the old figure, unchanged). The\n * meter still charges actual use, and the reservation each pass takes is\n * still the ceiling's 50 — only the words and the Free admission move:\n * a Free job is admitted outright when its p90 fits what is left, and asked\n * to be confirmed when it does not.\n *\n * This module is pure: the console, the doors and the specs read one table.\n */\n\n/** One measured cost: the median and the 90th percentile, in credits. */\nexport interface AiMeasuredCredits {\n median: number\n p90: number\n}\n\n/**\n * MEASURED IN PRODUCTION, credits per unit, over every `done` AI job since\n * 2026-10-01 (read 2026-10-09; a whole Free site job came to 148–160):\n *\n * page item 30 / 40 (a page of about `AI_MEASURED_PAGE_SECTIONS` sections, its listing included)\n * form 29 / 31\n * layout 23 / 24 (header and footer, one answer)\n * theme 4 / 8 (a site start's look)\n * plan step 21 / 70\n *\n * THE FAST TIER'S PART, REPRICED (AGL-3781). A site start's look, a Free\n * start's plan and every page's listing run on the fast tier, measured above\n * on Claude Haiku 4.5 at its cost. They run on Claude Haiku 5.5 at the\n * platform's markup now, so those parts are repriced from what production\n * sent, at the new rates and about 30% more tokens: over October 1–14 a look\n * 4 / 7 → 1 / 2, a Free start's plan 15 / 22 → 3 / 5, a listing 3 / 5 → 1 / 1\n * (2 less on every page), and a whole two-page Free site 148 → 121 at the\n * median. The other rows ran on the balanced tier and do not move.\n *\n * HOW TO RE-MEASURE: a read-only collection-group read of `aiJobs` in\n * production where `status == 'done'` and `createdAt` is in the window; group\n * each ledger row's (`items[]`) `creditsSpent` by its `op` (the site start's\n * look is the `theme` row), take the plan step's `steps[].creditsSpent` for\n * `plan`, and each job's `creditsSpent` for the whole-site band. Put each\n * group's median and 90th percentile here, and the dates in\n * `AI_MEASURED_AT`. `ai-credit-estimate.spec.ts` holds the table, the page\n * scaling and the measured whole-site band together, so a re-measure that\n * moves one moves the spec with it.\n */\nexport const AI_MEASURED_UNIT_CREDITS = {\n // 30 / 40 with its listing at Haiku 4.5's cost.\n page: { median: 28, p90: 38 },\n form: { median: 29, p90: 31 },\n layout: { median: 23, p90: 24 },\n // 4 / 8 on Haiku 4.5.\n theme: { median: 1, p90: 2 },\n // 21 / 70: the median is a Free start's plan, on the fast tier; the p90 a\n // plan on the balanced tier, which does not move.\n plan: { median: 4, p90: 70 },\n} as const satisfies Record<string, AiMeasuredCredits>\n\n/**\n * MEASURED IN PRODUCTION, credits per unit of a PAID site start, whose plan\n * and parts run on the paid routing (Sonnet) rather than a Free start's\n * (Haiku) the table above is mostly made of: every `done` paid site job\n * since 2026-10-01, read 2026-10-12 (6 jobs; products from the 4 stores; AGL-3739).\n *\n * plan step 81 / 86 (a Free start's plan: 20 / 42 on Haiku 4.5, 3 / 5 repriced)\n * page item 29 / 49 (about `AI_MEASURED_PAGE_SECTIONS` sections; 27 / 47 with its listing repriced)\n * form 32 / 39\n * layout 29 / 29\n * theme 4 / 4 (1 / 1 repriced)\n * email 49 / 51\n * products 64 / 66 (a store's first products, one answer)\n *\n * A 5-page store with its welcome email was quoted about 277 off the table\n * above: its plan at a Free start's 21 where paid plans run about 81, and its\n * first products not counted at all where they run about 64. Those stores\n * came to 413-448; this table quotes one at about 404.\n *\n * Re-measure as the table above says, keeping only jobs whose plan step's\n * `tokens.model` is the paid routing's.\n */\nexport const AI_MEASURED_PAID_SITE_CREDITS = {\n plan: { median: 81, p90: 86 },\n page: { median: 27, p90: 47 },\n form: { median: 32, p90: 39 },\n layout: { median: 29, p90: 29 },\n theme: { median: 1, p90: 1 },\n email: { median: 49, p90: 51 },\n products: { median: 64, p90: 66 },\n} as const satisfies Record<string, AiMeasuredCredits>\n\n/**\n * A page brought in from a person's own website (AGL-3976), priced as an AI\n * page: a paid site start's page as measured above, since an imported page is\n * one answer in the layout language and its listing, written inside one job\n * the way a site start writes its pages. Its site's header and footer, built\n * once in an import's first wave, is that table's layout.\n *\n * Re-measure from done `import` jobs' item ledgers once production has run\n * them: their `page` rows' `creditsSpent`, as the paid table is read.\n */\nexport const AI_MEASURED_IMPORT_CREDITS = {\n page: AI_MEASURED_PAID_SITE_CREDITS.page,\n frame: AI_MEASURED_PAID_SITE_CREDITS.layout,\n /**\n * A main page Aglyn AI arranges from its old page's own lines (AGL-4083):\n * one frame-sized structured answer, no prose, and its listing.\n * PROVISIONAL until the first live import measures it; it prices nothing\n * until the step marks a page `arranged`.\n */\n arranged: { median: 12, p90: 20 },\n /** A page copied as it is (AGL-4083): its listing pass alone. Provisional, as `arranged`. */\n copied: { median: 1, p90: 2 },\n} as const satisfies Record<string, AiMeasuredCredits>\n\n/**\n * When the table above was read, and from when its jobs ran. Both tables\n * were measured before AGL-3781 asked a paid site's plan at medium effort and\n * the layout language's pages and frames at low. A lower effort draws less,\n * so their plan, page and layout figures stand as the higher reading until\n * the next re-measure: a quote is never under what is drawn.\n */\nexport const AI_MEASURED_AT = { read: '2026-10-09', since: '2026-10-01' } as const\n\n/**\n * What a whole Free two-page site job measured at, in credits, in the same\n * window: 148–160 with its plan, look and listings at Haiku 4.5's cost, 27\n * less at the median with them repriced (AGL-3781).\n */\nexport const AI_MEASURED_FREE_SITE_CREDITS = { low: 121, high: 133 } as const\n\n/** The sections the measured page item is about: a page is scaled around it. */\nexport const AI_MEASURED_PAGE_SECTIONS = 5\n\n/**\n * What a page of this many sections is likely to cost: the measured page\n * scaled by its sections around the ~5-section median — about 6 credits a\n * section, 8 at the p90.\n */\nexport function aiMeasuredPageCredits(sections: number): AiMeasuredCredits {\n const count = Math.max(1, Math.floor(Number.isFinite(sections) ? sections : AI_MEASURED_PAGE_SECTIONS))\n const { median, p90 } = AI_MEASURED_UNIT_CREDITS.page\n return {\n median: Math.ceil((median * count) / AI_MEASURED_PAGE_SECTIONS),\n p90: Math.ceil((p90 * count) / AI_MEASURED_PAGE_SECTIONS),\n }\n}\n\n/**\n * What a page costs when it is written ON ITS OWN — a page an Assist build or\n * a page job writes, rather than one of a site start's pages (AGL-3722).\n *\n * The table's page item is a site start's: its pages are written one after\n * another inside one job, the later ones reading the prompt the first one\n * paid to write, so a 2-section second page measured 19–28. A page written on\n * its own pays that prompt itself and reads the site it lands on, and costs\n * what a site start's FIRST page costs: a 5-section home page measured 39–78\n * (median 40, p90 78), standalone page jobs' writing step 82–105 for a\n * 2-section page, and the Assist build pages of 2026-10-09 22–89 each. Quoting\n * a build's pages at the site start's scaled median told a Free workspace\n * \"about 48 credits\" for two 4-section pages (AGL-3722, 2026-10-10).\n *\n * So it is priced as a fixed cost — the prompt, the site, the listing — and a\n * cost per section: a 4-section page about 38 (p90 76), a 5-section 44 (p90 88).\n * The fixed part was 16 / 30 with its listing at Haiku 4.5's cost (AGL-3781).\n */\nexport const AI_MEASURED_STANDALONE_PAGE = {\n fixed: { median: 14, p90: 28 },\n perSection: { median: 6, p90: 12 },\n} as const satisfies Record<string, AiMeasuredCredits>\n\n/** What a page of this many sections written on its own is likely to cost. */\nexport function aiMeasuredStandalonePageCredits(sections: number): AiMeasuredCredits {\n const count = Math.max(1, Math.floor(Number.isFinite(sections) ? sections : AI_MEASURED_PAGE_SECTIONS))\n const { fixed, perSection } = AI_MEASURED_STANDALONE_PAGE\n return {\n median: fixed.median + perSection.median * count,\n p90: fixed.p90 + perSection.p90 * count,\n }\n}\n\n/**\n * What a creation costs when it is written ON ITS OWN (AGL-3722), where it\n * measured apart from a site start's: an Assist build's layout came to 24–28\n * (2026-10-09/10), against a site start's 23/24 written in its warm prompt.\n * A form measured the same either way (25–30 on its own, 29/31 in a site\n * start), so it keeps the table's figure.\n */\nexport const AI_MEASURED_STANDALONE_CREATION_CREDITS = {\n layout: { median: 26, p90: 30 },\n} as const satisfies Record<string, AiMeasuredCredits>\n\n/**\n * What one pass of a kind with no measurement of its own is likely to cost\n * (a component, an email design, a template, an item another runner\n * builds), written on its own: a component job's writing step measured\n * 52–65, a form job's 37, a layout job's 63, and the Assist build of\n * 2026-10-09 a welcome email at 46 and a products step at 60. Until\n * 2026-10-10 this was a site start's form (29/31), which is written inside a\n * site job's warm prompt and is the cheapest single answer there is.\n */\nexport const AI_MEASURED_PASS_CREDITS: AiMeasuredCredits = { median: 50, p90: 65 }\n\n/** A job's cost as it is quoted: likely, nine times in ten, and at its worst. */\nexport interface AiCreditRange {\n /** The sum of the measured medians: what the job is likely to spend. */\n likely: number\n /** The sum of the measured p90s: what a Free job is admitted on. */\n p90: number\n /** Every pass at its reserve: what it can spend at most. */\n ceiling: number\n}\n\nexport const AI_CREDIT_RANGE_ZERO: AiCreditRange = { likely: 0, p90: 0, ceiling: 0 }\n\n/** Two ranges added. */\nexport function aiCreditRangeAdd(a: AiCreditRange, b: AiCreditRange): AiCreditRange {\n return { likely: a.likely + b.likely, p90: a.p90 + b.p90, ceiling: a.ceiling + b.ceiling }\n}\n\n/** A measured cost as a range with its own ceiling, the likely and p90 never above it. */\nexport function aiCreditRangeOf(measured: AiMeasuredCredits, ceiling: number): AiCreditRange {\n const top = Math.max(0, Math.ceil(ceiling))\n return { likely: Math.min(measured.median, top), p90: Math.min(measured.p90, top), ceiling: top }\n}\n\n/** A range whose likely and p90 are each at least the last, and at most the ceiling. */\nexport function aiCreditRangeOrdered(range: AiCreditRange): AiCreditRange {\n const ceiling = Math.max(0, Math.ceil(range.ceiling))\n const likely = Math.min(Math.max(0, Math.ceil(range.likely)), ceiling)\n const p90 = Math.min(Math.max(likely, Math.ceil(range.p90)), ceiling)\n return { likely, p90, ceiling }\n}\n\nconst n = (value: number) => value.toLocaleString('en-US')\n\n/**\n * The range as every surface quotes it: \"About 106 credits (up to 650)\".\n * A job whose likely figure is its ceiling says one number.\n */\nexport function aiCreditRangeText(range: AiCreditRange): string {\n if (range.likely >= range.ceiling) return `About ${n(range.ceiling)} credits`\n return `About ${n(range.likely)} credits (up to ${n(range.ceiling)})`\n}\n\n/**\n * Whether a Free job of this range needs the person's go-ahead before it\n * starts (AGL-3722): its p90 is more than what is left. A job whose p90\n * fits starts with no prompt; one that does not starts only when confirmed,\n * builds as much as what is left pays for, and pauses with Resume.\n */\nexport function aiCreditsNeedConfirm(range: Pick<AiCreditRange, 'p90'>, left: number): boolean {\n return Number.isFinite(left) && range.p90 > Math.max(0, Math.floor(left))\n}\n\n/** The smaller first build a Free job that does not fit is offered (AGL-3722). */\nexport interface AiCreditsSmaller {\n /** \"Build the home page first\". */\n label: string\n likely: number\n p90: number\n ceiling: number\n}\n\n/**\n * What a Free job that does not fit what is left says before it starts, and\n * the choices it offers: build what fits, a smaller first build, or Upgrade.\n * The door that refuses an unconfirmed start answers with it (status 409,\n * `code: AI_CREDITS_CONFIRM_CODE`), and the plan card and the guided start\n * draw it from the same figures.\n */\nexport interface AiCreditsPrompt extends AiCreditRange {\n left: number\n resetsOn: string\n smaller: AiCreditsSmaller | null\n}\n\n/** The `code` a door answers an unconfirmed Free start with. */\nexport const AI_CREDITS_CONFIRM_CODE = 'credits-confirm'\n\n/** \"Build what fits\": the go-ahead for a Free job past what is left. */\nexport const AI_CREDITS_BUILD_WHAT_FITS = 'Build what fits'\n\n/** The smaller first build's button: \"Build the home page first (about 64 credits)\". */\nexport function aiCreditsSmallerText(smaller: Pick<AiCreditsSmaller, 'label' | 'likely'>): string {\n return `${smaller.label} (about ${n(smaller.likely)} credits)`\n}\n\n/** Which way on a prompt offers, in the order it offers them. */\nexport type AiCreditsChoiceKey = 'fits' | 'smaller' | 'upgrade'\n\n/** One way on, as its option button reads it: a short label and the figure under it (AGL-3722). */\nexport interface AiCreditsChoice {\n key: AiCreditsChoiceKey\n label: string\n detail: string\n /** The one choice drawn as the primary button. */\n recommended: boolean\n}\n\nconst credit = (count: number) => `${n(count)} ${count === 1 ? 'credit' : 'credits'}`\n\n/**\n * The prompt's ways on, as its three option buttons read them (Zach,\n * 2026-10-10: \"more appealing and consistent\"): build what fits, the smaller\n * first build when there is one, and Upgrade when it can be offered — each a\n * short sentence-case label with its figure under it. The smaller build is\n * the recommended one when its p90 fits what is left, since it then finishes;\n * otherwise building what fits is.\n */\nexport function aiCreditsChoices(\n prompt: Pick<AiCreditsPrompt, 'left' | 'smaller'>,\n offers: { smaller: boolean; upgrade: boolean },\n): AiCreditsChoice[] {\n const left = Math.max(0, Math.floor(prompt.left))\n const smaller = offers.smaller ? prompt.smaller : null\n const smallerFits = smaller !== null && smaller.p90 <= left\n const choices: AiCreditsChoice[] = [\n {\n key: 'fits',\n label: AI_CREDITS_BUILD_WHAT_FITS,\n detail: `Uses your ${credit(left)} left, then pauses`,\n recommended: !smallerFits,\n },\n ]\n if (smaller) {\n choices.push({\n key: 'smaller',\n label: smaller.label,\n detail: smallerFits ? `About ${credit(smaller.likely)}, fits what you have left` : `About ${credit(smaller.likely)}`,\n recommended: smallerFits,\n })\n }\n if (offers.upgrade) choices.push({ key: 'upgrade', label: 'Upgrade', detail: 'Get more credits', recommended: false })\n return choices\n}\n\n/** A reset day as a person reads it: \"November 1\". */\nfunction resetLabel(resetsOn: string): string {\n const at = new Date(`${resetsOn}T00:00:00.000Z`)\n return Number.isFinite(at.getTime())\n ? at.toLocaleDateString('en-US', { month: 'long', day: 'numeric', timeZone: 'UTC' })\n : 'the first of next month'\n}\n\n/**\n * The plain sentence (Zach, 2026-10-09): \"This build is about N credits (up\n * to M). You have L left, so it will build as much as it can and pause when\n * your credits run out. You can upgrade or resume when they renew.\"\n */\nexport function aiCreditsPromptText(prompt: AiCreditsPrompt, noun: 'build' | 'site' = 'build'): string {\n const size =\n prompt.likely >= prompt.ceiling\n ? `about ${n(prompt.ceiling)} credits`\n : `about ${n(prompt.likely)} credits (up to ${n(prompt.ceiling)})`\n const left = Math.max(0, Math.floor(prompt.left))\n return (\n `This ${noun} is ${size}. You have ${n(left)} left, so it will build as much as it can and pause ` +\n `when your credits run out. You can upgrade or resume when they renew on ${resetLabel(prompt.resetsOn)}.`\n )\n}\n\n/**\n * The record a confirmed Free start keeps (AGL-3722): who said go, when,\n * what was left then and the range they were shown.\n */\nexport interface AiJobCreditsConfirmation extends AiCreditRange {\n by: string\n at: Date | string\n left: number\n}\n\n/** The confirmation a door keeps on the job for the prompt the person confirmed, or `null` when there was none. */\nexport function aiCreditsConfirmation(\n prompt: AiCreditsPrompt | null,\n by: string,\n at: Date,\n): AiJobCreditsConfirmation | null {\n if (!prompt) return null\n return { by, at, left: prompt.left, likely: prompt.likely, p90: prompt.p90, ceiling: prompt.ceiling }\n}\n\n/** The prompt a range needs against what is left, or `null` when it fits. */\nexport function aiCreditsPromptFor(\n range: AiCreditRange,\n credits: { left: number; resetsOn: string } | null | undefined,\n smaller: AiCreditsSmaller | null = null,\n): AiCreditsPrompt | null {\n if (!credits || !aiCreditsNeedConfirm(range, credits.left)) return null\n return { ...range, left: Math.max(0, Math.floor(credits.left)), resetsOn: credits.resetsOn, smaller }\n}\n"],"names":["AI_MEASURED_UNIT_CREDITS","page","median","p90","form","layout","theme","plan","AI_MEASURED_PAID_SITE_CREDITS","email","products","AI_MEASURED_IMPORT_CREDITS","frame","arranged","copied","AI_MEASURED_AT","read","since","AI_MEASURED_FREE_SITE_CREDITS","low","high","AI_MEASURED_PAGE_SECTIONS","aiMeasuredPageCredits","sections","count","Math","max","floor","Number","isFinite","ceil","AI_MEASURED_STANDALONE_PAGE","fixed","perSection","aiMeasuredStandalonePageCredits","AI_MEASURED_STANDALONE_CREATION_CREDITS","AI_MEASURED_PASS_CREDITS","AI_CREDIT_RANGE_ZERO","likely","ceiling","aiCreditRangeAdd","a","b","aiCreditRangeOf","measured","top","min","aiCreditRangeOrdered","range","n","value","toLocaleString","aiCreditRangeText","aiCreditsNeedConfirm","left","AI_CREDITS_CONFIRM_CODE","AI_CREDITS_BUILD_WHAT_FITS","aiCreditsSmallerText","smaller","label","credit","aiCreditsChoices","prompt","offers","smallerFits","choices","key","detail","recommended","push","upgrade","resetLabel","resetsOn","at","Date","getTime","toLocaleDateString","month","day","timeZone","aiCreditsPromptText","noun","size","aiCreditsConfirmation","by","aiCreditsPromptFor","credits"],"mappings":"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"}
|
|
@@ -284,7 +284,19 @@ export type AiJobReviewReason =
|
|
|
284
284
|
* the site (AGL-3976): the proof is older than its 30 days, or was never
|
|
285
285
|
* made. Verifying again and starting the wave runs it.
|
|
286
286
|
*/
|
|
287
|
-
| 'ownership'
|
|
287
|
+
| 'ownership'
|
|
288
|
+
/**
|
|
289
|
+
* A site import's read found its home page turned away by the site's bot
|
|
290
|
+
* protection or firewall (AGL-4083): nothing is planned or spent. Letting
|
|
291
|
+
* AglynBot in and choosing Check again reads the site again.
|
|
292
|
+
*/
|
|
293
|
+
| 'blocked'
|
|
294
|
+
/**
|
|
295
|
+
* A site import's read could not read its home page for any other reason
|
|
296
|
+
* (AGL-4083): an error, no answer, or a page that needs a browser to show
|
|
297
|
+
* its words. Nothing is planned or spent; Check again reads it again.
|
|
298
|
+
*/
|
|
299
|
+
| 'unreadable';
|
|
288
300
|
/**
|
|
289
301
|
* A building rule an answer broke, as the job keeps it: the number, the code,
|
|
290
302
|
* the sentence, and where in the answer it was broken (AGL-3078).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-jobs.types.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 { ITimestamp } from '@aglyn/shared-util-timestamp'\nimport type { AiEffort } from '../providers/contract'\nimport type { AiLoadEstimate } from '../runtime/ai-palette'\nimport type { AiBuildPlan } from './ai-build-plan'\nimport type { AiFreeCreditsLeft } from './ai-site-job'\nimport type { AiJobCreditsConfirmation } from './ai-credit-estimate'\nimport type { AiSiteImportState } from './ai-site-import'\nimport type { AiSitePhotos } from './ai-site-photos'\nimport type { AiSiteInspiration } from '../jobs/ai-site-inspiration-signals'\n\n/**\n * AI generation jobs (AGL-2904).\n *\n * A job is one customer brief carried through one or more model steps by a\n * Firestore state machine, so that work longer than a request can survive\n * the request: the console route runs what it can inline, and the platform\n * job beat resumes whatever is left. The document is the whole state — no\n * process holds anything the next beat cannot read back.\n *\n * ── Outputs are drafts, never a publish ──────────────────────────────────\n *\n * Every output a job produces is a NEW draft or a new unpublished version of\n * an existing resource. A job never flips a version pointer, never registers\n * a route and never sends anything; a person opens the draft the job names\n * and publishes it through the door that already exists for that resource.\n * That is what lets a job run with no visitor, no session and no confirm\n * gate — the thing it writes cannot be seen by anyone but the workspace\n * until a member chooses otherwise.\n *\n * ── The lease ────────────────────────────────────────────────────────────\n *\n * A step runs under a lease: an owner id and an `until` instant. Two beats\n * fire a minute apart against a 120 s function timeout, and a console route\n * may still be running the first step when the first beat looks, so the\n * lease is what stops the same step being run — and metered — twice. A\n * claim is a transaction that refuses while another owner's lease is live;\n * an expired lease is simply claimable, which is also how a step abandoned\n * by a frozen process is recovered.\n */\n\n/**\n * What a job produces. `text` and `theme` have runners; every other kind is\n * registered and refuses with \"not available yet\" until its own issue lands,\n * so a kind's presence in the union says the job model accepts it, not that\n * a runner exists.\n */\nexport type AiJobKind =\n | 'page'\n | 'component'\n | 'layout'\n | 'template'\n | 'edit'\n | 'seo'\n | 'site'\n | 'email'\n | 'campaign'\n | 'form'\n | 'experiment'\n | 'insight'\n | 'products'\n | 'crm'\n | 'onboarding'\n | 'workflow'\n | 'logic'\n | 'text'\n | 'theme'\n | 'build'\n /** A change to a site's header or footer menu, made by code (AGL-3728). */\n | 'navigation'\n /** A copy of something a site already has, made by core's duplicate (AGL-3740). */\n | 'duplicate'\n /** A change to a site's settings, made by code and saved live (AGL-3748). */\n | 'settings'\n /** Folders, tags, alt text and names in a site's media library, made by code and saved live (AGL-3752). */\n | 'media'\n /** A change to a site's analytics and advertising tags, made by code and saved live (AGL-3751). */\n | 'tracking'\n /** A change to who can see a site — maintenance mode, search indexing, page visibility — made by code and saved live (AGL-4033). */\n | 'privacy'\n /** A person's own website brought in, page by page, in reviewed waves (AGL-3976). */\n | 'import'\n\n/** The union as a value, so a route can validate a body against it. */\nexport const AI_JOB_KINDS: readonly AiJobKind[] = [\n 'page',\n 'component',\n 'layout',\n 'template',\n 'edit',\n 'seo',\n 'site',\n 'email',\n 'campaign',\n 'form',\n 'experiment',\n 'insight',\n 'products',\n 'crm',\n 'onboarding',\n 'workflow',\n 'logic',\n 'text',\n 'theme',\n 'build',\n 'navigation',\n 'duplicate',\n 'settings',\n 'media',\n 'tracking',\n 'privacy',\n 'import',\n]\n\n/**\n * `queued` — waiting for a runner; `running` — a step holds the lease;\n * `needs_input` — the job stopped for something only the workspace can\n * change (credits, a cap, a switch), and the beat tries it again once that\n * may have changed — or, carrying `input`, for an answer only the person can\n * give (AGL-3978), which the beat leaves alone; `needs_review` — the job stopped for a person's decision\n * (AGL-2935): its plan is ready to confirm, or an answer broke a building\n * rule on its re-ask. No timer can make that decision, so neither the beat\n * nor anything but that person resumes it. The last three are terminal.\n */\nexport type AiJobStatus =\n | 'queued'\n | 'running'\n | 'needs_input'\n | 'needs_review'\n | 'done'\n | 'failed'\n | 'canceled'\n\nexport const AI_JOB_TERMINAL_STATUSES: readonly AiJobStatus[] = [\n 'done',\n 'failed',\n 'canceled',\n]\n\nexport type AiJobStepStatus = 'pending' | 'running' | 'done' | 'failed'\n\n/** The step a planned kind runs first (AGL-2935): its plan, before anything is built. */\nexport const AI_JOB_PLAN_STEP = 'plan'\n\nexport interface AiJobStep {\n /** Stable within the job (`plan`, `draft`, `generate`), unique per job. */\n name: string\n status: AiJobStepStatus\n startedAt?: ITimestamp | null\n endedAt?: ITimestamp | null\n /** Credits this step has actually cost, at the plan's credit rate. */\n creditsSpent: number\n /** Customer-safe only; provider detail goes to the server log. */\n error?: string | null\n /**\n * How many times a runner has been handed this step. A retryable provider\n * failure hands the step back as `pending`; the count is what stops a\n * provider outage re-running a step forever.\n */\n attempts?: number\n /**\n * Passes of a step that works through a list (AGL-2910): each one a\n * recorded, metered exchange that asked to continue. Attempts count again\n * from zero after a pass; this count is what bounds the step.\n */\n passes?: number\n /**\n * What the step's model calls cost in tokens and time (AGL-2937), summed\n * over every run that reached the provider. Absent on a step none has.\n */\n tokens?: AiJobStepTokens\n /**\n * The ids the step writes its drafts under, by draft (AGL-3079): minted\n * when the job is created, so every run of the step names the same drafts.\n * Absent on a step that writes none; a step that records none names its\n * drafts by the job's own id.\n */\n draftIds?: Partial<Record<AiJobDraftSlot, string>>\n}\n\n/**\n * A draft a job's kind always writes (AGL-3079): a screen, layout, template,\n * form or component; an email design; a campaign; an automation.\n */\nexport type AiJobDraftSlot =\n | 'screen'\n | 'layout'\n | 'template'\n | 'form'\n | 'component'\n | 'email'\n | 'campaign'\n | 'workflow'\n\n/**\n * A step's measure (AGL-2937): the four token counts the meter prices, the\n * time spent in the runner, and the model and thinking effort of the last\n * run. The machine writes it beside `creditsSpent`, so what a kind of step\n * costs in tokens is read off the job rather than inferred from the month.\n */\nexport interface AiJobStepTokens {\n /** Prompt tokens sent uncached. */\n input: number\n /** Prompt tokens read from the prompt cache. */\n cachedRead: number\n /** Prompt tokens written to the prompt cache. */\n cacheWrite: number\n /** Tokens generated, thinking included. */\n output: number\n /** The model the last run was served by. */\n model: string | null\n /** The thinking effort the last run asked for; `null` when it named none. */\n effort: AiEffort | null\n /** Milliseconds spent in the runner, summed over runs. */\n latencyMs: number\n /** Runs that reached the provider. */\n runs: number\n /**\n * Why the step's latest runs stopped (AGL-3042), oldest first and the last\n * run last: each run's stop reason and the tokens it generated, so a pass\n * cut off at its ceiling reads off the job rather than out of the sums. At\n * most `AI_JOB_STEP_LAST_RUNS` are kept; absent on a step measured before.\n */\n lastRuns?: AiJobStepRunStop[]\n}\n\n/** One run of a step, by why it stopped (AGL-3042). */\nexport interface AiJobStepRunStop {\n /**\n * The stop reason of the run's last model call — `tool_use` or `end_turn`\n * for an answer, `max_tokens` for one cut off at its ceiling, `refusal` —\n * and `null` when the provider named none.\n */\n stopReason: string | null\n /** Tokens the run generated, over every model call it made. */\n output: number\n}\n\nexport type AiJobOutputResource =\n | 'screen'\n | 'reusableComponent'\n | 'layout'\n | 'template'\n | 'form'\n | 'emailScreen'\n | 'campaign'\n | 'product'\n | 'experiment'\n | 'workflow'\n | 'text'\n | 'theme'\n | 'seo'\n | 'insight'\n | 'crm'\n /** A draft another plugin wrote for a build (AGL-3616), named by `draftResource`. */\n | 'draft'\n | 'logic'\n | 'orgAutomation'\n /** A post a guided start wrote into the site's blog (AGL-3676): a content collection entry. */\n | 'entry'\n /** A site's settings a build changed (AGL-3748): saved live, on the host document. */\n | 'settings'\n /** Files in a site's media library a build organized (AGL-3752): saved live, on their documents. */\n | 'media'\n /** A site's tracking tags a build changed (AGL-3751): saved live, on the host document. */\n | 'tracking'\n /** Who can see a site, as a build changed it (AGL-4033): saved live, on the host and page documents. */\n | 'privacy'\n\n/**\n * One thing a job wrote. Addressed by resource and id so the console can\n * build the \"open draft\" link without knowing what the runner did; `text`\n * has no document of its own, so its body rides on the output itself.\n *\n * A `theme` output is a proposal rather than a document (AGL-2938). A site's\n * theme is fields on its host document, and writing those fields is the save\n * the Theme section's editor performs, which a job never does. So the change\n * set rides on the output as `proposal`, and a person puts it in the editor\n * and saves it there, or does not.\n *\n * An `seo` output is a proposal rather than a document (AGL-2910): search\n * listing values for a page or a product, or a site audit's findings and\n * fixes. A screen's listing is served from the screen document itself, so a\n * job never writes it; the values ride on the output as `proposal`, a person\n * puts them in the editor and saves them there, and an audit's content fixes\n * become new versions only when a person applies them.\n *\n * A `crm` output is a proposal too (AGL-2917): a record's summary and next\n * step, an email draft, or an import's column matches. The CRM's own task\n * form, stage route, composer and import drawer are the writes, and a person\n * makes each of them, or does not.\n *\n * A `logic` output is a proposal too (AGL-3603): a site function or variable\n * the Functions & Variables editor opens unsaved. Saving it is the editor's\n * write, which a person makes, or does not.\n *\n * An `orgAutomation` output is a proposal too (AGL-3603): one of the\n * workspace's automations, which the Org automations editor opens unsaved and\n * switched off. `hostId` is `null`: it belongs to no one site.\n */\nexport interface AiJobOutput {\n resource: AiJobOutputResource\n id: string\n /** The unpublished version the job created, where the resource has versions. */\n versionId?: string | null\n /** `null` for an output that belongs to the org rather than one site. */\n hostId: string | null\n /**\n * The site's subdomain, which is what a console URL names a site by: the\n * `[host]` segment resolves by subdomain, so a link built from `hostId`\n * names no site the console can open.\n */\n hostSubdomain?: string | null\n label: string\n /** The body of a `text` output. Absent on every other resource. */\n text?: string\n /**\n * The change set of an output a person applies rather than opens — a\n * `theme` or `seo` proposal — in the shape its job kind's runner defines.\n */\n proposal?: Record<string, unknown>\n /**\n * What the person decides next about this output, in customer-safe words:\n * a routing the draft could not store, or part of the brief it could not\n * build. Absent when there is nothing to decide.\n */\n note?: string | null\n /**\n * What a first visit is estimated to transfer, as the doctrine measured\n * the generated document (AGL-2935) — the weight the proposal shows before\n * anything is applied. Absent where the output is not a page's document.\n */\n load?: AiLoadEstimate | null\n /**\n * For a `draft` output (AGL-3616): the resource whose owner wrote it, as\n * its writer is registered on the resource-draft seam.\n */\n draftResource?: string\n}\n\nexport interface AiJobLease {\n /** The process that holds the step: a route request id or a beat id. */\n owner: string\n until: ITimestamp\n}\n\n/** Why a job stopped for a person (AGL-2935). */\nexport type AiJobReviewReason =\n /** The plan step proposed a plan; generation waits for it to be confirmed. */\n | 'plan'\n /** An answer broke a building rule, and so did the answer to the re-ask. */\n | 'doctrine'\n /**\n * The site is at an allowance its plan sets for what the step writes, such\n * as its shared layouts or its templates (AGL-2909). Freeing one, or a plan\n * that holds more, is the member's decision; trying again runs the step\n * again.\n */\n | 'limit'\n /**\n * A site import's wave is in (AGL-3976): its pages are drafts for the\n * person to review, and the next wave starts only when they say so. Or the\n * import has read the site and waits for the first wave to be started.\n */\n | 'wave'\n /**\n * A site import's next wave needs a current proof that the workspace owns\n * the site (AGL-3976): the proof is older than its 30 days, or was never\n * made. Verifying again and starting the wave runs it.\n */\n | 'ownership'\n\n/**\n * A building rule an answer broke, as the job keeps it: the number, the code,\n * the sentence, and where in the answer it was broken (AGL-3078).\n */\nexport interface AiJobRuleFinding {\n rule: number | null\n code: string\n /** Customer-safe. */\n message: string\n /** The nodes the finding names, by the ids the refused answer gave them. Absent when it names none. */\n nodeIds?: string[]\n /** The plan entries or answer fields it names, as `screens[0].sections[2]`. Absent when it names none. */\n paths?: string[]\n}\n\n/**\n * One node of the parts of a refused answer its findings name (AGL-3078), as\n * the review keeps it: the node's place, its element and the names of what it\n * sets, and never its copy. A Grid keeps the values of its layout props as\n * they were written, so a review can tell a shape the rules refuse from one\n * the model was asked to mend and did not.\n */\nexport interface AiJobReviewOutlineNode {\n /** The node's id, as the refused answer wrote it. */\n id: string\n /** How far below a node a finding names it sits: 0 for that node. */\n depth: number\n componentId: string\n /** The props it sets, by name only. */\n props: string[]\n /** The `sx` keys it sets, by name only; absent when it sets none. */\n sx?: string[]\n /** A Grid's layout props, `container`, `size`, `direction`, `spacing` and the rest, as written. */\n grid?: Record<string, string | number | boolean | null>\n /** Its children's element ids, in order. */\n children: string[]\n}\n\nexport interface AiJobReview {\n reason: AiJobReviewReason\n /** Customer-safe: what the person is asked to decide. */\n message: string\n /** The rules the last answer broke; empty for a plan. */\n findings: AiJobRuleFinding[]\n /**\n * The parts of the refused answer its findings name, each node with the\n * nodes below it (AGL-3078); absent when they name none. Kept with the job\n * for diagnosis, and shown to staff.\n */\n outline?: AiJobReviewOutlineNode[]\n /**\n * The checks' own sentence, where `message` was written for the member\n * instead (AGL-3594): rule numbers and all, for staff. Its presence is what\n * tells the drawer the findings are staff reading too.\n */\n detail?: string\n /**\n * Why trying again cannot work yet (AGL-3594) — the Free allowance left is\n * less than the step can cost — or absent when it can. The button is drawn\n * disabled with this beside it.\n */\n retryRefusal?: string\n /**\n * What a Free workspace had left when a build's plan was kept (AGL-3722),\n * so its plan card says, before Confirm, whether the build fits and offers\n * the go-ahead or a smaller first build when it does not. The confirm door\n * reads what is left again; this is what the card shows.\n */\n freeCredits?: AiFreeCreditsLeft\n}\n\n/**\n * One answer a job waits for (AGL-3978): an id only the person has, which the\n * operation that needs it describes — a tracking change's Meta pixel ID.\n */\nexport interface AiJobInputField {\n /** The plan item the answer fills, by its name in the plan. */\n item: string\n /** The item's argument the answer is written to. */\n arg: string\n /** The field's label, naming the provider: \"Meta pixel ID\". */\n label: string\n /** What an answer looks like, in the words of the card that sets it: \"8–20 digits\". */\n shape: string\n /** The pattern an answer is held to, as a RegExp's source. */\n pattern: string\n}\n\n/** What a job parked `needs_input` for a person asks (AGL-3978). */\nexport interface AiJobInputRequest {\n /** Customer-safe: the question, in the operation's own words. */\n message: string\n fields: AiJobInputField[]\n}\n\nexport type AiJobPlanStatus = 'proposed' | 'confirmed'\n\n/** The plan a job builds from, as the job keeps it once its plan step proposed it. */\nexport interface AiJobPlan extends AiBuildPlan {\n status: AiJobPlanStatus\n /** Names for the inventory ids the plan references, read when it was made. */\n labels: Record<string, string>\n proposedAt: ITimestamp\n confirmedAt: ITimestamp | null\n /** The member who confirmed it. */\n confirmedBy: string | null\n /**\n * A digest of the whole request that produced this plan (AGL-2937): the\n * job's kind, site, brief and scalar inputs, the model that answered, and\n * the prompt as it was rendered — the site inventory included. A later job\n * whose request hashes the same reuses this plan rather than paying for the\n * same answer again. A hash of the request, never of the answer, and no\n * part of the brief is recoverable from it.\n */\n key?: string\n /** The job this plan was reused from, when it was not asked for again. */\n reusedFrom?: string\n /**\n * What a site start read of the sites the person likes (AGL-3975): signals\n * only, each from a closed set or a hex color, which the plan was told the\n * structure of and the look pins the style of. Absent without them.\n */\n inspiration?: AiSiteInspiration\n}\n\n/**\n * Where one item of a `build` job stands (AGL-3616). A build settles item by\n * item: a failed item gives back its own spend and the rest of the plan still\n * runs, so the job keeps one row per item, written only by the machine's\n * `recordStep` — in the same transaction as the spend it accounts for — and\n * by the resume door's retry.\n */\nexport type AiJobItemStatus = 'pending' | 'running' | 'succeeded' | 'degraded' | 'failed' | 'skipped'\n\n/** Why an item failed: a give-back reason when it was ours, else the model's refusal or a review. */\nexport type AiJobItemFailureReason =\n | 'plan-refused'\n | 'doctrine-refused'\n | 'provider'\n | 'timeout'\n | 'step-failure'\n | 'unavailable'\n | 'refused'\n | 'review'\n\nexport interface AiJobItemFailure {\n /** Whether the failure was on our side: only then is the item's spend given back. */\n ours: boolean\n reason: AiJobItemFailureReason\n /** Customer-safe. */\n message: string\n /**\n * The checks' own sentence behind a `doctrine-refused` failure (AGL-3596),\n * rule numbers and all: kept on the job for staff and debugging, never on\n * the wire a customer reads.\n */\n detail?: string\n}\n\nexport interface AiJobItemLedger {\n /** The unit's slot within the plan: `c0` a creation, `p0` a page, `i0` an item. */\n slot: string\n /** The capability operation that builds it: `page`, `form`, or an owner's op. */\n op: string\n /** What a person reads: the page's title, the creation's or item's name. */\n label: string\n status: AiJobItemStatus\n /** Bumped by Try again; the refund key names it, so each attempt gives back once. */\n attempt: number\n /** What this item's passes cost, every attempt. */\n creditsSpent: number\n /** What this attempt's passes cost so far: what a failure of this attempt gives back. */\n attemptCredits?: number\n /** What was given back for this item's failures. */\n creditsRefunded: number\n /** The key the latest give-back for this item was made under. */\n refundKey?: string\n failure?: AiJobItemFailure | null\n /** The ids of the outputs this item produced. */\n outputs: string[]\n /** The slots whose failure changed how this item was built. */\n degradedBy?: string[]\n /** Customer-safe: what the person should know about this item. */\n note?: string | null\n /**\n * When this attempt settled, as an ISO string (AGL-3596): the moment the\n * next item started, which is what its progress row counts from. Absent\n * while the item is open, and on a ledger written before it was kept.\n */\n settledAt?: string | null\n}\n\n/** Whether a build's own planning was charged or given back (AGL-3616). */\nexport interface AiJobOrchestration {\n /** What the plan step spent. */\n creditsSpent: number\n /** `charged` once anything was delivered; `refunded` when nothing was. */\n settled: 'pending' | 'charged' | 'refunded'\n}\n\nexport interface AiJob {\n $id: string\n orgId: string\n hostId?: string | null\n kind: AiJobKind\n status: AiJobStatus\n /** The customer's brief, verbatim. Expires with the document. */\n brief: string\n /** Kind-specific inputs the runner reads (a screen id to edit, a tone). */\n inputs: Record<string, unknown>\n /**\n * The model the creator picked for the job's steps (AGL-2942), or `null`\n * for Auto. Each step honors it only where the plan and the allotment\n * allowlists allow it, and runs on the routing table otherwise.\n */\n model?: string | null\n steps: AiJobStep[]\n outputs: AiJobOutput[]\n /**\n * The nominal credit figure held against the job while steps are\n * outstanding. The meter's real bound is the per-step message reservation\n * and the org's monthly ceiling; this is the number the console shows so\n * a running job is not read as free.\n */\n creditsReserved: number\n creditsSpent: number\n /**\n * Credits the job gave back because it failed on our side (AGL-3594),\n * summed over its give-backs; absent when it gave none. `creditsSpent`\n * stays what ran, so what the job cost is the one less the other.\n */\n refundedCredits?: number\n /** Why the latest give-back was made (`AiJobRefundReason`). */\n refundReason?: string\n /** How many give-backs the job has had: the next one's key. */\n refunds?: number\n createdBy: string\n createdAt: ITimestamp\n updatedAt: ITimestamp\n /** TTL — the brief is verbatim customer text and expires like an exchange. */\n expiresAt: ITimestamp\n /** Customer-safe only. */\n error?: string | null\n lease?: AiJobLease | null\n /** The plan the job builds from, once its plan step proposed one. */\n plan?: AiJobPlan | null\n /** What the person is asked to decide while the job is `needs_review`. */\n review?: AiJobReview | null\n /**\n * What the job waits for while it is `needs_input` for a person rather than\n * the meter (AGL-3978): the fields only they can fill, an id the planner\n * must never make up. Its plan and the plan's review are kept beside it,\n * and the answer returns the job to that review.\n */\n input?: AiJobInputRequest | null\n /**\n * What a person applied from the job's outputs (AGL-2910). Written by the\n * door that applied them, never by a step runner, and absent until then.\n */\n applied?: AiJobApplied | null\n /**\n * What a guided site start put live when it finished (AGL-3596): written\n * once by the site step's last pass, never by a person. Absent on every\n * other job, which leaves its pages as drafts.\n */\n sitePublish?: AiJobSitePublish | null\n /** A `build` job's items, one row each (AGL-3616); absent on every other kind. */\n items?: AiJobItemLedger[] | null\n /** A `build` job's planning spend and how it settled (AGL-3616). */\n orchestration?: AiJobOrchestration | null\n /**\n * A Free job started past what was left (AGL-3722): the person chose to\n * build what fits, knowing it pauses where the credits run out. Who, when,\n * what was left and the range they were shown; absent on every job that fit.\n */\n creditsConfirmed?: AiJobCreditsConfirmation | null\n /**\n * A person asked to stop the job (AGL-3616): written only by the cancel\n * door, in the transaction that decides it. A job no step holds ends\n * `canceled` in that same write; a job whose step holds a live lease keeps\n * running until the step finishes or aborts, and the machine ends it\n * `canceled` at its next write. Absent on a job nobody canceled.\n */\n cancelRequested?: AiJobCancelRequest | null\n /**\n * The empty site this guided start was for was removed (AGL-3936): the\n * job was cancelled or failed before it built anything, nobody chose what\n * to do next within the grace period, and the AI jobs beat deleted the\n * site. Written once, by the beat, after the site is gone. Absent on every\n * other job.\n */\n siteRemoved?: AiJobSiteRemoved | null\n /**\n * When the one reminder about a plan waiting for its confirmation went out\n * (AGL-4076), a day after the plan was ready. Written once, by the AI jobs\n * beat, in the transaction that decides to send it, and never cleared: a\n * job is reminded at most once, however many plans it waits on. Absent on\n * every job that was not reminded.\n */\n remindedAt?: ITimestamp | null\n /**\n * An `import` job's site, read and cut into waves (AGL-3976): what was\n * found, what is brought in in which wave, what was left out and why, and\n * each wave's state. Written only by the machine's `recordStep`, from what\n * the import step returns; absent on every other kind.\n */\n siteImport?: AiSiteImportState | null\n /**\n * A `site` job's pictures left on a starter photo (AGL-3988): each page\n * picture and dataset record its library could not fill in its own pass,\n * with the picks the image hosts refused. Written only by the machine's\n * `recordStep`, from what the site step returns; emptied by its late photo\n * pass before the site is published. Never on the wire. Absent on every\n * other kind.\n */\n sitePhotos?: AiSitePhotos | null\n}\n\n/** When the AI jobs beat removed a guided start's empty site, and which (AGL-3936). */\nexport interface AiJobSiteRemoved {\n at: ITimestamp\n /** The site's name as it was, for a sentence that names it. */\n name: string | null\n}\n\n/** Who asked to cancel a job, and when (AGL-3616). */\nexport interface AiJobCancelRequest {\n at: ITimestamp\n /** The member or staff account that asked; `null` when nobody was named. */\n by: string | null\n}\n\n/**\n * A guided site start's publish (AGL-3596): the pages that went live, with\n * the address each answers, and the pages that stayed drafts, each with the\n * plain sentence for why. A page that stayed a draft never fails the job.\n */\nexport interface AiJobSitePublish {\n /** The site's live address, `https://<subdomain>.aglyn.app/`; `null` when the site has no subdomain. */\n liveUrl: string | null\n published: Array<{ id: string; label: string; path: string }>\n drafts: Array<{ id: string; label: string; reason: string }>\n}\n\n/**\n * The record of an apply (AGL-2910): what became a new unpublished version,\n * and what waits in its editor for a person to save.\n */\nexport interface AiJobApplied {\n at: ITimestamp\n /** The member who applied. */\n by: string\n /** The new version opened for each resource, by resource id. */\n versions: Record<string, string>\n /** Resource ids whose proposed values wait in their editor, unsaved. */\n staged: string[]\n}\n\n/** The plan on the wire: every instant an ISO string. */\nexport interface AiJobPlanSummary extends AiBuildPlan {\n status: AiJobPlanStatus\n labels: Record<string, string>\n proposedAt: string | null\n confirmedAt: string | null\n confirmedBy: string | null\n key?: string\n reusedFrom?: string\n}\n\n/**\n * The wire form the console reads: every instant an ISO string, the lease\n * reduced to whether a runner holds the job. The brief and the outputs\n * travel as they are — the reader is a member of the org that wrote them.\n */\nexport interface AiJobSummary {\n id: string\n orgId: string\n hostId: string | null\n kind: AiJobKind\n status: AiJobStatus\n brief: string\n /**\n * The batch a door created this job as one of (AGL-2911), read off the\n * job's inputs; `null` for a job created on its own. The console groups a\n * run of jobs by it, which is the one thing it cannot do from a list whose\n * rows carry no inputs — and the only input the wire form carries, because\n * a batch id names nothing the member wrote.\n */\n batch: string | null\n steps: Array<{\n name: string\n status: AiJobStepStatus\n startedAt: string | null\n endedAt: string | null\n creditsSpent: number\n error: string | null\n }>\n outputs: AiJobOutput[]\n creditsReserved: number\n creditsSpent: number\n /** Credits given back for a failure on our side (AGL-3594); 0 when none. */\n refundedCredits?: number\n createdBy: string\n createdAt: string\n updatedAt: string\n error: string | null\n running: boolean\n plan: AiJobPlanSummary | null\n review: AiJobReview | null\n /** The fields a `needs_input` job waits for a person to fill (AGL-3978); absent otherwise. */\n input?: AiJobInputRequest | null\n /** What a person applied from the outputs (AGL-2910); instants as ISO strings. */\n applied?: {\n at: string | null\n by: string\n versions: Record<string, string>\n staged: string[]\n } | null\n /** What a guided site start put live (AGL-3596); absent on every other job. */\n sitePublish?: AiJobSitePublish | null\n /**\n * A site job's answers (AGL-3596): the guided start's questions as the person\n * answered them, so a failed start's Try again reopens them filled in. The\n * scalar site inputs only, never anything else a job's inputs carry; absent\n * on every other kind.\n */\n siteInputs?: Record<string, unknown> | null\n /**\n * Whether the machine confirms this job's plan itself (AGL-3594): a guided\n * site start. Its plan parks for the instant the confirmation takes, and a\n * surface reads that park as the build going on (AGL-3596). Absent on every\n * other job.\n */\n autoConfirm?: boolean\n /** A `build` job's items (AGL-3616); absent on every other job. */\n items?: AiJobItemLedger[]\n /**\n * Whether a build's request asked to publish (AGL-3616), so its plan card\n * offers the box that confirms it. Absent on every other job.\n */\n publishAsked?: boolean\n /** A `build` job's planning spend and how it settled (AGL-3616). */\n orchestration?: AiJobOrchestration\n /**\n * A cancel was asked and the job has not ended yet (AGL-3616): the step in\n * flight is finishing or aborting, and nothing after it runs. Absent once\n * the job is terminal, and on every job nobody canceled.\n */\n cancelRequested?: boolean\n /**\n * The empty site this guided start was for was removed (AGL-3936): its\n * page is gone, and Start again opens Create site with the same answers.\n * Absent on every other job.\n */\n siteRemoved?: boolean\n /**\n * An `import` job's waves (AGL-3976), without the page text a wave read\n * holds for the model; absent on every other job.\n */\n siteImport?: AiSiteImportState | null\n}\n"],"names":["AI_JOB_KINDS","AI_JOB_TERMINAL_STATUSES","AI_JOB_PLAN_STEP"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAoFD,qEAAqE,GACrE,OAAO,MAAMA,eAAqC;IAChD;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;CACD,CAAA;AAqBD,OAAO,MAAMC,2BAAmD;IAC9D;IACA;IACA;CACD,CAAA;AAID,uFAAuF,GACvF,OAAO,MAAMC,mBAAmB,OAAM"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-jobs.types.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 { ITimestamp } from '@aglyn/shared-util-timestamp'\nimport type { AiEffort } from '../providers/contract'\nimport type { AiLoadEstimate } from '../runtime/ai-palette'\nimport type { AiBuildPlan } from './ai-build-plan'\nimport type { AiFreeCreditsLeft } from './ai-site-job'\nimport type { AiJobCreditsConfirmation } from './ai-credit-estimate'\nimport type { AiSiteImportState } from './ai-site-import'\nimport type { AiSitePhotos } from './ai-site-photos'\nimport type { AiSiteInspiration } from '../jobs/ai-site-inspiration-signals'\n\n/**\n * AI generation jobs (AGL-2904).\n *\n * A job is one customer brief carried through one or more model steps by a\n * Firestore state machine, so that work longer than a request can survive\n * the request: the console route runs what it can inline, and the platform\n * job beat resumes whatever is left. The document is the whole state — no\n * process holds anything the next beat cannot read back.\n *\n * ── Outputs are drafts, never a publish ──────────────────────────────────\n *\n * Every output a job produces is a NEW draft or a new unpublished version of\n * an existing resource. A job never flips a version pointer, never registers\n * a route and never sends anything; a person opens the draft the job names\n * and publishes it through the door that already exists for that resource.\n * That is what lets a job run with no visitor, no session and no confirm\n * gate — the thing it writes cannot be seen by anyone but the workspace\n * until a member chooses otherwise.\n *\n * ── The lease ────────────────────────────────────────────────────────────\n *\n * A step runs under a lease: an owner id and an `until` instant. Two beats\n * fire a minute apart against a 120 s function timeout, and a console route\n * may still be running the first step when the first beat looks, so the\n * lease is what stops the same step being run — and metered — twice. A\n * claim is a transaction that refuses while another owner's lease is live;\n * an expired lease is simply claimable, which is also how a step abandoned\n * by a frozen process is recovered.\n */\n\n/**\n * What a job produces. `text` and `theme` have runners; every other kind is\n * registered and refuses with \"not available yet\" until its own issue lands,\n * so a kind's presence in the union says the job model accepts it, not that\n * a runner exists.\n */\nexport type AiJobKind =\n | 'page'\n | 'component'\n | 'layout'\n | 'template'\n | 'edit'\n | 'seo'\n | 'site'\n | 'email'\n | 'campaign'\n | 'form'\n | 'experiment'\n | 'insight'\n | 'products'\n | 'crm'\n | 'onboarding'\n | 'workflow'\n | 'logic'\n | 'text'\n | 'theme'\n | 'build'\n /** A change to a site's header or footer menu, made by code (AGL-3728). */\n | 'navigation'\n /** A copy of something a site already has, made by core's duplicate (AGL-3740). */\n | 'duplicate'\n /** A change to a site's settings, made by code and saved live (AGL-3748). */\n | 'settings'\n /** Folders, tags, alt text and names in a site's media library, made by code and saved live (AGL-3752). */\n | 'media'\n /** A change to a site's analytics and advertising tags, made by code and saved live (AGL-3751). */\n | 'tracking'\n /** A change to who can see a site — maintenance mode, search indexing, page visibility — made by code and saved live (AGL-4033). */\n | 'privacy'\n /** A person's own website brought in, page by page, in reviewed waves (AGL-3976). */\n | 'import'\n\n/** The union as a value, so a route can validate a body against it. */\nexport const AI_JOB_KINDS: readonly AiJobKind[] = [\n 'page',\n 'component',\n 'layout',\n 'template',\n 'edit',\n 'seo',\n 'site',\n 'email',\n 'campaign',\n 'form',\n 'experiment',\n 'insight',\n 'products',\n 'crm',\n 'onboarding',\n 'workflow',\n 'logic',\n 'text',\n 'theme',\n 'build',\n 'navigation',\n 'duplicate',\n 'settings',\n 'media',\n 'tracking',\n 'privacy',\n 'import',\n]\n\n/**\n * `queued` — waiting for a runner; `running` — a step holds the lease;\n * `needs_input` — the job stopped for something only the workspace can\n * change (credits, a cap, a switch), and the beat tries it again once that\n * may have changed — or, carrying `input`, for an answer only the person can\n * give (AGL-3978), which the beat leaves alone; `needs_review` — the job stopped for a person's decision\n * (AGL-2935): its plan is ready to confirm, or an answer broke a building\n * rule on its re-ask. No timer can make that decision, so neither the beat\n * nor anything but that person resumes it. The last three are terminal.\n */\nexport type AiJobStatus =\n | 'queued'\n | 'running'\n | 'needs_input'\n | 'needs_review'\n | 'done'\n | 'failed'\n | 'canceled'\n\nexport const AI_JOB_TERMINAL_STATUSES: readonly AiJobStatus[] = [\n 'done',\n 'failed',\n 'canceled',\n]\n\nexport type AiJobStepStatus = 'pending' | 'running' | 'done' | 'failed'\n\n/** The step a planned kind runs first (AGL-2935): its plan, before anything is built. */\nexport const AI_JOB_PLAN_STEP = 'plan'\n\nexport interface AiJobStep {\n /** Stable within the job (`plan`, `draft`, `generate`), unique per job. */\n name: string\n status: AiJobStepStatus\n startedAt?: ITimestamp | null\n endedAt?: ITimestamp | null\n /** Credits this step has actually cost, at the plan's credit rate. */\n creditsSpent: number\n /** Customer-safe only; provider detail goes to the server log. */\n error?: string | null\n /**\n * How many times a runner has been handed this step. A retryable provider\n * failure hands the step back as `pending`; the count is what stops a\n * provider outage re-running a step forever.\n */\n attempts?: number\n /**\n * Passes of a step that works through a list (AGL-2910): each one a\n * recorded, metered exchange that asked to continue. Attempts count again\n * from zero after a pass; this count is what bounds the step.\n */\n passes?: number\n /**\n * What the step's model calls cost in tokens and time (AGL-2937), summed\n * over every run that reached the provider. Absent on a step none has.\n */\n tokens?: AiJobStepTokens\n /**\n * The ids the step writes its drafts under, by draft (AGL-3079): minted\n * when the job is created, so every run of the step names the same drafts.\n * Absent on a step that writes none; a step that records none names its\n * drafts by the job's own id.\n */\n draftIds?: Partial<Record<AiJobDraftSlot, string>>\n}\n\n/**\n * A draft a job's kind always writes (AGL-3079): a screen, layout, template,\n * form or component; an email design; a campaign; an automation.\n */\nexport type AiJobDraftSlot =\n | 'screen'\n | 'layout'\n | 'template'\n | 'form'\n | 'component'\n | 'email'\n | 'campaign'\n | 'workflow'\n\n/**\n * A step's measure (AGL-2937): the four token counts the meter prices, the\n * time spent in the runner, and the model and thinking effort of the last\n * run. The machine writes it beside `creditsSpent`, so what a kind of step\n * costs in tokens is read off the job rather than inferred from the month.\n */\nexport interface AiJobStepTokens {\n /** Prompt tokens sent uncached. */\n input: number\n /** Prompt tokens read from the prompt cache. */\n cachedRead: number\n /** Prompt tokens written to the prompt cache. */\n cacheWrite: number\n /** Tokens generated, thinking included. */\n output: number\n /** The model the last run was served by. */\n model: string | null\n /** The thinking effort the last run asked for; `null` when it named none. */\n effort: AiEffort | null\n /** Milliseconds spent in the runner, summed over runs. */\n latencyMs: number\n /** Runs that reached the provider. */\n runs: number\n /**\n * Why the step's latest runs stopped (AGL-3042), oldest first and the last\n * run last: each run's stop reason and the tokens it generated, so a pass\n * cut off at its ceiling reads off the job rather than out of the sums. At\n * most `AI_JOB_STEP_LAST_RUNS` are kept; absent on a step measured before.\n */\n lastRuns?: AiJobStepRunStop[]\n}\n\n/** One run of a step, by why it stopped (AGL-3042). */\nexport interface AiJobStepRunStop {\n /**\n * The stop reason of the run's last model call — `tool_use` or `end_turn`\n * for an answer, `max_tokens` for one cut off at its ceiling, `refusal` —\n * and `null` when the provider named none.\n */\n stopReason: string | null\n /** Tokens the run generated, over every model call it made. */\n output: number\n}\n\nexport type AiJobOutputResource =\n | 'screen'\n | 'reusableComponent'\n | 'layout'\n | 'template'\n | 'form'\n | 'emailScreen'\n | 'campaign'\n | 'product'\n | 'experiment'\n | 'workflow'\n | 'text'\n | 'theme'\n | 'seo'\n | 'insight'\n | 'crm'\n /** A draft another plugin wrote for a build (AGL-3616), named by `draftResource`. */\n | 'draft'\n | 'logic'\n | 'orgAutomation'\n /** A post a guided start wrote into the site's blog (AGL-3676): a content collection entry. */\n | 'entry'\n /** A site's settings a build changed (AGL-3748): saved live, on the host document. */\n | 'settings'\n /** Files in a site's media library a build organized (AGL-3752): saved live, on their documents. */\n | 'media'\n /** A site's tracking tags a build changed (AGL-3751): saved live, on the host document. */\n | 'tracking'\n /** Who can see a site, as a build changed it (AGL-4033): saved live, on the host and page documents. */\n | 'privacy'\n\n/**\n * One thing a job wrote. Addressed by resource and id so the console can\n * build the \"open draft\" link without knowing what the runner did; `text`\n * has no document of its own, so its body rides on the output itself.\n *\n * A `theme` output is a proposal rather than a document (AGL-2938). A site's\n * theme is fields on its host document, and writing those fields is the save\n * the Theme section's editor performs, which a job never does. So the change\n * set rides on the output as `proposal`, and a person puts it in the editor\n * and saves it there, or does not.\n *\n * An `seo` output is a proposal rather than a document (AGL-2910): search\n * listing values for a page or a product, or a site audit's findings and\n * fixes. A screen's listing is served from the screen document itself, so a\n * job never writes it; the values ride on the output as `proposal`, a person\n * puts them in the editor and saves them there, and an audit's content fixes\n * become new versions only when a person applies them.\n *\n * A `crm` output is a proposal too (AGL-2917): a record's summary and next\n * step, an email draft, or an import's column matches. The CRM's own task\n * form, stage route, composer and import drawer are the writes, and a person\n * makes each of them, or does not.\n *\n * A `logic` output is a proposal too (AGL-3603): a site function or variable\n * the Functions & Variables editor opens unsaved. Saving it is the editor's\n * write, which a person makes, or does not.\n *\n * An `orgAutomation` output is a proposal too (AGL-3603): one of the\n * workspace's automations, which the Org automations editor opens unsaved and\n * switched off. `hostId` is `null`: it belongs to no one site.\n */\nexport interface AiJobOutput {\n resource: AiJobOutputResource\n id: string\n /** The unpublished version the job created, where the resource has versions. */\n versionId?: string | null\n /** `null` for an output that belongs to the org rather than one site. */\n hostId: string | null\n /**\n * The site's subdomain, which is what a console URL names a site by: the\n * `[host]` segment resolves by subdomain, so a link built from `hostId`\n * names no site the console can open.\n */\n hostSubdomain?: string | null\n label: string\n /** The body of a `text` output. Absent on every other resource. */\n text?: string\n /**\n * The change set of an output a person applies rather than opens — a\n * `theme` or `seo` proposal — in the shape its job kind's runner defines.\n */\n proposal?: Record<string, unknown>\n /**\n * What the person decides next about this output, in customer-safe words:\n * a routing the draft could not store, or part of the brief it could not\n * build. Absent when there is nothing to decide.\n */\n note?: string | null\n /**\n * What a first visit is estimated to transfer, as the doctrine measured\n * the generated document (AGL-2935) — the weight the proposal shows before\n * anything is applied. Absent where the output is not a page's document.\n */\n load?: AiLoadEstimate | null\n /**\n * For a `draft` output (AGL-3616): the resource whose owner wrote it, as\n * its writer is registered on the resource-draft seam.\n */\n draftResource?: string\n}\n\nexport interface AiJobLease {\n /** The process that holds the step: a route request id or a beat id. */\n owner: string\n until: ITimestamp\n}\n\n/** Why a job stopped for a person (AGL-2935). */\nexport type AiJobReviewReason =\n /** The plan step proposed a plan; generation waits for it to be confirmed. */\n | 'plan'\n /** An answer broke a building rule, and so did the answer to the re-ask. */\n | 'doctrine'\n /**\n * The site is at an allowance its plan sets for what the step writes, such\n * as its shared layouts or its templates (AGL-2909). Freeing one, or a plan\n * that holds more, is the member's decision; trying again runs the step\n * again.\n */\n | 'limit'\n /**\n * A site import's wave is in (AGL-3976): its pages are drafts for the\n * person to review, and the next wave starts only when they say so. Or the\n * import has read the site and waits for the first wave to be started.\n */\n | 'wave'\n /**\n * A site import's next wave needs a current proof that the workspace owns\n * the site (AGL-3976): the proof is older than its 30 days, or was never\n * made. Verifying again and starting the wave runs it.\n */\n | 'ownership'\n /**\n * A site import's read found its home page turned away by the site's bot\n * protection or firewall (AGL-4083): nothing is planned or spent. Letting\n * AglynBot in and choosing Check again reads the site again.\n */\n | 'blocked'\n /**\n * A site import's read could not read its home page for any other reason\n * (AGL-4083): an error, no answer, or a page that needs a browser to show\n * its words. Nothing is planned or spent; Check again reads it again.\n */\n | 'unreadable'\n\n/**\n * A building rule an answer broke, as the job keeps it: the number, the code,\n * the sentence, and where in the answer it was broken (AGL-3078).\n */\nexport interface AiJobRuleFinding {\n rule: number | null\n code: string\n /** Customer-safe. */\n message: string\n /** The nodes the finding names, by the ids the refused answer gave them. Absent when it names none. */\n nodeIds?: string[]\n /** The plan entries or answer fields it names, as `screens[0].sections[2]`. Absent when it names none. */\n paths?: string[]\n}\n\n/**\n * One node of the parts of a refused answer its findings name (AGL-3078), as\n * the review keeps it: the node's place, its element and the names of what it\n * sets, and never its copy. A Grid keeps the values of its layout props as\n * they were written, so a review can tell a shape the rules refuse from one\n * the model was asked to mend and did not.\n */\nexport interface AiJobReviewOutlineNode {\n /** The node's id, as the refused answer wrote it. */\n id: string\n /** How far below a node a finding names it sits: 0 for that node. */\n depth: number\n componentId: string\n /** The props it sets, by name only. */\n props: string[]\n /** The `sx` keys it sets, by name only; absent when it sets none. */\n sx?: string[]\n /** A Grid's layout props, `container`, `size`, `direction`, `spacing` and the rest, as written. */\n grid?: Record<string, string | number | boolean | null>\n /** Its children's element ids, in order. */\n children: string[]\n}\n\nexport interface AiJobReview {\n reason: AiJobReviewReason\n /** Customer-safe: what the person is asked to decide. */\n message: string\n /** The rules the last answer broke; empty for a plan. */\n findings: AiJobRuleFinding[]\n /**\n * The parts of the refused answer its findings name, each node with the\n * nodes below it (AGL-3078); absent when they name none. Kept with the job\n * for diagnosis, and shown to staff.\n */\n outline?: AiJobReviewOutlineNode[]\n /**\n * The checks' own sentence, where `message` was written for the member\n * instead (AGL-3594): rule numbers and all, for staff. Its presence is what\n * tells the drawer the findings are staff reading too.\n */\n detail?: string\n /**\n * Why trying again cannot work yet (AGL-3594) — the Free allowance left is\n * less than the step can cost — or absent when it can. The button is drawn\n * disabled with this beside it.\n */\n retryRefusal?: string\n /**\n * What a Free workspace had left when a build's plan was kept (AGL-3722),\n * so its plan card says, before Confirm, whether the build fits and offers\n * the go-ahead or a smaller first build when it does not. The confirm door\n * reads what is left again; this is what the card shows.\n */\n freeCredits?: AiFreeCreditsLeft\n}\n\n/**\n * One answer a job waits for (AGL-3978): an id only the person has, which the\n * operation that needs it describes — a tracking change's Meta pixel ID.\n */\nexport interface AiJobInputField {\n /** The plan item the answer fills, by its name in the plan. */\n item: string\n /** The item's argument the answer is written to. */\n arg: string\n /** The field's label, naming the provider: \"Meta pixel ID\". */\n label: string\n /** What an answer looks like, in the words of the card that sets it: \"8–20 digits\". */\n shape: string\n /** The pattern an answer is held to, as a RegExp's source. */\n pattern: string\n}\n\n/** What a job parked `needs_input` for a person asks (AGL-3978). */\nexport interface AiJobInputRequest {\n /** Customer-safe: the question, in the operation's own words. */\n message: string\n fields: AiJobInputField[]\n}\n\nexport type AiJobPlanStatus = 'proposed' | 'confirmed'\n\n/** The plan a job builds from, as the job keeps it once its plan step proposed it. */\nexport interface AiJobPlan extends AiBuildPlan {\n status: AiJobPlanStatus\n /** Names for the inventory ids the plan references, read when it was made. */\n labels: Record<string, string>\n proposedAt: ITimestamp\n confirmedAt: ITimestamp | null\n /** The member who confirmed it. */\n confirmedBy: string | null\n /**\n * A digest of the whole request that produced this plan (AGL-2937): the\n * job's kind, site, brief and scalar inputs, the model that answered, and\n * the prompt as it was rendered — the site inventory included. A later job\n * whose request hashes the same reuses this plan rather than paying for the\n * same answer again. A hash of the request, never of the answer, and no\n * part of the brief is recoverable from it.\n */\n key?: string\n /** The job this plan was reused from, when it was not asked for again. */\n reusedFrom?: string\n /**\n * What a site start read of the sites the person likes (AGL-3975): signals\n * only, each from a closed set or a hex color, which the plan was told the\n * structure of and the look pins the style of. Absent without them.\n */\n inspiration?: AiSiteInspiration\n}\n\n/**\n * Where one item of a `build` job stands (AGL-3616). A build settles item by\n * item: a failed item gives back its own spend and the rest of the plan still\n * runs, so the job keeps one row per item, written only by the machine's\n * `recordStep` — in the same transaction as the spend it accounts for — and\n * by the resume door's retry.\n */\nexport type AiJobItemStatus = 'pending' | 'running' | 'succeeded' | 'degraded' | 'failed' | 'skipped'\n\n/** Why an item failed: a give-back reason when it was ours, else the model's refusal or a review. */\nexport type AiJobItemFailureReason =\n | 'plan-refused'\n | 'doctrine-refused'\n | 'provider'\n | 'timeout'\n | 'step-failure'\n | 'unavailable'\n | 'refused'\n | 'review'\n\nexport interface AiJobItemFailure {\n /** Whether the failure was on our side: only then is the item's spend given back. */\n ours: boolean\n reason: AiJobItemFailureReason\n /** Customer-safe. */\n message: string\n /**\n * The checks' own sentence behind a `doctrine-refused` failure (AGL-3596),\n * rule numbers and all: kept on the job for staff and debugging, never on\n * the wire a customer reads.\n */\n detail?: string\n}\n\nexport interface AiJobItemLedger {\n /** The unit's slot within the plan: `c0` a creation, `p0` a page, `i0` an item. */\n slot: string\n /** The capability operation that builds it: `page`, `form`, or an owner's op. */\n op: string\n /** What a person reads: the page's title, the creation's or item's name. */\n label: string\n status: AiJobItemStatus\n /** Bumped by Try again; the refund key names it, so each attempt gives back once. */\n attempt: number\n /** What this item's passes cost, every attempt. */\n creditsSpent: number\n /** What this attempt's passes cost so far: what a failure of this attempt gives back. */\n attemptCredits?: number\n /** What was given back for this item's failures. */\n creditsRefunded: number\n /** The key the latest give-back for this item was made under. */\n refundKey?: string\n failure?: AiJobItemFailure | null\n /** The ids of the outputs this item produced. */\n outputs: string[]\n /** The slots whose failure changed how this item was built. */\n degradedBy?: string[]\n /** Customer-safe: what the person should know about this item. */\n note?: string | null\n /**\n * When this attempt settled, as an ISO string (AGL-3596): the moment the\n * next item started, which is what its progress row counts from. Absent\n * while the item is open, and on a ledger written before it was kept.\n */\n settledAt?: string | null\n}\n\n/** Whether a build's own planning was charged or given back (AGL-3616). */\nexport interface AiJobOrchestration {\n /** What the plan step spent. */\n creditsSpent: number\n /** `charged` once anything was delivered; `refunded` when nothing was. */\n settled: 'pending' | 'charged' | 'refunded'\n}\n\nexport interface AiJob {\n $id: string\n orgId: string\n hostId?: string | null\n kind: AiJobKind\n status: AiJobStatus\n /** The customer's brief, verbatim. Expires with the document. */\n brief: string\n /** Kind-specific inputs the runner reads (a screen id to edit, a tone). */\n inputs: Record<string, unknown>\n /**\n * The model the creator picked for the job's steps (AGL-2942), or `null`\n * for Auto. Each step honors it only where the plan and the allotment\n * allowlists allow it, and runs on the routing table otherwise.\n */\n model?: string | null\n steps: AiJobStep[]\n outputs: AiJobOutput[]\n /**\n * The nominal credit figure held against the job while steps are\n * outstanding. The meter's real bound is the per-step message reservation\n * and the org's monthly ceiling; this is the number the console shows so\n * a running job is not read as free.\n */\n creditsReserved: number\n creditsSpent: number\n /**\n * Credits the job gave back because it failed on our side (AGL-3594),\n * summed over its give-backs; absent when it gave none. `creditsSpent`\n * stays what ran, so what the job cost is the one less the other.\n */\n refundedCredits?: number\n /** Why the latest give-back was made (`AiJobRefundReason`). */\n refundReason?: string\n /** How many give-backs the job has had: the next one's key. */\n refunds?: number\n createdBy: string\n createdAt: ITimestamp\n updatedAt: ITimestamp\n /** TTL — the brief is verbatim customer text and expires like an exchange. */\n expiresAt: ITimestamp\n /** Customer-safe only. */\n error?: string | null\n lease?: AiJobLease | null\n /** The plan the job builds from, once its plan step proposed one. */\n plan?: AiJobPlan | null\n /** What the person is asked to decide while the job is `needs_review`. */\n review?: AiJobReview | null\n /**\n * What the job waits for while it is `needs_input` for a person rather than\n * the meter (AGL-3978): the fields only they can fill, an id the planner\n * must never make up. Its plan and the plan's review are kept beside it,\n * and the answer returns the job to that review.\n */\n input?: AiJobInputRequest | null\n /**\n * What a person applied from the job's outputs (AGL-2910). Written by the\n * door that applied them, never by a step runner, and absent until then.\n */\n applied?: AiJobApplied | null\n /**\n * What a guided site start put live when it finished (AGL-3596): written\n * once by the site step's last pass, never by a person. Absent on every\n * other job, which leaves its pages as drafts.\n */\n sitePublish?: AiJobSitePublish | null\n /** A `build` job's items, one row each (AGL-3616); absent on every other kind. */\n items?: AiJobItemLedger[] | null\n /** A `build` job's planning spend and how it settled (AGL-3616). */\n orchestration?: AiJobOrchestration | null\n /**\n * A Free job started past what was left (AGL-3722): the person chose to\n * build what fits, knowing it pauses where the credits run out. Who, when,\n * what was left and the range they were shown; absent on every job that fit.\n */\n creditsConfirmed?: AiJobCreditsConfirmation | null\n /**\n * A person asked to stop the job (AGL-3616): written only by the cancel\n * door, in the transaction that decides it. A job no step holds ends\n * `canceled` in that same write; a job whose step holds a live lease keeps\n * running until the step finishes or aborts, and the machine ends it\n * `canceled` at its next write. Absent on a job nobody canceled.\n */\n cancelRequested?: AiJobCancelRequest | null\n /**\n * The empty site this guided start was for was removed (AGL-3936): the\n * job was cancelled or failed before it built anything, nobody chose what\n * to do next within the grace period, and the AI jobs beat deleted the\n * site. Written once, by the beat, after the site is gone. Absent on every\n * other job.\n */\n siteRemoved?: AiJobSiteRemoved | null\n /**\n * When the one reminder about a plan waiting for its confirmation went out\n * (AGL-4076), a day after the plan was ready. Written once, by the AI jobs\n * beat, in the transaction that decides to send it, and never cleared: a\n * job is reminded at most once, however many plans it waits on. Absent on\n * every job that was not reminded.\n */\n remindedAt?: ITimestamp | null\n /**\n * An `import` job's site, read and cut into waves (AGL-3976): what was\n * found, what is brought in in which wave, what was left out and why, and\n * each wave's state. Written only by the machine's `recordStep`, from what\n * the import step returns; absent on every other kind.\n */\n siteImport?: AiSiteImportState | null\n /**\n * A `site` job's pictures left on a starter photo (AGL-3988): each page\n * picture and dataset record its library could not fill in its own pass,\n * with the picks the image hosts refused. Written only by the machine's\n * `recordStep`, from what the site step returns; emptied by its late photo\n * pass before the site is published. Never on the wire. Absent on every\n * other kind.\n */\n sitePhotos?: AiSitePhotos | null\n}\n\n/** When the AI jobs beat removed a guided start's empty site, and which (AGL-3936). */\nexport interface AiJobSiteRemoved {\n at: ITimestamp\n /** The site's name as it was, for a sentence that names it. */\n name: string | null\n}\n\n/** Who asked to cancel a job, and when (AGL-3616). */\nexport interface AiJobCancelRequest {\n at: ITimestamp\n /** The member or staff account that asked; `null` when nobody was named. */\n by: string | null\n}\n\n/**\n * A guided site start's publish (AGL-3596): the pages that went live, with\n * the address each answers, and the pages that stayed drafts, each with the\n * plain sentence for why. A page that stayed a draft never fails the job.\n */\nexport interface AiJobSitePublish {\n /** The site's live address, `https://<subdomain>.aglyn.app/`; `null` when the site has no subdomain. */\n liveUrl: string | null\n published: Array<{ id: string; label: string; path: string }>\n drafts: Array<{ id: string; label: string; reason: string }>\n}\n\n/**\n * The record of an apply (AGL-2910): what became a new unpublished version,\n * and what waits in its editor for a person to save.\n */\nexport interface AiJobApplied {\n at: ITimestamp\n /** The member who applied. */\n by: string\n /** The new version opened for each resource, by resource id. */\n versions: Record<string, string>\n /** Resource ids whose proposed values wait in their editor, unsaved. */\n staged: string[]\n}\n\n/** The plan on the wire: every instant an ISO string. */\nexport interface AiJobPlanSummary extends AiBuildPlan {\n status: AiJobPlanStatus\n labels: Record<string, string>\n proposedAt: string | null\n confirmedAt: string | null\n confirmedBy: string | null\n key?: string\n reusedFrom?: string\n}\n\n/**\n * The wire form the console reads: every instant an ISO string, the lease\n * reduced to whether a runner holds the job. The brief and the outputs\n * travel as they are — the reader is a member of the org that wrote them.\n */\nexport interface AiJobSummary {\n id: string\n orgId: string\n hostId: string | null\n kind: AiJobKind\n status: AiJobStatus\n brief: string\n /**\n * The batch a door created this job as one of (AGL-2911), read off the\n * job's inputs; `null` for a job created on its own. The console groups a\n * run of jobs by it, which is the one thing it cannot do from a list whose\n * rows carry no inputs — and the only input the wire form carries, because\n * a batch id names nothing the member wrote.\n */\n batch: string | null\n steps: Array<{\n name: string\n status: AiJobStepStatus\n startedAt: string | null\n endedAt: string | null\n creditsSpent: number\n error: string | null\n }>\n outputs: AiJobOutput[]\n creditsReserved: number\n creditsSpent: number\n /** Credits given back for a failure on our side (AGL-3594); 0 when none. */\n refundedCredits?: number\n createdBy: string\n createdAt: string\n updatedAt: string\n error: string | null\n running: boolean\n plan: AiJobPlanSummary | null\n review: AiJobReview | null\n /** The fields a `needs_input` job waits for a person to fill (AGL-3978); absent otherwise. */\n input?: AiJobInputRequest | null\n /** What a person applied from the outputs (AGL-2910); instants as ISO strings. */\n applied?: {\n at: string | null\n by: string\n versions: Record<string, string>\n staged: string[]\n } | null\n /** What a guided site start put live (AGL-3596); absent on every other job. */\n sitePublish?: AiJobSitePublish | null\n /**\n * A site job's answers (AGL-3596): the guided start's questions as the person\n * answered them, so a failed start's Try again reopens them filled in. The\n * scalar site inputs only, never anything else a job's inputs carry; absent\n * on every other kind.\n */\n siteInputs?: Record<string, unknown> | null\n /**\n * Whether the machine confirms this job's plan itself (AGL-3594): a guided\n * site start. Its plan parks for the instant the confirmation takes, and a\n * surface reads that park as the build going on (AGL-3596). Absent on every\n * other job.\n */\n autoConfirm?: boolean\n /** A `build` job's items (AGL-3616); absent on every other job. */\n items?: AiJobItemLedger[]\n /**\n * Whether a build's request asked to publish (AGL-3616), so its plan card\n * offers the box that confirms it. Absent on every other job.\n */\n publishAsked?: boolean\n /** A `build` job's planning spend and how it settled (AGL-3616). */\n orchestration?: AiJobOrchestration\n /**\n * A cancel was asked and the job has not ended yet (AGL-3616): the step in\n * flight is finishing or aborting, and nothing after it runs. Absent once\n * the job is terminal, and on every job nobody canceled.\n */\n cancelRequested?: boolean\n /**\n * The empty site this guided start was for was removed (AGL-3936): its\n * page is gone, and Start again opens Create site with the same answers.\n * Absent on every other job.\n */\n siteRemoved?: boolean\n /**\n * An `import` job's waves (AGL-3976), without the page text a wave read\n * holds for the model; absent on every other job.\n */\n siteImport?: AiSiteImportState | null\n}\n"],"names":["AI_JOB_KINDS","AI_JOB_TERMINAL_STATUSES","AI_JOB_PLAN_STEP"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAoFD,qEAAqE,GACrE,OAAO,MAAMA,eAAqC;IAChD;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;CACD,CAAA;AAqBD,OAAO,MAAMC,2BAAmD;IAC9D;IACA;IACA;CACD,CAAA;AAID,uFAAuF,GACvF,OAAO,MAAMC,mBAAmB,OAAM"}
|