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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/package.json +17 -17
  2. package/src/lib/components/ai-site-start-card.component.js +43 -34
  3. package/src/lib/components/ai-site-start-card.component.js.map +1 -1
  4. package/src/lib/jobs/ai-job-admission.d.ts +6 -0
  5. package/src/lib/jobs/ai-job-admission.js.map +1 -1
  6. package/src/lib/jobs/ai-job-site-step.js +10 -6
  7. package/src/lib/jobs/ai-job-site-step.js.map +1 -1
  8. package/src/lib/jobs/ai-layout-stock-photos.d.ts +43 -11
  9. package/src/lib/jobs/ai-layout-stock-photos.js +319 -32
  10. package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
  11. package/src/lib/jobs/ai-stock-photo-picker.js +13 -7
  12. package/src/lib/jobs/ai-stock-photo-picker.js.map +1 -1
  13. package/src/lib/jobs/fixtures/ai-stock-photo-agl3856-recordings.d.ts +35 -0
  14. package/src/lib/jobs/fixtures/ai-stock-photo-agl3856-recordings.js +404 -0
  15. package/src/lib/jobs/fixtures/ai-stock-photo-agl3856-recordings.js.map +1 -0
  16. package/src/lib/layout-language/ai-layout-motion.d.ts +9 -3
  17. package/src/lib/layout-language/ai-layout-motion.js +9 -3
  18. package/src/lib/layout-language/ai-layout-motion.js.map +1 -1
  19. package/src/lib/layout-language/ai-layout-pictures.d.ts +10 -1
  20. package/src/lib/layout-language/ai-layout-pictures.js +12 -6
  21. package/src/lib/layout-language/ai-layout-pictures.js.map +1 -1
  22. package/src/lib/runtime/ai-doctrine-validators.d.ts +40 -0
  23. package/src/lib/runtime/ai-doctrine-validators.js +132 -1
  24. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  25. package/src/lib/runtime/ai-doctrine.js +6 -2
  26. package/src/lib/runtime/ai-doctrine.js.map +1 -1
  27. package/src/lib/server/ai-jobs-route.js +19 -0
  28. package/src/lib/server/ai-jobs-route.js.map +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/layout-language/ai-layout-pictures.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 { DEFAULT_SITE_IMAGES } from '@aglyn/aglyn/app-utils/default-site'\nimport type { NodesMap } from '@aglyn/aglyn/types/nodes'\nimport { aiLayoutLabel, aiLayoutSpeaksOtherThanEnglish } from './ai-layout-labels'\n\n/**\n * THE PICTURES OF A LANGUAGE PAGE (AGL-3660).\n *\n * The compiler writes a picture slot as a frame holding an icon and an\n * `image` with alt text and no source (`ai-layout-compiler.ts`), and stays\n * pure: it never looks a photo up. This module is what runs after it, on the\n * stored page — it finds each empty slot, decides what kind of picture the\n * slot is, and fills it with a photo the site can serve from its own origin.\n *\n * Two sources fill a slot, in order:\n *\n * 1. A stock photo searched for by the slot's own description and copied into\n * the site's media library (`jobs/ai-layout-stock-photos.ts`), when the\n * deployment has a stock-photo key.\n * 2. Where the library cannot be asked at all — no stock-photo key, the\n * library down, refusing or out of time before it answered — one of the\n * STARTER PHOTOS every new site is born with (`DEFAULT_SITE_IMAGES`,\n * served at `/_static/starter/` by the tenant and the console alike,\n * credited in the CREDITS file beside them). They are the platform's own\n * files, so a visitor's browser asks no other host for them, and a\n * starter reads better than no picture when there is no library.\n *\n * Where the library answered and nothing in it fits a slot\n * ({@link AI_LAYOUT_NO_PHOTO}), the slot keeps the compiler's no-photo\n * treatment — its frame with its icon, or its section's own band behind a\n * cover — and never a starter: the beta.257 Copper Kettle Catering start\n * (2026-10-15) opened a barbecue caterer's home page on the starters'\n * pottery workbench (AGL-3831). An empty picture draws nothing on the\n * published page, and the Besigner shows the owner where to choose one.\n *\n * A slot keeps the alt text the model wrote only when it describes the photo\n * placed: a stock photo that is not what it says is described by what it is\n * (`aiStockPhotoAlt`), and a starter photo always by its own words, since\n * the model never wrote for it (AGL-3831).\n */\n\n/** What a picture slot is for, read from where it sits and what it says. */\nexport type AiLayoutPictureRole = 'hero' | 'about' | 'gallery'\n\n/** One empty picture slot of a stored page, in document order. */\nexport interface AiLayoutPictureSlot {\n /** The `image` node. */\n imageId: string\n /** The frame around it, which sets the slot's shape. */\n frameId: string | null\n /** The placeholder icon sharing the frame, taken out once a photo fills it. */\n iconId: string | null\n alt: string\n /** The frame's shape as width over height; 1 when it names none. */\n aspect: number\n /** The plan index of the section it sits in, or -1. */\n sectionIndex: number\n role: AiLayoutPictureRole\n /**\n * A product's photo (AGL-3676): what it is of, most specific first — its\n * name, then what its photo should show — searched for with the shop's\n * category and held to it, so a wick trimmer is never a laptop on a desk.\n */\n product?: { subjects: readonly string[] }\n}\n\n/** A photo a slot is filled with. */\nexport interface AiLayoutPicturePhoto {\n /** The site-served address: a starter path or the media library's own. */\n src: string\n width: number\n height: number\n /**\n * What the photo shows, where the slot's own alt text does not say it: the\n * slot takes it in place of the words the model wrote before the photo was\n * found. Absent, the slot's words describe it.\n */\n alt?: string\n}\n\n/** Words that say a picture shows people, or a section is about them. */\nconst PEOPLE =\n /\\b(about|team|story|owner|owners|founder|founders|staff|crew|people|person|portrait|meet|family|families|who we are|our (?:team|crew|people|story)|therapist|counsel(?:l)?or|coach|doctor|dentist|stylist|trainer|smil(?:e|es|ing)|headshot)\\b/i\n\n/**\n * An `aspectRatio` (`'16 / 9'`) as a number, or `null` where none is named.\n * A responsive one (`{ xs: '4 / 3', md: '4 / 5' }`, as a designed page's\n * pictures carry, AGL-3660) reads at the desktop breakpoint first: one photo\n * serves every width, and the wide screen is where its shape shows most.\n */\nfunction aspectOf(value: unknown): number | null {\n if (value && typeof value === 'object') {\n const byBreakpoint = value as Record<string, unknown>\n for (const breakpoint of ['md', 'lg', 'xl', 'sm', 'xs']) {\n const ratio = aspectOf(byBreakpoint[breakpoint])\n if (ratio !== null) return ratio\n }\n return null\n }\n const match = /^\\s*(\\d+(?:\\.\\d+)?)\\s*\\/\\s*(\\d+(?:\\.\\d+)?)\\s*$/.exec(String(value ?? ''))\n if (!match) return null\n const ratio = Number(match[1]) / Number(match[2])\n return Number.isFinite(ratio) && ratio > 0 ? ratio : null\n}\n\n/** The shape a full-bleed cover's photo is searched for: it fills a wide band. */\nconst COVER_ASPECT = 16 / 9\n\n/**\n * A slot's shape (AGL-3660): the image's own `aspectRatio` (a designed\n * picture card sets it on the image), else its frame's, else — for a frame\n * laid absolutely over its whole section, a designed photo cover — a wide\n * band's. 1 where nothing says.\n */\nfunction slotAspect(image: { sx?: Record<string, unknown> } | undefined, frame: { sx?: Record<string, unknown> } | undefined): number {\n const own = aspectOf(image?.sx?.['aspectRatio']) ?? aspectOf(frame?.sx?.['aspectRatio'])\n if (own !== null) return own\n const sx = frame?.sx\n if (sx?.['position'] === 'absolute' && sx['width'] === '100%' && sx['height'] === '100%') return COVER_ASPECT\n return 1\n}\n\n/** A slot's role: the first section's picture is the hero; people make an about picture. */\nexport function aiLayoutPictureRole(input: {\n sectionIndex: number\n sectionName: string\n alt: string\n}): AiLayoutPictureRole {\n if (input.sectionIndex === 0) return 'hero'\n if (PEOPLE.test(input.sectionName) || PEOPLE.test(input.alt)) return 'about'\n return 'gallery'\n}\n\n/**\n * Every empty picture slot of a stored page, walked from its root in\n * document order. A slot that already has a source — one the owner placed, or\n * an earlier pass filled — is not a slot.\n */\nexport function aiLayoutPictureSlots(\n nodes: NodesMap,\n rootId: string,\n sections: { ids: readonly string[]; names: readonly string[] },\n): AiLayoutPictureSlot[] {\n const map = nodes as unknown as Record<\n string,\n { componentId?: string; props?: Record<string, unknown>; sx?: Record<string, unknown>; nodes?: string[] }\n >\n const slots: AiLayoutPictureSlot[] = []\n const seen = new Set<string>()\n const visit = (id: string, parentId: string | null, sectionIndex: number) => {\n if (seen.has(id)) return\n seen.add(id)\n const node = map[id]\n if (!node) return\n const index = sections.ids.indexOf(id)\n const here = index >= 0 ? index : sectionIndex\n if (node.componentId === 'image') {\n const src = node.props?.['src']\n const alt = typeof node.props?.['alt'] === 'string' ? (node.props['alt'] as string).trim() : ''\n if ((typeof src !== 'string' || !src.trim()) && alt) {\n const frame = parentId ? map[parentId] : undefined\n const iconId =\n frame?.nodes?.find((child) => child !== id && map[child]?.componentId === 'icon') ?? null\n slots.push({\n imageId: id,\n frameId: frame ? parentId : null,\n iconId,\n alt,\n aspect: slotAspect(node, frame),\n sectionIndex: here,\n role: aiLayoutPictureRole({\n sectionIndex: here,\n sectionName: here >= 0 ? (sections.names[here] ?? '') : '',\n alt,\n }),\n })\n }\n }\n for (const child of node.nodes ?? []) visit(child, id, here)\n }\n visit(rootId, null, -1)\n return slots\n}\n\n/** The starter photos, by name, each with the words that describe it. */\nexport const AI_LAYOUT_STARTER_PHOTOS = DEFAULT_SITE_IMAGES\n\n/**\n * A source's answer for a slot the library answered and nothing in it fits\n * (AGL-3831): the slot keeps its no-photo treatment rather than take a\n * starter. Its source is empty, so a caller reading `src` to fill something\n * that must hold a photo (a product's, a record's) falls back as it would on\n * `null`.\n */\nexport const AI_LAYOUT_NO_PHOTO: Readonly<AiLayoutPicturePhoto> = Object.freeze({ src: '', width: 0, height: 0 })\n\ntype StarterName = keyof typeof AI_LAYOUT_STARTER_PHOTOS\n\n/**\n * The starter photos each role draws from, best first. A hero draws from the\n * wide photos that read as a page's opening, an about picture leads with the\n * owner, and the rest are the gallery's.\n */\nconst STARTER_POOLS: Readonly<Record<AiLayoutPictureRole, readonly StarterName[]>> = {\n hero: ['hero', 'desk', 'craft'],\n about: ['about', 'hero'],\n gallery: ['desk', 'plants', 'craft'],\n}\n\nconst ALL_STARTERS = Object.keys(AI_LAYOUT_STARTER_PHOTOS) as StarterName[]\n\n/** A small stable number from a seed, so the same job always picks the same photos. */\nexport function aiLayoutSeedNumber(seed: string): number {\n let hash = 2166136261\n for (let index = 0; index < seed.length; index += 1) {\n hash ^= seed.charCodeAt(index)\n hash = Math.imul(hash, 16777619)\n }\n return hash >>> 0\n}\n\nfunction rotate<T>(list: readonly T[], by: number): T[] {\n if (!list.length) return []\n const start = by % list.length\n return [...list.slice(start), ...list.slice(0, start)]\n}\n\n/**\n * A starter photo for each slot (AGL-3660). No photo repeats on a page while\n * an unused one remains, and every pool turns by the seed, so sites do not\n * all open with the same photo while one job always picks the same.\n *\n * The slots are served by what they are for, not where they sit: the hero\n * first, then the pictures of people (an about picture leads with the owner),\n * then the rest. The live Juniper Clay start (2026-10-08) had six slots and\n * five starters, and the owner's photo went to a stoneware card before the\n * About section that described the artist asked for it. Only a page with more\n * slots than starters repeats one: the photo placed longest ago, never the\n * one in a slot beside it. The owner's portrait goes only to a picture of\n * people; a gallery picture never takes it, even to avoid a repeat.\n */\nexport function aiLayoutStarterPhotos(\n slots: readonly AiLayoutPictureSlot[],\n seed: string,\n): AiLayoutPicturePhoto[] {\n const turn = aiLayoutSeedNumber(seed)\n const order: AiLayoutPictureRole[] = ['hero', 'about', 'gallery']\n const served = slots\n .map((slot, index) => ({ slot, index }))\n .sort((a, b) => order.indexOf(a.slot.role) - order.indexOf(b.slot.role) || a.index - b.index)\n const names: Array<StarterName | undefined> = slots.map(() => undefined)\n /** When each starter was last placed, by its turn in the serving order. */\n const placedAt = new Map<StarterName, number>()\n served.forEach(({ slot, index }, step) => {\n // An about picture leads with the owner on every site; the others turn.\n const pool = slot.role === 'about' ? [...STARTER_POOLS.about] : rotate(STARTER_POOLS[slot.role], turn)\n const rest = rotate(\n ALL_STARTERS.filter((name) => !pool.includes(name)),\n turn,\n )\n // The owner's portrait is a picture of a person, and only a picture of\n // people takes it: never a vase in a gallery (AGL-3660).\n const candidates = [...pool, ...rest].filter((name) => slot.role === 'about' || name !== 'about')\n const unused = candidates.find((candidate) => !placedAt.has(candidate))\n const beside = new Set([names[index - 1], names[index + 1]].filter(Boolean))\n const name =\n unused ??\n [...candidates]\n .sort((a, b) => (placedAt.get(a) ?? -1) - (placedAt.get(b) ?? -1))\n .find((candidate) => !beside.has(candidate)) ??\n candidates[0]\n names[index] = name\n placedAt.set(name, step)\n })\n return names.map((name) => {\n const photo = AI_LAYOUT_STARTER_PHOTOS[name as StarterName]\n return { src: photo.src, width: photo.width, height: photo.height, alt: photo.alt }\n })\n}\n\n/**\n * The page with each slot filled by its photo: the `image` given its source,\n * its intrinsic size and how it loads (the hero eagerly, as the starter home\n * page loads its own), and the placeholder icon taken out of the frame. The\n * alt text is kept as the model wrote it unless the photo brings its own\n * words. A slot given no photo is left as the compiler wrote it.\n */\nexport function aiLayoutPlacePictures(\n nodes: NodesMap,\n slots: readonly AiLayoutPictureSlot[],\n photos: ReadonlyArray<AiLayoutPicturePhoto | null>,\n): NodesMap {\n const next = { ...(nodes as unknown as Record<string, Record<string, unknown>>) }\n // Only the page's first picture loads eagerly (rule 16, AGL-3660): a hero\n // of several photos loaded them all eagerly, and a live Portfolio page was\n // refused for it (2026-10-10). The slots are in document order; every later\n // picture leaves `loading` unset, which the renderer loads when reached.\n const lead = slots.findIndex((_, index) => Boolean(photos[index] && next[slots[index].imageId]))\n slots.forEach((slot, index) => {\n const photo = photos[index]\n const node = next[slot.imageId]\n if (!photo || !node) return\n const props = { ...((node['props'] as Record<string, unknown> | undefined) ?? {}) }\n delete props['loading']\n next[slot.imageId] = {\n ...node,\n props: {\n ...props,\n src: photo.src,\n intrinsicWidth: photo.width,\n intrinsicHeight: photo.height,\n ...(photo.alt?.trim() ? { alt: photo.alt.trim() } : {}),\n ...(index === lead && slot.role === 'hero' ? { loading: 'eager' } : {}),\n },\n }\n if (slot.iconId && slot.frameId && next[slot.frameId]) {\n const frame = next[slot.frameId]\n next[slot.frameId] = {\n ...frame,\n nodes: ((frame['nodes'] as string[] | undefined) ?? []).filter((child) => child !== slot.iconId),\n }\n delete next[slot.iconId]\n }\n })\n return next as unknown as NodesMap\n}\n\n/**\n * A source of photos found for the slots themselves — a stock search copied\n * into the site's library — answering per slot, in order: a photo; or\n * {@link AI_LAYOUT_NO_PHOTO} where the library answered and nothing fits,\n * which leaves the slot without a photo; or `null` where the library could\n * not be asked, which takes a starter photo. It may throw, and then every\n * slot takes a starter.\n */\nexport type AiLayoutPictureSource = (\n slots: readonly AiLayoutPictureSlot[],\n) => Promise<ReadonlyArray<AiLayoutPicturePhoto | null>>\n\n/**\n * Fills a stored page's empty picture slots (AGL-3660): from `source` where\n * one is given and it answers; a slot it answers with nothing fitting stays\n * without a photo; every other slot takes a starter photo, described by its\n * own words (AGL-3831). Never throws, and never leaves a page worse than it\n * came: a picture is decoration, and a job never fails over one.\n */\nexport async function aiResolveLayoutPictures(\n nodes: NodesMap,\n input: {\n rootId: string\n sectionIds: readonly string[]\n sectionNames: readonly string[]\n /** A per-job word, so two sites do not open with the same photo. */\n seed: string\n source?: AiLayoutPictureSource | null\n /** The site's language (AGL-3750); a starter photo's English words are not its own. */\n language?: string\n },\n): Promise<NodesMap> {\n try {\n const slots = aiLayoutPictureSlots(nodes, input.rootId, {\n ids: input.sectionIds,\n names: input.sectionNames,\n })\n if (!slots.length) return nodes\n let found: ReadonlyArray<AiLayoutPicturePhoto | null> = []\n if (input.source) {\n try {\n found = await input.source(slots)\n } catch (error) {\n console.warn('ai layout pictures: the stock source failed; starter photos fill the page', { error })\n found = []\n }\n }\n // The starters' words are English; a site in another language says it\n // shows an illustrative photo, true of any photo, as a stock photo's does.\n const otherLanguage = aiLayoutSpeaksOtherThanEnglish(input.language)\n const starters = aiLayoutStarterPhotos(slots, input.seed).map((photo) =>\n otherLanguage ? { ...photo, alt: aiLayoutLabel('illustrativePhoto', input.language) } : photo,\n )\n return aiLayoutPlacePictures(\n nodes,\n slots,\n slots.map((_slot, index) => {\n const answer = found[index]\n if (answer) return answer.src ? answer : null\n return starters[index] ?? null\n }),\n )\n } catch (error) {\n console.error('ai layout pictures: the page keeps its empty picture slots', { error })\n return nodes\n }\n}\n"],"names":["DEFAULT_SITE_IMAGES","aiLayoutLabel","aiLayoutSpeaksOtherThanEnglish","PEOPLE","aspectOf","value","byBreakpoint","breakpoint","ratio","match","exec","String","Number","isFinite","COVER_ASPECT","slotAspect","image","frame","own","sx","aiLayoutPictureRole","input","sectionIndex","test","sectionName","alt","aiLayoutPictureSlots","nodes","rootId","sections","map","slots","seen","Set","visit","id","parentId","node","has","add","index","ids","indexOf","here","componentId","src","props","trim","undefined","iconId","find","child","push","imageId","frameId","aspect","role","names","AI_LAYOUT_STARTER_PHOTOS","AI_LAYOUT_NO_PHOTO","Object","freeze","width","height","STARTER_POOLS","hero","about","gallery","ALL_STARTERS","keys","aiLayoutSeedNumber","seed","hash","length","charCodeAt","Math","imul","rotate","list","by","start","slice","aiLayoutStarterPhotos","turn","order","served","slot","sort","a","b","placedAt","Map","forEach","step","unused","pool","rest","filter","name","includes","candidates","candidate","beside","Boolean","get","set","photo","aiLayoutPlacePictures","photos","next","lead","findIndex","_","intrinsicWidth","intrinsicHeight","loading","aiResolveLayoutPictures","sectionIds","sectionNames","found","source","error","console","warn","otherLanguage","language","starters","_slot","answer"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/layout-language/ai-layout-pictures.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 { DEFAULT_SITE_IMAGES } from '@aglyn/aglyn/app-utils/default-site'\nimport type { NodesMap } from '@aglyn/aglyn/types/nodes'\nimport { aiLayoutLabel, aiLayoutSpeaksOtherThanEnglish } from './ai-layout-labels'\n\n/**\n * THE PICTURES OF A LANGUAGE PAGE (AGL-3660).\n *\n * The compiler writes a picture slot as a frame holding an icon and an\n * `image` with alt text and no source (`ai-layout-compiler.ts`), and stays\n * pure: it never looks a photo up. This module is what runs after it, on the\n * stored page — it finds each empty slot, decides what kind of picture the\n * slot is, and fills it with a photo the site can serve from its own origin.\n *\n * Two sources fill a slot, in order:\n *\n * 1. A stock photo searched for by the slot's own description and copied into\n * the site's media library (`jobs/ai-layout-stock-photos.ts`), when the\n * deployment has a stock-photo key.\n * 2. Where the library cannot be asked at all — no stock-photo key, the\n * library down, refusing or out of time before it answered — one of the\n * STARTER PHOTOS every new site is born with (`DEFAULT_SITE_IMAGES`,\n * served at `/_static/starter/` by the tenant and the console alike,\n * credited in the CREDITS file beside them). They are the platform's own\n * files, so a visitor's browser asks no other host for them, and a\n * starter reads better than no picture when there is no library.\n *\n * Where the library answered and nothing in it fits a slot\n * ({@link AI_LAYOUT_NO_PHOTO}), the slot keeps the compiler's no-photo\n * treatment — its frame with its icon, or its section's own band behind a\n * cover — and never a starter: the beta.257 Copper Kettle Catering start\n * (2026-10-15) opened a barbecue caterer's home page on the starters'\n * pottery workbench (AGL-3831). An empty picture draws nothing on the\n * published page, and the Besigner shows the owner where to choose one.\n *\n * A slot keeps the alt text the model wrote only when it describes the photo\n * placed: a stock photo that is not what it says is described by what it is\n * (`aiStockPhotoAlt`), and a starter photo always by its own words, since\n * the model never wrote for it (AGL-3831). A stand-in the compiler drew where\n * the model described no picture is decorative while empty; the photo placed\n * there is described by its own words and is no longer decorative (AGL-3856).\n */\n\n/** What a picture slot is for, read from where it sits and what it says. */\nexport type AiLayoutPictureRole = 'hero' | 'about' | 'gallery'\n\n/** One empty picture slot of a stored page, in document order. */\nexport interface AiLayoutPictureSlot {\n /** The `image` node. */\n imageId: string\n /** The frame around it, which sets the slot's shape. */\n frameId: string | null\n /** The placeholder icon sharing the frame, taken out once a photo fills it. */\n iconId: string | null\n alt: string\n /** The frame's shape as width over height; 1 when it names none. */\n aspect: number\n /** The plan index of the section it sits in, or -1. */\n sectionIndex: number\n role: AiLayoutPictureRole\n /**\n * A product's photo (AGL-3676): what it is of, most specific first — its\n * name, then what its photo should show — searched for with the shop's\n * category and held to it, so a wick trimmer is never a laptop on a desk.\n */\n product?: { subjects: readonly string[] }\n /**\n * A stand-in the compiler drew where the model described no picture\n * (AGL-3856): its words are what a search looks for, never what a photo\n * shows, so a photo placed there is described by its own, and is no longer\n * decorative once it is.\n */\n standIn?: boolean\n}\n\n/** A photo a slot is filled with. */\nexport interface AiLayoutPicturePhoto {\n /** The site-served address: a starter path or the media library's own. */\n src: string\n width: number\n height: number\n /**\n * What the photo shows, where the slot's own alt text does not say it: the\n * slot takes it in place of the words the model wrote before the photo was\n * found. Absent, the slot's words describe it.\n */\n alt?: string\n}\n\n/** Words that say a picture shows people, or a section is about them. */\nconst PEOPLE =\n /\\b(about|team|story|owner|owners|founder|founders|staff|crew|people|person|portrait|meet|family|families|who we are|our (?:team|crew|people|story)|therapist|counsel(?:l)?or|coach|doctor|dentist|stylist|trainer|smil(?:e|es|ing)|headshot)\\b/i\n\n/**\n * An `aspectRatio` (`'16 / 9'`) as a number, or `null` where none is named.\n * A responsive one (`{ xs: '4 / 3', md: '4 / 5' }`, as a designed page's\n * pictures carry, AGL-3660) reads at the desktop breakpoint first: one photo\n * serves every width, and the wide screen is where its shape shows most.\n */\nfunction aspectOf(value: unknown): number | null {\n if (value && typeof value === 'object') {\n const byBreakpoint = value as Record<string, unknown>\n for (const breakpoint of ['md', 'lg', 'xl', 'sm', 'xs']) {\n const ratio = aspectOf(byBreakpoint[breakpoint])\n if (ratio !== null) return ratio\n }\n return null\n }\n const match = /^\\s*(\\d+(?:\\.\\d+)?)\\s*\\/\\s*(\\d+(?:\\.\\d+)?)\\s*$/.exec(String(value ?? ''))\n if (!match) return null\n const ratio = Number(match[1]) / Number(match[2])\n return Number.isFinite(ratio) && ratio > 0 ? ratio : null\n}\n\n/** The shape a full-bleed cover's photo is searched for: it fills a wide band. */\nconst COVER_ASPECT = 16 / 9\n\n/**\n * A slot's shape (AGL-3660): the image's own `aspectRatio` (a designed\n * picture card sets it on the image), else its frame's, else — for a frame\n * laid absolutely over its whole section, a designed photo cover — a wide\n * band's. 1 where nothing says.\n */\nfunction slotAspect(image: { sx?: Record<string, unknown> } | undefined, frame: { sx?: Record<string, unknown> } | undefined): number {\n const own = aspectOf(image?.sx?.['aspectRatio']) ?? aspectOf(frame?.sx?.['aspectRatio'])\n if (own !== null) return own\n const sx = frame?.sx\n if (sx?.['position'] === 'absolute' && sx['width'] === '100%' && sx['height'] === '100%') return COVER_ASPECT\n return 1\n}\n\n/** A slot's role: the first section's picture is the hero; people make an about picture. */\nexport function aiLayoutPictureRole(input: {\n sectionIndex: number\n sectionName: string\n alt: string\n}): AiLayoutPictureRole {\n if (input.sectionIndex === 0) return 'hero'\n if (PEOPLE.test(input.sectionName) || PEOPLE.test(input.alt)) return 'about'\n return 'gallery'\n}\n\n/**\n * Every empty picture slot of a stored page, walked from its root in\n * document order. A slot that already has a source — one the owner placed, or\n * an earlier pass filled — is not a slot.\n */\nexport function aiLayoutPictureSlots(\n nodes: NodesMap,\n rootId: string,\n sections: { ids: readonly string[]; names: readonly string[] },\n): AiLayoutPictureSlot[] {\n const map = nodes as unknown as Record<\n string,\n { componentId?: string; props?: Record<string, unknown>; sx?: Record<string, unknown>; nodes?: string[] }\n >\n const slots: AiLayoutPictureSlot[] = []\n const seen = new Set<string>()\n const visit = (id: string, parentId: string | null, sectionIndex: number) => {\n if (seen.has(id)) return\n seen.add(id)\n const node = map[id]\n if (!node) return\n const index = sections.ids.indexOf(id)\n const here = index >= 0 ? index : sectionIndex\n if (node.componentId === 'image') {\n const src = node.props?.['src']\n const alt = typeof node.props?.['alt'] === 'string' ? (node.props['alt'] as string).trim() : ''\n if ((typeof src !== 'string' || !src.trim()) && alt) {\n const frame = parentId ? map[parentId] : undefined\n const iconId =\n frame?.nodes?.find((child) => child !== id && map[child]?.componentId === 'icon') ?? null\n slots.push({\n imageId: id,\n frameId: frame ? parentId : null,\n iconId,\n alt,\n aspect: slotAspect(node, frame),\n sectionIndex: here,\n ...(node.props?.['decorative'] === true ? { standIn: true } : {}),\n role: aiLayoutPictureRole({\n sectionIndex: here,\n sectionName: here >= 0 ? (sections.names[here] ?? '') : '',\n alt,\n }),\n })\n }\n }\n for (const child of node.nodes ?? []) visit(child, id, here)\n }\n visit(rootId, null, -1)\n return slots\n}\n\n/** The starter photos, by name, each with the words that describe it. */\nexport const AI_LAYOUT_STARTER_PHOTOS = DEFAULT_SITE_IMAGES\n\n/**\n * A source's answer for a slot the library answered and nothing in it fits\n * (AGL-3831): the slot keeps its no-photo treatment rather than take a\n * starter. Its source is empty, so a caller reading `src` to fill something\n * that must hold a photo (a product's, a record's) falls back as it would on\n * `null`.\n */\nexport const AI_LAYOUT_NO_PHOTO: Readonly<AiLayoutPicturePhoto> = Object.freeze({ src: '', width: 0, height: 0 })\n\ntype StarterName = keyof typeof AI_LAYOUT_STARTER_PHOTOS\n\n/**\n * The starter photos each role draws from, best first. A hero draws from the\n * wide photos that read as a page's opening, an about picture leads with the\n * owner, and the rest are the gallery's.\n */\nconst STARTER_POOLS: Readonly<Record<AiLayoutPictureRole, readonly StarterName[]>> = {\n hero: ['hero', 'desk', 'craft'],\n about: ['about', 'hero'],\n gallery: ['desk', 'plants', 'craft'],\n}\n\nconst ALL_STARTERS = Object.keys(AI_LAYOUT_STARTER_PHOTOS) as StarterName[]\n\n/** A small stable number from a seed, so the same job always picks the same photos. */\nexport function aiLayoutSeedNumber(seed: string): number {\n let hash = 2166136261\n for (let index = 0; index < seed.length; index += 1) {\n hash ^= seed.charCodeAt(index)\n hash = Math.imul(hash, 16777619)\n }\n return hash >>> 0\n}\n\nfunction rotate<T>(list: readonly T[], by: number): T[] {\n if (!list.length) return []\n const start = by % list.length\n return [...list.slice(start), ...list.slice(0, start)]\n}\n\n/**\n * A starter photo for each slot (AGL-3660). No photo repeats on a page while\n * an unused one remains, and every pool turns by the seed, so sites do not\n * all open with the same photo while one job always picks the same.\n *\n * The slots are served by what they are for, not where they sit: the hero\n * first, then the pictures of people (an about picture leads with the owner),\n * then the rest. The live Juniper Clay start (2026-10-08) had six slots and\n * five starters, and the owner's photo went to a stoneware card before the\n * About section that described the artist asked for it. Only a page with more\n * slots than starters repeats one: the photo placed longest ago, never the\n * one in a slot beside it. The owner's portrait goes only to a picture of\n * people; a gallery picture never takes it, even to avoid a repeat.\n */\nexport function aiLayoutStarterPhotos(\n slots: readonly AiLayoutPictureSlot[],\n seed: string,\n): AiLayoutPicturePhoto[] {\n const turn = aiLayoutSeedNumber(seed)\n const order: AiLayoutPictureRole[] = ['hero', 'about', 'gallery']\n const served = slots\n .map((slot, index) => ({ slot, index }))\n .sort((a, b) => order.indexOf(a.slot.role) - order.indexOf(b.slot.role) || a.index - b.index)\n const names: Array<StarterName | undefined> = slots.map(() => undefined)\n /** When each starter was last placed, by its turn in the serving order. */\n const placedAt = new Map<StarterName, number>()\n served.forEach(({ slot, index }, step) => {\n // An about picture leads with the owner on every site; the others turn.\n const pool = slot.role === 'about' ? [...STARTER_POOLS.about] : rotate(STARTER_POOLS[slot.role], turn)\n const rest = rotate(\n ALL_STARTERS.filter((name) => !pool.includes(name)),\n turn,\n )\n // The owner's portrait is a picture of a person, and only a picture of\n // people takes it: never a vase in a gallery (AGL-3660).\n const candidates = [...pool, ...rest].filter((name) => slot.role === 'about' || name !== 'about')\n const unused = candidates.find((candidate) => !placedAt.has(candidate))\n const beside = new Set([names[index - 1], names[index + 1]].filter(Boolean))\n const name =\n unused ??\n [...candidates]\n .sort((a, b) => (placedAt.get(a) ?? -1) - (placedAt.get(b) ?? -1))\n .find((candidate) => !beside.has(candidate)) ??\n candidates[0]\n names[index] = name\n placedAt.set(name, step)\n })\n return names.map((name) => {\n const photo = AI_LAYOUT_STARTER_PHOTOS[name as StarterName]\n return { src: photo.src, width: photo.width, height: photo.height, alt: photo.alt }\n })\n}\n\n/**\n * The page with each slot filled by its photo: the `image` given its source,\n * its intrinsic size and how it loads (the hero eagerly, as the starter home\n * page loads its own), and the placeholder icon taken out of the frame. The\n * alt text is kept as the model wrote it unless the photo brings its own\n * words. A slot given no photo is left as the compiler wrote it.\n */\nexport function aiLayoutPlacePictures(\n nodes: NodesMap,\n slots: readonly AiLayoutPictureSlot[],\n photos: ReadonlyArray<AiLayoutPicturePhoto | null>,\n): NodesMap {\n const next = { ...(nodes as unknown as Record<string, Record<string, unknown>>) }\n // Only the page's first picture loads eagerly (rule 16, AGL-3660): a hero\n // of several photos loaded them all eagerly, and a live Portfolio page was\n // refused for it (2026-10-10). The slots are in document order; every later\n // picture leaves `loading` unset, which the renderer loads when reached.\n const lead = slots.findIndex((_, index) => Boolean(photos[index] && next[slots[index].imageId]))\n slots.forEach((slot, index) => {\n const photo = photos[index]\n const node = next[slot.imageId]\n if (!photo || !node) return\n const props = { ...((node['props'] as Record<string, unknown> | undefined) ?? {}) }\n delete props['loading']\n // A photo placed is described by its words, never hidden from a screen\n // reader: a stand-in was decorative only while it showed nothing (AGL-3856).\n if (photo.alt?.trim()) delete props['decorative']\n next[slot.imageId] = {\n ...node,\n props: {\n ...props,\n src: photo.src,\n intrinsicWidth: photo.width,\n intrinsicHeight: photo.height,\n ...(photo.alt?.trim() ? { alt: photo.alt.trim() } : {}),\n ...(index === lead && slot.role === 'hero' ? { loading: 'eager' } : {}),\n },\n }\n if (slot.iconId && slot.frameId && next[slot.frameId]) {\n const frame = next[slot.frameId]\n next[slot.frameId] = {\n ...frame,\n nodes: ((frame['nodes'] as string[] | undefined) ?? []).filter((child) => child !== slot.iconId),\n }\n delete next[slot.iconId]\n }\n })\n return next as unknown as NodesMap\n}\n\n/**\n * A source of photos found for the slots themselves — a stock search copied\n * into the site's library — answering per slot, in order: a photo; or\n * {@link AI_LAYOUT_NO_PHOTO} where the library answered and nothing fits,\n * which leaves the slot without a photo; or `null` where the library could\n * not be asked, which takes a starter photo. It may throw, and then every\n * slot takes a starter.\n */\nexport type AiLayoutPictureSource = (\n slots: readonly AiLayoutPictureSlot[],\n) => Promise<ReadonlyArray<AiLayoutPicturePhoto | null>>\n\n/**\n * Fills a stored page's empty picture slots (AGL-3660): from `source` where\n * one is given and it answers; a slot it answers with nothing fitting stays\n * without a photo; every other slot takes a starter photo, described by its\n * own words (AGL-3831). Never throws, and never leaves a page worse than it\n * came: a picture is decoration, and a job never fails over one.\n */\nexport async function aiResolveLayoutPictures(\n nodes: NodesMap,\n input: {\n rootId: string\n sectionIds: readonly string[]\n sectionNames: readonly string[]\n /** A per-job word, so two sites do not open with the same photo. */\n seed: string\n source?: AiLayoutPictureSource | null\n /** The site's language (AGL-3750); a starter photo's English words are not its own. */\n language?: string\n },\n): Promise<NodesMap> {\n try {\n const slots = aiLayoutPictureSlots(nodes, input.rootId, {\n ids: input.sectionIds,\n names: input.sectionNames,\n })\n if (!slots.length) return nodes\n let found: ReadonlyArray<AiLayoutPicturePhoto | null> = []\n if (input.source) {\n try {\n found = await input.source(slots)\n } catch (error) {\n console.warn('ai layout pictures: the stock source failed; starter photos fill the page', { error })\n found = []\n }\n }\n // The starters' words are English; a site in another language says it\n // shows an illustrative photo, true of any photo, as a stock photo's does.\n const otherLanguage = aiLayoutSpeaksOtherThanEnglish(input.language)\n const starters = aiLayoutStarterPhotos(slots, input.seed).map((photo) =>\n otherLanguage ? { ...photo, alt: aiLayoutLabel('illustrativePhoto', input.language) } : photo,\n )\n return aiLayoutPlacePictures(\n nodes,\n slots,\n slots.map((_slot, index) => {\n const answer = found[index]\n if (answer) return answer.src ? answer : null\n return starters[index] ?? null\n }),\n )\n } catch (error) {\n console.error('ai layout pictures: the page keeps its empty picture slots', { error })\n return nodes\n }\n}\n"],"names":["DEFAULT_SITE_IMAGES","aiLayoutLabel","aiLayoutSpeaksOtherThanEnglish","PEOPLE","aspectOf","value","byBreakpoint","breakpoint","ratio","match","exec","String","Number","isFinite","COVER_ASPECT","slotAspect","image","frame","own","sx","aiLayoutPictureRole","input","sectionIndex","test","sectionName","alt","aiLayoutPictureSlots","nodes","rootId","sections","map","slots","seen","Set","visit","id","parentId","node","has","add","index","ids","indexOf","here","componentId","src","props","trim","undefined","iconId","find","child","push","imageId","frameId","aspect","standIn","role","names","AI_LAYOUT_STARTER_PHOTOS","AI_LAYOUT_NO_PHOTO","Object","freeze","width","height","STARTER_POOLS","hero","about","gallery","ALL_STARTERS","keys","aiLayoutSeedNumber","seed","hash","length","charCodeAt","Math","imul","rotate","list","by","start","slice","aiLayoutStarterPhotos","turn","order","served","slot","sort","a","b","placedAt","Map","forEach","step","unused","pool","rest","filter","name","includes","candidates","candidate","beside","Boolean","get","set","photo","aiLayoutPlacePictures","photos","next","lead","findIndex","_","intrinsicWidth","intrinsicHeight","loading","aiResolveLayoutPictures","sectionIds","sectionNames","found","source","error","console","warn","otherLanguage","language","starters","_slot","answer"],"mappings":"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"}
@@ -898,6 +898,25 @@ export declare function detectPlanSplitLists(plan: AiBuildPlan, inventory: AiSit
898
898
  * can answer with the form's bare name, which is no reference at all.
899
899
  */
900
900
  export declare function aiSettlePlanRefs(plan: AiBuildPlan, inventory?: AiSiteInventory | null): AiBuildPlan;
901
+ /**
902
+ * References to nothing at all are taken out before the rules read the plan
903
+ * (AGL-3845), where each has one reading. The live Free candle-shop start of
904
+ * 2026-10-16, on an empty site, reused a layout `laySite` and duplicated a
905
+ * page `scrStarter` the site does not have, and wrote what each section
906
+ * shows into its uses ("hero", "product grid", "help text"). It was refused
907
+ * for the unknown records, and the Free wall's trim, which never takes a
908
+ * section that places something, could take none of them.
909
+ *
910
+ * - A reuse of a record the site does not have is dropped, and a page laid
911
+ * out in it names no layout, for the layouts settle to place.
912
+ * - A page duplicated from a page the site does not have starts fresh.
913
+ * - A section's use that names no record the site has and no creation of the
914
+ * plan is a description, not a reference, and comes out.
915
+ *
916
+ * Runs after `aiSettlePlanRefs`, so a creation spelled another way is
917
+ * already pointed at, never dropped.
918
+ */
919
+ export declare function aiSettlePlanUnknownIds(plan: AiBuildPlan, inventory: AiSiteInventory | null): AiBuildPlan;
901
920
  /**
902
921
  * What customers say, which no dataset of the site's holds: a review, a
903
922
  * testimonial, a rating, a quote (AGL-3616). A job never writes them for the
@@ -1076,6 +1095,27 @@ export declare function detectCreateBeforeReuse(plan: AiBuildPlan, inventory: Ai
1076
1095
  * creation, each saying why and what to do instead.
1077
1096
  */
1078
1097
  export declare function detectPlanUncreatable(plan: AiBuildPlan, capabilities: AiPlanCapabilities | null): AiDoctrineViolation[];
1098
+ /**
1099
+ * A section that places a creation the plan never declares is settled
1100
+ * before the rules read the plan, where it has one answer (AGL-3845). The
1101
+ * live candle-shop start of 2026-10-16 placed `new:contactForm` in its
1102
+ * "Care, shipping and contact" section on both answers and stopped needing
1103
+ * input: the camelCase name read as a component, which a site start may not
1104
+ * make, and the refusal itself said to take it out.
1105
+ *
1106
+ * - A name that reads as a form, where this job may make one more form, is
1107
+ * declared as one: a newsletter's takes an email, any other the contact
1108
+ * form's fields, named in words ("Contact form") with the reference
1109
+ * pointed at it.
1110
+ * - Anything else this job could not make one more of is taken out of the
1111
+ * section's uses, and the section is drawn on the page — unless the
1112
+ * section itself is the form ("consultation request form"): without it
1113
+ * the section has no purpose, and what to put there instead is the
1114
+ * rule's to ask.
1115
+ * - A component the job could declare is left for the rule to ask about:
1116
+ * what it should be has more than one answer.
1117
+ */
1118
+ export declare function aiSettlePlanUndeclaredUses(plan: AiBuildPlan, inventory: AiSiteInventory | null, capabilities?: AiPlanCapabilities | null): AiBuildPlan;
1079
1119
  /**
1080
1120
  * Rule 7 (plan): what a plan places, it reuses or it creates (AGL-3040). The
1081
1121
  * doctrine has a plan refer to what it creates as new:<name>, so a reference
@@ -3892,6 +3892,53 @@ const FORM_SECTION_NAME = /\bform\b|\bsign[- ]?up\b|\bsubscribe\b|\bnewsletter\b
3892
3892
  screens
3893
3893
  }) : plan;
3894
3894
  }
3895
+ /**
3896
+ * References to nothing at all are taken out before the rules read the plan
3897
+ * (AGL-3845), where each has one reading. The live Free candle-shop start of
3898
+ * 2026-10-16, on an empty site, reused a layout `laySite` and duplicated a
3899
+ * page `scrStarter` the site does not have, and wrote what each section
3900
+ * shows into its uses ("hero", "product grid", "help text"). It was refused
3901
+ * for the unknown records, and the Free wall's trim, which never takes a
3902
+ * section that places something, could take none of them.
3903
+ *
3904
+ * - A reuse of a record the site does not have is dropped, and a page laid
3905
+ * out in it names no layout, for the layouts settle to place.
3906
+ * - A page duplicated from a page the site does not have starts fresh.
3907
+ * - A section's use that names no record the site has and no creation of the
3908
+ * plan is a description, not a reference, and comes out.
3909
+ *
3910
+ * Runs after `aiSettlePlanRefs`, so a creation spelled another way is
3911
+ * already pointed at, never dropped.
3912
+ */ export function aiSettlePlanUnknownIds(plan, inventory) {
3913
+ const kinds = inventoryKinds(inventory);
3914
+ const reuse = plan.reuse.filter((entry)=>entry.kind === 'theme' || kinds.get(entry.id) === entry.kind);
3915
+ const dropped = new Set(plan.reuse.filter((entry)=>!reuse.includes(entry)).map((entry)=>entry.id));
3916
+ const names = (ref)=>!!ref && (kinds.has(ref) || !!aiPlanCreateFor(plan, ref) || !!aiPlanItemFor(plan, ref) || isAiPlanNewRef(ref) && !dropped.has(ref));
3917
+ let changed = reuse.length !== plan.reuse.length;
3918
+ const screens = plan.screens.map((screen)=>{
3919
+ const layout = screen.layout && dropped.has(screen.layout) ? null : screen.layout;
3920
+ const duplicateOf = screen.duplicateOf && !kinds.has(screen.duplicateOf) ? null : screen.duplicateOf;
3921
+ const sections = screen.sections.map((section)=>{
3922
+ const uses = section.uses.filter(names);
3923
+ return uses.length === section.uses.length ? section : _extends({}, section, {
3924
+ uses
3925
+ });
3926
+ });
3927
+ if (layout === screen.layout && duplicateOf === screen.duplicateOf && sections.every((one, index)=>one === screen.sections[index])) {
3928
+ return screen;
3929
+ }
3930
+ changed = true;
3931
+ return _extends({}, screen, {
3932
+ layout,
3933
+ duplicateOf,
3934
+ sections
3935
+ });
3936
+ });
3937
+ return changed ? _extends({}, plan, {
3938
+ reuse,
3939
+ screens
3940
+ }) : plan;
3941
+ }
3895
3942
  /**
3896
3943
  * What customers say, which no dataset of the site's holds: a review, a
3897
3944
  * testimonial, a rating, a quote (AGL-3616). A job never writes them for the
@@ -4560,10 +4607,94 @@ const REUSE_BEFORE_CREATE_KINDS = new Set([
4560
4607
  * a component. Only the message reads it; the reference is refused whatever
4561
4608
  * it was meant to be.
4562
4609
  */ function undeclaredPlacementKind(name, section, plan, kinds) {
4563
- if (FORM_SECTION_NAME.test(name)) return 'form';
4610
+ if (readsAsForm(name)) return 'form';
4564
4611
  const placesForm = section.uses.some((ref)=>refKind(ref, plan, kinds) === 'form');
4565
4612
  return FORM_SECTION_NAME.test(section.name) && !placesForm ? 'form' : 'component';
4566
4613
  }
4614
+ /** A creation's name in words: `contactForm` → "contact Form", so `\\bform\\b` can read it. */ function nameInWords(name) {
4615
+ return name.replace(/([a-z0-9])([A-Z])/g, '$1 $2').replace(/[-_]+/g, ' ').trim();
4616
+ }
4617
+ /** Whether a creation's name, camelCase or not, names a form. */ function readsAsForm(name) {
4618
+ return FORM_SECTION_NAME.test(nameInWords(name));
4619
+ }
4620
+ const NEWSLETTER_NAME = /\bnewsletter\b|\bsubscribe\b|\bsign[- ]?up\b/i;
4621
+ /**
4622
+ * A section that places a creation the plan never declares is settled
4623
+ * before the rules read the plan, where it has one answer (AGL-3845). The
4624
+ * live candle-shop start of 2026-10-16 placed `new:contactForm` in its
4625
+ * "Care, shipping and contact" section on both answers and stopped needing
4626
+ * input: the camelCase name read as a component, which a site start may not
4627
+ * make, and the refusal itself said to take it out.
4628
+ *
4629
+ * - A name that reads as a form, where this job may make one more form, is
4630
+ * declared as one: a newsletter's takes an email, any other the contact
4631
+ * form's fields, named in words ("Contact form") with the reference
4632
+ * pointed at it.
4633
+ * - Anything else this job could not make one more of is taken out of the
4634
+ * section's uses, and the section is drawn on the page — unless the
4635
+ * section itself is the form ("consultation request form"): without it
4636
+ * the section has no purpose, and what to put there instead is the
4637
+ * rule's to ask.
4638
+ * - A component the job could declare is left for the rule to ask about:
4639
+ * what it should be has more than one answer.
4640
+ */ export function aiSettlePlanUndeclaredUses(plan, inventory, capabilities = null) {
4641
+ if (!capabilities) return plan;
4642
+ const kinds = inventoryKinds(inventory);
4643
+ let next = plan;
4644
+ const repoint = new Map();
4645
+ const drop = new Set();
4646
+ for (const ref of aiPlanUndeclaredRefs(plan)){
4647
+ if (ref.field !== 'uses') continue;
4648
+ const key = ref.name.toLowerCase();
4649
+ if (repoint.has(key) || drop.has(ref.path)) continue;
4650
+ const section = plan.screens[ref.screenIndex].sections[ref.sectionIndex];
4651
+ const kind = undeclaredPlacementKind(ref.name, section, next, kinds);
4652
+ if (kind === 'form' && readsAsForm(ref.name) && !aiPlanUncreatableKind(next, 'form', capabilities)) {
4653
+ const words = nameInWords(ref.name).toLowerCase();
4654
+ const name = `${words.charAt(0).toUpperCase()}${words.slice(1)}`;
4655
+ repoint.set(key, `new:${name}`);
4656
+ if (!aiPlanCreateFor(next, `new:${name}`)) {
4657
+ next = _extends({}, next, {
4658
+ create: [
4659
+ ...next.create,
4660
+ {
4661
+ kind: 'form',
4662
+ name,
4663
+ why: AI_PLAN_SETTLED_FORM.why,
4664
+ duplicateOf: null,
4665
+ fields: NEWSLETTER_NAME.test(words) ? [
4666
+ 'email'
4667
+ ] : [
4668
+ ...AI_PLAN_SETTLED_FORM.fields
4669
+ ]
4670
+ }
4671
+ ]
4672
+ });
4673
+ }
4674
+ } else if (!FORM_SECTION_NAME.test(section.name) && aiPlanUncreatableKind(next, kind, capabilities)) {
4675
+ drop.add(ref.path);
4676
+ }
4677
+ }
4678
+ if (!repoint.size && !drop.size) return plan;
4679
+ const undeclaredName = (ref)=>isAiPlanNewRef(ref) && !aiPlanCreateFor(plan, ref) && !aiPlanItemFor(plan, ref) ? ref.slice('new:'.length).trim().toLowerCase() : null;
4680
+ return _extends({}, next, {
4681
+ screens: next.screens.map((screen, screenIndex)=>_extends({}, screen, {
4682
+ sections: screen.sections.map((section, sectionIndex)=>{
4683
+ const uses = section.uses.flatMap((ref, useIndex)=>{
4684
+ var _repoint_get;
4685
+ if (drop.has(`screens[${screenIndex}].sections[${sectionIndex}].uses[${useIndex}]`)) return [];
4686
+ const name = undeclaredName(ref);
4687
+ return [
4688
+ name !== null ? (_repoint_get = repoint.get(name)) != null ? _repoint_get : ref : ref
4689
+ ];
4690
+ });
4691
+ return uses.length === section.uses.length && uses.every((ref, index)=>ref === section.uses[index]) ? section : _extends({}, section, {
4692
+ uses
4693
+ });
4694
+ })
4695
+ }))
4696
+ });
4697
+ }
4567
4698
  /**
4568
4699
  * Rule 7 (plan): what a plan places, it reuses or it creates (AGL-3040). The
4569
4700
  * doctrine has a plan refer to what it creates as new:<name>, so a reference