@aglyn/plugins-ai 1.0.0-beta.237 → 1.0.0-beta.238

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 (128) hide show
  1. package/package.json +17 -17
  2. package/src/lib/components/ai-credits-prompt.component.d.ts +11 -4
  3. package/src/lib/components/ai-credits-prompt.component.js +84 -34
  4. package/src/lib/components/ai-credits-prompt.component.js.map +1 -1
  5. package/src/lib/components/ai-job-cancel.component.d.ts +38 -0
  6. package/src/lib/components/ai-job-cancel.component.js +156 -0
  7. package/src/lib/components/ai-job-cancel.component.js.map +1 -0
  8. package/src/lib/components/ai-job-plan.component.js +12 -1
  9. package/src/lib/components/ai-job-plan.component.js.map +1 -1
  10. package/src/lib/components/ai-jobs-list-page.component.js +141 -72
  11. package/src/lib/components/ai-jobs-list-page.component.js.map +1 -1
  12. package/src/lib/components/ai-site-build-page.component.js +22 -2
  13. package/src/lib/components/ai-site-build-page.component.js.map +1 -1
  14. package/src/lib/components/ai-usage-strip.component.d.ts +19 -2
  15. package/src/lib/components/ai-usage-strip.component.js +40 -9
  16. package/src/lib/components/ai-usage-strip.component.js.map +1 -1
  17. package/src/lib/components/assist-jobs-drawer.component.js +12 -16
  18. package/src/lib/components/assist-jobs-drawer.component.js.map +1 -1
  19. package/src/lib/components/assist-panel.component.js +87 -51
  20. package/src/lib/components/assist-panel.component.js.map +1 -1
  21. package/src/lib/jobs/ai-job-build-step.js +4 -1
  22. package/src/lib/jobs/ai-job-build-step.js.map +1 -1
  23. package/src/lib/jobs/ai-job-cancel.d.ts +71 -0
  24. package/src/lib/jobs/ai-job-cancel.js +102 -0
  25. package/src/lib/jobs/ai-job-cancel.js.map +1 -0
  26. package/src/lib/jobs/ai-job-component-step.js +4 -1
  27. package/src/lib/jobs/ai-job-component-step.js.map +1 -1
  28. package/src/lib/jobs/ai-job-drafts.js +2 -0
  29. package/src/lib/jobs/ai-job-drafts.js.map +1 -1
  30. package/src/lib/jobs/ai-job-form-step.js +4 -1
  31. package/src/lib/jobs/ai-job-form-step.js.map +1 -1
  32. package/src/lib/jobs/ai-job-layout-language.d.ts +2 -0
  33. package/src/lib/jobs/ai-job-layout-language.js +13 -1
  34. package/src/lib/jobs/ai-job-layout-language.js.map +1 -1
  35. package/src/lib/jobs/ai-job-layout-step.js +6 -1
  36. package/src/lib/jobs/ai-job-layout-step.js.map +1 -1
  37. package/src/lib/jobs/ai-job-page-language.js +8 -0
  38. package/src/lib/jobs/ai-job-page-language.js.map +1 -1
  39. package/src/lib/jobs/ai-job-page-step.js +12 -1
  40. package/src/lib/jobs/ai-job-page-step.js.map +1 -1
  41. package/src/lib/jobs/ai-job-plan-step.d.ts +20 -1
  42. package/src/lib/jobs/ai-job-plan-step.js +49 -9
  43. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  44. package/src/lib/jobs/ai-job-site-content.d.ts +15 -0
  45. package/src/lib/jobs/ai-job-site-content.js +66 -7
  46. package/src/lib/jobs/ai-job-site-content.js.map +1 -1
  47. package/src/lib/jobs/ai-job-site-step.js +4 -1
  48. package/src/lib/jobs/ai-job-site-step.js.map +1 -1
  49. package/src/lib/jobs/ai-job-template-step.js +4 -1
  50. package/src/lib/jobs/ai-job-template-step.js.map +1 -1
  51. package/src/lib/jobs/ai-jobs.d.ts +37 -12
  52. package/src/lib/jobs/ai-jobs.js +184 -28
  53. package/src/lib/jobs/ai-jobs.js.map +1 -1
  54. package/src/lib/jobs/ai-layout-stock-photos.d.ts +143 -70
  55. package/src/lib/jobs/ai-layout-stock-photos.js +649 -176
  56. package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
  57. package/src/lib/jobs/fixtures/ai-ceramics-portfolio-site-plan.d.ts +30 -0
  58. package/src/lib/jobs/fixtures/ai-ceramics-portfolio-site-plan.js +220 -0
  59. package/src/lib/jobs/fixtures/ai-ceramics-portfolio-site-plan.js.map +1 -0
  60. package/src/lib/jobs/fixtures/ai-stock-photo-pixabay-recordings.d.ts +27 -0
  61. package/src/lib/jobs/fixtures/ai-stock-photo-pixabay-recordings.js +151 -0
  62. package/src/lib/jobs/fixtures/ai-stock-photo-pixabay-recordings.js.map +1 -0
  63. package/src/lib/layout-language/ai-layout-compiler.d.ts +6 -0
  64. package/src/lib/layout-language/ai-layout-compiler.js +304 -8
  65. package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
  66. package/src/lib/layout-language/ai-layout-design.d.ts +6 -3
  67. package/src/lib/layout-language/ai-layout-design.js +5 -3
  68. package/src/lib/layout-language/ai-layout-design.js.map +1 -1
  69. package/src/lib/layout-language/ai-layout-frame.d.ts +12 -0
  70. package/src/lib/layout-language/ai-layout-frame.js +58 -3
  71. package/src/lib/layout-language/ai-layout-frame.js.map +1 -1
  72. package/src/lib/layout-language/ai-layout-listings.d.ts +13 -1
  73. package/src/lib/layout-language/ai-layout-listings.js +61 -46
  74. package/src/lib/layout-language/ai-layout-listings.js.map +1 -1
  75. package/src/lib/layout-language/ai-layout-pictures.d.ts +8 -0
  76. package/src/lib/layout-language/ai-layout-pictures.js.map +1 -1
  77. package/src/lib/layout-language/ai-layout-storefront.d.ts +82 -0
  78. package/src/lib/layout-language/ai-layout-storefront.js +223 -0
  79. package/src/lib/layout-language/ai-layout-storefront.js.map +1 -0
  80. package/src/lib/model/ai-build-job.d.ts +9 -1
  81. package/src/lib/model/ai-build-job.js +19 -5
  82. package/src/lib/model/ai-build-job.js.map +1 -1
  83. package/src/lib/model/ai-build-progress.d.ts +2 -2
  84. package/src/lib/model/ai-build-progress.js +6 -4
  85. package/src/lib/model/ai-build-progress.js.map +1 -1
  86. package/src/lib/model/ai-credit-estimate.d.ts +69 -2
  87. package/src/lib/model/ai-credit-estimate.js +91 -3
  88. package/src/lib/model/ai-credit-estimate.js.map +1 -1
  89. package/src/lib/model/ai-job-cancel-copy.d.ts +44 -0
  90. package/src/lib/model/ai-job-cancel-copy.js +53 -0
  91. package/src/lib/model/ai-job-cancel-copy.js.map +1 -0
  92. package/src/lib/model/ai-job-failure-copy.d.ts +4 -1
  93. package/src/lib/model/ai-job-failure-copy.js +8 -3
  94. package/src/lib/model/ai-job-failure-copy.js.map +1 -1
  95. package/src/lib/model/ai-jobs.types.d.ts +20 -0
  96. package/src/lib/model/ai-jobs.types.js.map +1 -1
  97. package/src/lib/model/ai-site-build-progress.js +7 -4
  98. package/src/lib/model/ai-site-build-progress.js.map +1 -1
  99. package/src/lib/model/ai-site-job.d.ts +19 -2
  100. package/src/lib/model/ai-site-job.js +29 -8
  101. package/src/lib/model/ai-site-job.js.map +1 -1
  102. package/src/lib/model/ai-site-kinds.d.ts +4 -1
  103. package/src/lib/model/ai-site-kinds.js +34 -6
  104. package/src/lib/model/ai-site-kinds.js.map +1 -1
  105. package/src/lib/runtime/ai-doctrine-validators.d.ts +37 -1
  106. package/src/lib/runtime/ai-doctrine-validators.js +151 -6
  107. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  108. package/src/lib/runtime/ai-doctrine.js +4 -3
  109. package/src/lib/runtime/ai-doctrine.js.map +1 -1
  110. package/src/lib/runtime/ai-palette.generated.js +53 -7
  111. package/src/lib/runtime/ai-palette.generated.js.map +1 -1
  112. package/src/lib/server/ai-jobs-cancel.d.ts +13 -5
  113. package/src/lib/server/ai-jobs-cancel.js +27 -9
  114. package/src/lib/server/ai-jobs-cancel.js.map +1 -1
  115. package/src/lib/server/ai-jobs-gate.d.ts +12 -2
  116. package/src/lib/server/ai-jobs-gate.js +20 -9
  117. package/src/lib/server/ai-jobs-gate.js.map +1 -1
  118. package/src/lib/usage/ai-usage-meter.d.ts +2 -1
  119. package/src/lib/usage/ai-usage-meter.js +17 -1
  120. package/src/lib/usage/ai-usage-meter.js.map +1 -1
  121. package/src/lib/usage/ai-usage-wire.d.ts +6 -0
  122. package/src/lib/usage/ai-usage-wire.js.map +1 -1
  123. package/src/lib/usage/assist-usage.d.ts +7 -0
  124. package/src/lib/usage/assist-usage.js +14 -4
  125. package/src/lib/usage/assist-usage.js.map +1 -1
  126. package/src/lib/usage/free-ai-credits-left.d.ts +7 -0
  127. package/src/lib/usage/free-ai-credits-left.js +4 -2
  128. package/src/lib/usage/free-ai-credits-left.js.map +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/layout-language/ai-layout-listings.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 * A SITE'S OWN RECORDS, PLACED BY THE PLATFORM (AGL-3676).\n *\n * A guided start writes a store's first products and a blog's first posts,\n * then built the pages around them in the layout language — and the pages\n * showed neither. \"Featured candles\" and \"The candle range\" on the live\n * Ember & Wick start (2026-10-08) were cards the model wrote, a name and a\n * line each: no photo, no price, no link, nothing a shopper could open; the\n * Clay Notes blog's \"Featured writing\" was three cards that linked to no\n * post. The real records were on the site, one click away from nowhere.\n *\n * A LISTING is one kind of record a site keeps — its catalog, its blog —\n * placed by code in the sections whose plan says they show it, through the\n * element the plugin that keeps the records renders them with, bound to the\n * records themselves rather than to words about them. It is the form binding\n * one step on (a section whose plan `uses` a form places that form by its\n * id, whatever the answer remembered): the model writes the section's\n * heading and words, the platform places the records.\n *\n * - `products` — the commerce plugin's Product grid (`product-grid`), over\n * the site's whole active catalog: a photo, the name, the price (or\n * \"Price coming soon\" where the owner cleared it), each card linking its\n * product's page.\n * - `posts` — the content plugin's Collection Entries (`collectionEntries`)\n * over the blog's collection, its card a cover, the date and byline, the\n * title and the excerpt, linking each post.\n * - `tracks` — the music plugin's Music player (`musicPlayer`, AGL-3716) on a\n * music site, placed EMPTY: its \"add your tracks\" state, for the owner to\n * fill from their own media library. No job ever sources a recording, so\n * a tracks listing has no records and the player no source.\n *\n * The elements are named by their persisted ids, as the form binding names\n * `form`: a plugin never imports another's internals, and\n * `ai-layout-listings.spec.ts` holds the ids and props named here to the\n * source of the plugins that declare them.\n *\n * Model only: shapes and the inputs reader; the compiler draws them.\n */\n\nexport type AiLayoutListingKind = 'products' | 'posts' | 'tracks'\n\n/** How a section shows a listing: a few of its records on a page about something else, or all of them on its own page. */\nexport type AiLayoutListingRole = 'featured' | 'index'\n\n/** One section of one page that places a listing. */\nexport interface AiLayoutListingPlacement {\n /** The page, by its plan id. */\n screenId: string\n /** The section, by plan index. */\n section: number\n role: AiLayoutListingRole\n}\n\n/** A kind of record the site keeps, and where its pages place it. */\nexport interface AiLayoutListing {\n /** `listing:products`, `listing:posts`: what a section's placement names. */\n id: string\n kind: AiLayoutListingKind\n /** What a visitor would call the whole list: \"the shop\", \"the blog\". */\n name: string\n /** The records it shows today, by name, so a page's words can name them. */\n records: string[]\n /** Where the whole list lives, as a path on the site: the blog's `/blog`. */\n href?: string\n /** The content collection a posts listing repeats, by its slug. */\n collectionSlug?: string\n /**\n * Where a visitor goes from a store with nothing listed yet (AGL-3676): the\n * site's contact page, by its path. The empty state names it as its one\n * action, so a shop that opens empty still answers a visitor.\n */\n emptyAction?: { label: string; href: string }\n /**\n * `false` for a store that lists its catalog but cannot sell yet (its first\n * products were skipped or failed, AGL-3676): the header carries no cart.\n */\n cart?: boolean\n placements: AiLayoutListingPlacement[]\n}\n\n/** The unit input a site's listings travel in, to its layout and its pages. */\nexport const AI_LAYOUT_LISTINGS_INPUT = 'siteListings'\n\n/** The id a listing of a kind is placed by. */\nexport function aiLayoutListingId(kind: AiLayoutListingKind): string {\n return `listing:${kind}`\n}\n\n/** The persisted element a listing is drawn with, by kind (held to its plugin's source by a spec). */\nexport const AI_LAYOUT_LISTING_ELEMENTS: Readonly<Record<AiLayoutListingKind, string>> = {\n products: 'product-grid',\n posts: 'collectionEntries',\n tracks: 'musicPlayer',\n}\n\n/** The header's cart button: the commerce plugin's Cart in its `button` variant. */\nexport const AI_LAYOUT_CART_ELEMENT = 'cart'\n\n/** The most records a featured band shows; a page of its own shows them all. */\nexport const AI_LAYOUT_FEATURED_RECORDS = { products: 4, posts: 3, tracks: 0 } as const\n\nconst PLACEMENT_ROLES: readonly AiLayoutListingRole[] = ['featured', 'index']\nconst KINDS: readonly AiLayoutListingKind[] = ['products', 'posts', 'tracks']\n\nconst text = (value: unknown): string => (typeof value === 'string' ? value.trim() : '')\n\n/** A path on the site, as a listing's links are kept. */\nconst SITE_PATH = /^\\/[a-z0-9-]+(?:\\/[a-z0-9-]+)*$/\n\n/** A site's listings as its unit inputs carry them; nothing that does not read is kept. */\nexport function aiLayoutListingsOf(inputs: Readonly<Record<string, unknown>> | null | undefined): AiLayoutListing[] {\n const raw = inputs?.[AI_LAYOUT_LISTINGS_INPUT]\n if (!Array.isArray(raw)) return []\n const listings: AiLayoutListing[] = []\n for (const entry of raw) {\n if (!entry || typeof entry !== 'object') continue\n const record = entry as Record<string, unknown>\n const kind = record['kind'] as AiLayoutListingKind\n if (!KINDS.includes(kind) || listings.some((listing) => listing.kind === kind)) continue\n const href = text(record['href'])\n const action = (record['emptyAction'] ?? {}) as Record<string, unknown>\n const actionLabel = text(action['label']).slice(0, 40)\n const actionHref = text(action['href'])\n const collectionSlug = text(record['collectionSlug'])\n // A posts listing with no collection to repeat shows nothing.\n if (kind === 'posts' && !/^[a-z0-9-]{1,100}$/.test(collectionSlug)) continue\n const placements = (Array.isArray(record['placements']) ? record['placements'] : []).flatMap((value) => {\n const placement = (value ?? {}) as Record<string, unknown>\n const screenId = text(placement['screenId'])\n const section = placement['section']\n const role = placement['role'] as AiLayoutListingRole\n return screenId && typeof section === 'number' && Number.isInteger(section) && section >= 0 && PLACEMENT_ROLES.includes(role)\n ? [{ screenId, section, role }]\n : []\n })\n listings.push({\n id: aiLayoutListingId(kind),\n kind,\n name: text(record['name']) || (kind === 'products' ? 'the shop' : kind === 'posts' ? 'the blog' : 'the music'),\n // A tracks listing names no records: no job sources a recording (AGL-3716).\n records:\n kind === 'tracks'\n ? []\n : (Array.isArray(record['records']) ? record['records'] : []).map(text).filter(Boolean).slice(0, 12),\n ...(SITE_PATH.test(href) ? { href } : {}),\n ...(kind === 'posts' ? { collectionSlug } : {}),\n ...(kind === 'products' && record['cart'] === false ? { cart: false } : {}),\n ...(kind === 'products' && actionLabel && SITE_PATH.test(actionHref) ? { emptyAction: { label: actionLabel, href: actionHref } } : {}),\n placements,\n })\n }\n return listings\n}\n\n/** The listing a section of a page places, and how; `null` for a section that places none. */\nexport function aiLayoutListingAt(\n listings: readonly AiLayoutListing[],\n screenId: string | null | undefined,\n section: number,\n): { listing: AiLayoutListing; role: AiLayoutListingRole } | null {\n if (!screenId) return null\n for (const listing of listings) {\n const placement = listing.placements.find((entry) => entry.screenId === screenId && entry.section === section)\n if (placement) return { listing, role: placement.role }\n }\n return null\n}\n\n// ── Where a site's pages place them ────────────────────────────────────\n\n/** The plan of a page, as a placement is chosen from it. */\nexport interface AiLayoutListingScreen {\n id?: string | null\n title: string\n slug: string\n sections: ReadonlyArray<{ name: string; items: number; uses?: readonly string[] }>\n}\n\nconst PRODUCT_PAGE = /\\b(shop|store|products?|catalog(?:ue)?|collections?|range|browse)\\b/i\nconst PRODUCT_SECTION =\n /\\b(shop|store|products?|catalog(?:ue)?|collections?|range|bestsellers?|best[- ]sellers?|new arrivals|arrivals|featured|favou?rites|signature)\\b/i\nconst POST_SECTION = /\\b(blog|posts?|articles?|writing|journal|stories|essays?|latest|recent|news|featured)\\b/i\n/** A music site's page or section about its recordings (AGL-3716). */\nconst MUSIC_PAGE = /\\b(music|listen|tracks?|songs?|releases?|discography|albums?|eps?|singles?|sounds?)\\b/i\n\n/**\n * The section of a shop's own page the whole catalog fills (AGL-3676): the\n * first after its opening whose name says products (\"Product grid\", \"The\n * range\"), else the section planned with the most items, else its second.\n * The live Hearth & Wick start (2026-10-09) planned \"Product range image\n * cards\" — six cards naming kinds of candle, with no product, price or cart.\n */\nexport function aiLayoutShopGridSection(sections: ReadonlyArray<{ name: string; items: number }>): number {\n const named = sections.findIndex((section, index) => index > 0 && PRODUCT_SECTION.test(section.name) && !SHOP_FRAMING.test(section.name))\n if (named !== -1) return named\n const most = Math.max(0, ...sections.map((section) => section.items))\n const byItems = most > 0 ? sections.findIndex((section) => section.items === most) : -1\n return byItems !== -1 ? byItems : sections.length > 1 ? 1 : 0\n}\n\n/** A section of a shop page that only opens or closes it: \"Shop intro heading\", \"Gift help call to action\". */\nconst SHOP_FRAMING = /\\b(hero|intro(?:duction)?|heading|banner|cta|call to action|contact)\\b/i\n\n/** Whether a planned page is the store's own Shop page: its address or its name says it sells. */\nexport function aiLayoutIsShopPage(screen: { slug: string; title: string }): boolean {\n return !isHome(screen.slug) && PRODUCT_PAGE.test(`${firstSegment(screen.slug)} ${screen.title}`)\n}\n\nconst firstSegment = (slug: string) => slug.trim().replace(/^\\/+/, '').split('/')[0].toLowerCase()\nconst isHome = (slug: string) => firstSegment(slug) === ''\n\n/**\n * The sections of a site's pages that show a listing, chosen by code from the\n * plan (AGL-3676), never asked of the model:\n *\n * - PRODUCTS. The shop's own page — the page whose address or name says it\n * sells (`/shop`, \"Shop\", \"Products\") — shows the whole catalog in the\n * section the plan gave the most items, else its second; and the home\n * page features a few in its first section after the hero whose name\n * says products (\"Featured candles\", \"Bestsellers\"), else in the first\n * section after the hero planned with items.\n * - POSTS. The home features the latest in its first section of items after\n * the hero whose name says writing (\"Featured writing\", \"Latest posts\"),\n * else its first section of items after the hero; and any other page\n * whose name says it is about the writing does the same. The blog's own\n * index is the platform's.\n *\n * A section placed once is placed for one listing.\n */\nexport function aiLayoutListingPlacements(\n kind: AiLayoutListingKind,\n screens: readonly AiLayoutListingScreen[],\n): AiLayoutListingPlacement[] {\n const placements: AiLayoutListingPlacement[] = []\n const place = (screen: AiLayoutListingScreen, section: number, role: AiLayoutListingRole) => {\n if (typeof screen.id === 'string' && screen.id && section >= 0) placements.push({ screenId: screen.id, section, role })\n }\n const afterHero = (screen: AiLayoutListingScreen, test: (name: string, items: number) => boolean) =>\n screen.sections.findIndex((section, index) => index > 0 && test(section.name, section.items))\n if (kind === 'tracks') {\n // A music site's player (AGL-3716): on its Music page, in the section that\n // says it is about the recordings, else the one after the opening; on the\n // home, only in a section named for them. A site with neither gets it on\n // its home, after the opening.\n for (const screen of screens) {\n if (isHome(screen.slug)) {\n place(screen, afterHero(screen, (name) => MUSIC_PAGE.test(name)), 'featured')\n continue\n }\n if (!MUSIC_PAGE.test(`${firstSegment(screen.slug)} ${screen.title}`)) continue\n const named = afterHero(screen, (name) => MUSIC_PAGE.test(name))\n place(screen, named !== -1 ? named : screen.sections.length > 1 ? 1 : 0, 'index')\n }\n if (!placements.length) {\n const home = screens.find((screen) => isHome(screen.slug))\n if (home) place(home, home.sections.length > 1 ? 1 : 0, 'featured')\n }\n return placements\n }\n if (kind === 'products') {\n for (const screen of screens) {\n if (isHome(screen.slug)) {\n const named = afterHero(screen, (name) => PRODUCT_SECTION.test(name))\n place(screen, named !== -1 ? named : afterHero(screen, (_name, items) => items > 0), 'featured')\n continue\n }\n if (!aiLayoutIsShopPage(screen)) continue\n place(screen, aiLayoutShopGridSection(screen.sections), 'index')\n }\n return placements\n }\n for (const screen of screens) {\n if (isHome(screen.slug)) {\n // On a blog's home the band that names the writing (\"Featured posts\n // cards\", \"Featured journeys\") lists it; failing one, the first band of\n // items after the hero. A live Clay Notes plan (2026-10-09) opened its\n // home with \"What you will find\" before \"Featured posts cards\".\n const named = afterHero(screen, (name, items) => items > 0 && POST_SECTION.test(name))\n place(screen, named !== -1 ? named : afterHero(screen, (_name, items) => items > 0), 'featured')\n continue\n }\n if (!POST_SECTION.test(screen.title)) continue\n place(screen, afterHero(screen, (name) => POST_SECTION.test(name)), 'featured')\n }\n return placements\n}\n"],"names":["AI_LAYOUT_LISTINGS_INPUT","aiLayoutListingId","kind","AI_LAYOUT_LISTING_ELEMENTS","products","posts","tracks","AI_LAYOUT_CART_ELEMENT","AI_LAYOUT_FEATURED_RECORDS","PLACEMENT_ROLES","KINDS","text","value","trim","SITE_PATH","aiLayoutListingsOf","inputs","raw","Array","isArray","listings","entry","record","includes","some","listing","href","action","actionLabel","slice","actionHref","collectionSlug","test","placements","flatMap","placement","screenId","section","role","Number","isInteger","push","id","name","records","map","filter","Boolean","cart","emptyAction","label","aiLayoutListingAt","find","PRODUCT_PAGE","PRODUCT_SECTION","POST_SECTION","MUSIC_PAGE","aiLayoutShopGridSection","sections","named","findIndex","index","SHOP_FRAMING","most","Math","max","items","byItems","length","aiLayoutIsShopPage","screen","isHome","slug","firstSegment","title","replace","split","toLowerCase","aiLayoutListingPlacements","screens","place","afterHero","home","_name"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/layout-language/ai-layout-listings.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 { aiStorefrontSays } from './ai-layout-storefront'\n\n/**\n * A SITE'S OWN RECORDS, PLACED BY THE PLATFORM (AGL-3676).\n *\n * A guided start writes a store's first products and a blog's first posts,\n * then built the pages around them in the layout language — and the pages\n * showed neither. \"Featured candles\" and \"The candle range\" on the live\n * Ember & Wick start (2026-10-08) were cards the model wrote, a name and a\n * line each: no photo, no price, no link, nothing a shopper could open; the\n * Clay Notes blog's \"Featured writing\" was three cards that linked to no\n * post. The real records were on the site, one click away from nowhere.\n *\n * A LISTING is one kind of record a site keeps — its catalog, its blog —\n * placed by code in the sections whose plan says they show it, through the\n * element the plugin that keeps the records renders them with, bound to the\n * records themselves rather than to words about them. It is the form binding\n * one step on (a section whose plan `uses` a form places that form by its\n * id, whatever the answer remembered): the model writes the section's\n * heading and words, the platform places the records.\n *\n * - `products` — the commerce plugin's Product grid (`product-grid`), over\n * the site's whole active catalog: a photo, the name, the price (or\n * \"Price coming soon\" where the owner cleared it), each card linking its\n * product's page.\n * - `posts` — the content plugin's Collection Entries (`collectionEntries`)\n * over the blog's collection, its card a cover, the date and byline, the\n * title and the excerpt, linking each post.\n * - `tracks` — the music plugin's Music player (`musicPlayer`, AGL-3716) on a\n * music site, placed EMPTY: its \"add your tracks\" state, for the owner to\n * fill from their own media library. No job ever sources a recording, so\n * a tracks listing has no records and the player no source.\n * - `reviews` — a selling store's own customer reviews, through the commerce\n * plugin's Product reviews in its store-wide scope (`product-reviews`,\n * AGL-3676): approved reviews across the catalog with their star ratings.\n * A store that opens has none, and the element then shows nothing at all\n * on the published page, so no review is ever written for a shopper who\n * did not leave one (the FTC's fake-review rule; rule 14's \"never invent\n * a customer\").\n * - `signup` — a selling store's newsletter sign-up, through the commerce\n * plugin's Newsletter signup (`newsletter-signup`, AGL-3676): a consented\n * email field into the store's contacts, where a section says newsletter.\n *\n * The elements are named by their persisted ids, as the form binding names\n * `form`: a plugin never imports another's internals, and\n * `ai-layout-listings.spec.ts` holds the ids and props named here to the\n * source of the plugins that declare them.\n *\n * Model only: shapes and the inputs reader; the compiler draws them.\n */\n\nexport type AiLayoutListingKind = 'products' | 'posts' | 'tracks' | 'reviews' | 'signup'\n\n/** How a section shows a listing: a few of its records on a page about something else, or all of them on its own page. */\nexport type AiLayoutListingRole = 'featured' | 'index'\n\n/** One section of one page that places a listing. */\nexport interface AiLayoutListingPlacement {\n /** The page, by its plan id. */\n screenId: string\n /** The section, by plan index. */\n section: number\n role: AiLayoutListingRole\n}\n\n/** A kind of record the site keeps, and where its pages place it. */\nexport interface AiLayoutListing {\n /** `listing:products`, `listing:posts`: what a section's placement names. */\n id: string\n kind: AiLayoutListingKind\n /** What a visitor would call the whole list: \"the shop\", \"the blog\". */\n name: string\n /** The records it shows today, by name, so a page's words can name them. */\n records: string[]\n /** Where the whole list lives, as a path on the site: the blog's `/blog`. */\n href?: string\n /** The content collection a posts listing repeats, by its slug. */\n collectionSlug?: string\n /**\n * Where a visitor goes from a store with nothing listed yet (AGL-3676): the\n * site's contact page, by its path. The empty state names it as its one\n * action, so a shop that opens empty still answers a visitor.\n */\n emptyAction?: { label: string; href: string }\n /**\n * `false` for a store that lists its catalog but cannot sell yet (its first\n * products were skipped or failed, AGL-3676): the header carries no cart.\n */\n cart?: boolean\n placements: AiLayoutListingPlacement[]\n}\n\n/** The unit input a site's listings travel in, to its layout and its pages. */\nexport const AI_LAYOUT_LISTINGS_INPUT = 'siteListings'\n\n/** The id a listing of a kind is placed by. */\nexport function aiLayoutListingId(kind: AiLayoutListingKind): string {\n return `listing:${kind}`\n}\n\n/** The persisted element a listing is drawn with, by kind (held to its plugin's source by a spec). */\nexport const AI_LAYOUT_LISTING_ELEMENTS: Readonly<Record<AiLayoutListingKind, string>> = {\n products: 'product-grid',\n posts: 'collectionEntries',\n tracks: 'musicPlayer',\n reviews: 'product-reviews',\n signup: 'newsletter-signup',\n}\n\n/** The header's cart button: the commerce plugin's Cart in its `button` variant. */\nexport const AI_LAYOUT_CART_ELEMENT = 'cart'\n\n/** The most records a featured band shows; a page of its own shows them all. */\nexport const AI_LAYOUT_FEATURED_RECORDS = { products: 4, posts: 3, tracks: 0, reviews: 3, signup: 0 } as const\n\nconst PLACEMENT_ROLES: readonly AiLayoutListingRole[] = ['featured', 'index']\nconst KINDS: readonly AiLayoutListingKind[] = ['products', 'posts', 'tracks', 'reviews', 'signup']\n\nconst text = (value: unknown): string => (typeof value === 'string' ? value.trim() : '')\n\n/** What a visitor calls each kind's whole list, where a unit's input names none. */\nconst LISTING_NAMES: Readonly<Record<AiLayoutListingKind, string>> = {\n products: 'the shop',\n posts: 'the blog',\n tracks: 'the music',\n reviews: 'the store',\n signup: 'the newsletter',\n}\n\n/** A path on the site, as a listing's links are kept. */\nconst SITE_PATH = /^\\/[a-z0-9-]+(?:\\/[a-z0-9-]+)*$/\n\n/** A site's listings as its unit inputs carry them; nothing that does not read is kept. */\nexport function aiLayoutListingsOf(inputs: Readonly<Record<string, unknown>> | null | undefined): AiLayoutListing[] {\n const raw = inputs?.[AI_LAYOUT_LISTINGS_INPUT]\n if (!Array.isArray(raw)) return []\n const listings: AiLayoutListing[] = []\n for (const entry of raw) {\n if (!entry || typeof entry !== 'object') continue\n const record = entry as Record<string, unknown>\n const kind = record['kind'] as AiLayoutListingKind\n if (!KINDS.includes(kind) || listings.some((listing) => listing.kind === kind)) continue\n const href = text(record['href'])\n const action = (record['emptyAction'] ?? {}) as Record<string, unknown>\n const actionLabel = text(action['label']).slice(0, 40)\n const actionHref = text(action['href'])\n const collectionSlug = text(record['collectionSlug'])\n // A posts listing with no collection to repeat shows nothing.\n if (kind === 'posts' && !/^[a-z0-9-]{1,100}$/.test(collectionSlug)) continue\n const placements = (Array.isArray(record['placements']) ? record['placements'] : []).flatMap((value) => {\n const placement = (value ?? {}) as Record<string, unknown>\n const screenId = text(placement['screenId'])\n const section = placement['section']\n const role = placement['role'] as AiLayoutListingRole\n return screenId && typeof section === 'number' && Number.isInteger(section) && section >= 0 && PLACEMENT_ROLES.includes(role)\n ? [{ screenId, section, role }]\n : []\n })\n listings.push({\n id: aiLayoutListingId(kind),\n kind,\n name: text(record['name']) || LISTING_NAMES[kind],\n // A tracks listing names no records: no job sources a recording (AGL-3716);\n // nor do a store's reviews and its sign-up, which shoppers fill (AGL-3676).\n records:\n kind !== 'products' && kind !== 'posts'\n ? []\n : (Array.isArray(record['records']) ? record['records'] : []).map(text).filter(Boolean).slice(0, 12),\n ...(SITE_PATH.test(href) ? { href } : {}),\n ...(kind === 'posts' ? { collectionSlug } : {}),\n ...(kind === 'products' && record['cart'] === false ? { cart: false } : {}),\n ...(kind === 'products' && actionLabel && SITE_PATH.test(actionHref) ? { emptyAction: { label: actionLabel, href: actionHref } } : {}),\n placements,\n })\n }\n return listings\n}\n\n/** The listing a section of a page places, and how; `null` for a section that places none. */\nexport function aiLayoutListingAt(\n listings: readonly AiLayoutListing[],\n screenId: string | null | undefined,\n section: number,\n): { listing: AiLayoutListing; role: AiLayoutListingRole } | null {\n if (!screenId) return null\n for (const listing of listings) {\n const placement = listing.placements.find((entry) => entry.screenId === screenId && entry.section === section)\n if (placement) return { listing, role: placement.role }\n }\n return null\n}\n\n// ── Where a site's pages place them ────────────────────────────────────\n\n/** The plan of a page, as a placement is chosen from it. */\nexport interface AiLayoutListingScreen {\n id?: string | null\n title: string\n slug: string\n sections: ReadonlyArray<{ name: string; items: number; uses?: readonly string[] }>\n}\n\nconst PRODUCT_PAGE = /\\b(shop|store|products?|catalog(?:ue)?|collections?|range|browse)\\b/i\nconst PRODUCT_SECTION =\n /\\b(shop|store|products?|catalog(?:ue)?|collections?|range|bestsellers?|best[- ]sellers?|new arrivals|arrivals|featured|favou?rites|signature)\\b/i\nconst POST_SECTION = /\\b(blog|posts?|articles?|writing|journal|stories|essays?|latest|recent|news|featured)\\b/i\n/** A music site's page or section about its recordings (AGL-3716). */\nconst MUSIC_PAGE = /\\b(music|listen|tracks?|songs?|releases?|discography|albums?|eps?|singles?|sounds?)\\b/i\n\n/**\n * The section of a shop's own page the whole catalog fills (AGL-3676): the\n * first after its opening whose name says products (\"Product grid\", \"The\n * range\"), else the section planned with the most items, else its second.\n * The live Hearth & Wick start (2026-10-09) planned \"Product range image\n * cards\" — six cards naming kinds of candle, with no product, price or cart.\n */\nexport function aiLayoutShopGridSection(sections: ReadonlyArray<{ name: string; items: number }>): number {\n const named = sections.findIndex((section, index) => index > 0 && PRODUCT_SECTION.test(section.name) && !SHOP_FRAMING.test(section.name))\n if (named !== -1) return named\n const most = Math.max(0, ...sections.map((section) => section.items))\n const byItems = most > 0 ? sections.findIndex((section) => section.items === most) : -1\n return byItems !== -1 ? byItems : sections.length > 1 ? 1 : 0\n}\n\n/**\n * The section of a store's home its bestsellers fill (AGL-3676): the first\n * after the hero whose name says them (\"Bestsellers\", \"Featured candles\", \"New\n * arrivals\"), never one about the store's collections, its reviews or its\n * sign-up; else the first after the hero naming products at all; else the\n * first after the hero planned with items that is none of those. The\n * beta.237 Willow Wick home would otherwise have listed products in its\n * \"Shop by collection\" tiles.\n */\nfunction aiLayoutFeaturedSection(screen: AiLayoutListingScreen): number {\n // A section about the collections that does not also name the bestsellers.\n const other = (name: string) =>\n (aiStorefrontSays('collections', name) && !aiStorefrontSays('featured', name)) ||\n aiStorefrontSays('reviews', name) ||\n aiStorefrontSays('signup', name)\n const first = (test: (name: string, items: number) => boolean) =>\n screen.sections.findIndex((section, index) => index > 0 && test(section.name, section.items))\n for (const test of [\n (name: string) => aiStorefrontSays('featured', name) && !other(name),\n (name: string) => PRODUCT_SECTION.test(name) && !other(name),\n (name: string, items: number) => items > 0 && !other(name),\n (name: string) => PRODUCT_SECTION.test(name),\n ]) {\n const found = first(test)\n if (found !== -1) return found\n }\n return -1\n}\n\n/** A section of a shop page that only opens or closes it: \"Shop intro heading\", \"Gift help call to action\". */\nconst SHOP_FRAMING = /\\b(hero|intro(?:duction)?|heading|banner|cta|call to action|contact)\\b/i\n\n/** Whether a planned page is the store's own Shop page: its address or its name says it sells. */\nexport function aiLayoutIsShopPage(screen: { slug: string; title: string }): boolean {\n return !isHome(screen.slug) && PRODUCT_PAGE.test(`${firstSegment(screen.slug)} ${screen.title}`)\n}\n\nconst firstSegment = (slug: string) => slug.trim().replace(/^\\/+/, '').split('/')[0].toLowerCase()\nconst isHome = (slug: string) => firstSegment(slug) === ''\n\n/**\n * The sections of a site's pages that show a listing, chosen by code from the\n * plan (AGL-3676), never asked of the model:\n *\n * - PRODUCTS. The shop's own page — the page whose address or name says it\n * sells (`/shop`, \"Shop\", \"Products\") — shows the whole catalog in the\n * section the plan gave the most items, else its second; and the home\n * page features a few in its first section after the hero whose name\n * says products (\"Featured candles\", \"Bestsellers\"), else in the first\n * section after the hero planned with items.\n * - POSTS. The home features the latest in its first section of items after\n * the hero whose name says writing (\"Featured writing\", \"Latest posts\"),\n * else its first section of items after the hero; and any other page\n * whose name says it is about the writing does the same. The blog's own\n * index is the platform's.\n *\n * A section placed once is placed for one listing.\n */\nexport function aiLayoutListingPlacements(\n kind: AiLayoutListingKind,\n screens: readonly AiLayoutListingScreen[],\n): AiLayoutListingPlacement[] {\n const placements: AiLayoutListingPlacement[] = []\n const place = (screen: AiLayoutListingScreen, section: number, role: AiLayoutListingRole) => {\n if (typeof screen.id === 'string' && screen.id && section >= 0) placements.push({ screenId: screen.id, section, role })\n }\n const afterHero = (screen: AiLayoutListingScreen, test: (name: string, items: number) => boolean) =>\n screen.sections.findIndex((section, index) => index > 0 && test(section.name, section.items))\n if (kind === 'tracks') {\n // A music site's player (AGL-3716): on its Music page, in the section that\n // says it is about the recordings, else the one after the opening; on the\n // home, only in a section named for them. A site with neither gets it on\n // its home, after the opening.\n for (const screen of screens) {\n if (isHome(screen.slug)) {\n place(screen, afterHero(screen, (name) => MUSIC_PAGE.test(name)), 'featured')\n continue\n }\n if (!MUSIC_PAGE.test(`${firstSegment(screen.slug)} ${screen.title}`)) continue\n const named = afterHero(screen, (name) => MUSIC_PAGE.test(name))\n place(screen, named !== -1 ? named : screen.sections.length > 1 ? 1 : 0, 'index')\n }\n if (!placements.length) {\n const home = screens.find((screen) => isHome(screen.slug))\n if (home) place(home, home.sections.length > 1 ? 1 : 0, 'featured')\n }\n return placements\n }\n if (kind === 'reviews' || kind === 'signup') {\n // A store's own reviews, on its home, in the section named for them; its\n // newsletter sign-up in any page's section named for it that places no\n // form of the plan's (AGL-3676). Neither is ever a page's opening.\n for (const screen of screens) {\n if (kind === 'reviews' && !isHome(screen.slug)) continue\n place(\n screen,\n afterHero(\n screen,\n (name) => aiStorefrontSays(kind, name),\n ),\n kind === 'reviews' ? 'featured' : 'index',\n )\n }\n return kind === 'signup'\n ? placements.filter((placement) => {\n const screen = screens.find((entry) => entry.id === placement.screenId)\n return !(screen?.sections[placement.section]?.uses ?? []).some((use) => /form/i.test(use))\n })\n : placements\n }\n if (kind === 'products') {\n for (const screen of screens) {\n if (isHome(screen.slug)) {\n place(screen, aiLayoutFeaturedSection(screen), 'featured')\n continue\n }\n if (!aiLayoutIsShopPage(screen)) continue\n place(screen, aiLayoutShopGridSection(screen.sections), 'index')\n }\n return placements\n }\n for (const screen of screens) {\n if (isHome(screen.slug)) {\n // On a blog's home the band that names the writing (\"Featured posts\n // cards\", \"Featured journeys\") lists it; failing one, the first band of\n // items after the hero. A live Clay Notes plan (2026-10-09) opened its\n // home with \"What you will find\" before \"Featured posts cards\".\n const named = afterHero(screen, (name, items) => items > 0 && POST_SECTION.test(name))\n place(screen, named !== -1 ? named : afterHero(screen, (_name, items) => items > 0), 'featured')\n continue\n }\n if (!POST_SECTION.test(screen.title)) continue\n place(screen, afterHero(screen, (name) => POST_SECTION.test(name)), 'featured')\n }\n return placements\n}\n"],"names":["aiStorefrontSays","AI_LAYOUT_LISTINGS_INPUT","aiLayoutListingId","kind","AI_LAYOUT_LISTING_ELEMENTS","products","posts","tracks","reviews","signup","AI_LAYOUT_CART_ELEMENT","AI_LAYOUT_FEATURED_RECORDS","PLACEMENT_ROLES","KINDS","text","value","trim","LISTING_NAMES","SITE_PATH","aiLayoutListingsOf","inputs","raw","Array","isArray","listings","entry","record","includes","some","listing","href","action","actionLabel","slice","actionHref","collectionSlug","test","placements","flatMap","placement","screenId","section","role","Number","isInteger","push","id","name","records","map","filter","Boolean","cart","emptyAction","label","aiLayoutListingAt","find","PRODUCT_PAGE","PRODUCT_SECTION","POST_SECTION","MUSIC_PAGE","aiLayoutShopGridSection","sections","named","findIndex","index","SHOP_FRAMING","most","Math","max","items","byItems","length","aiLayoutFeaturedSection","screen","other","first","found","aiLayoutIsShopPage","isHome","slug","firstSegment","title","replace","split","toLowerCase","aiLayoutListingPlacements","screens","place","afterHero","home","uses","use","_name"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,gBAAgB,QAAQ,4BAAwB;AA4FzD,6EAA6E,GAC7E,OAAO,MAAMC,2BAA2B,eAAc;AAEtD,6CAA6C,GAC7C,OAAO,SAASC,kBAAkBC,IAAyB;IACzD,OAAO,CAAC,QAAQ,EAAEA,MAAM;AAC1B;AAEA,oGAAoG,GACpG,OAAO,MAAMC,6BAA4E;IACvFC,UAAU;IACVC,OAAO;IACPC,QAAQ;IACRC,SAAS;IACTC,QAAQ;AACV,EAAC;AAED,kFAAkF,GAClF,OAAO,MAAMC,yBAAyB,OAAM;AAE5C,8EAA8E,GAC9E,OAAO,MAAMC,6BAA6B;IAAEN,UAAU;IAAGC,OAAO;IAAGC,QAAQ;IAAGC,SAAS;IAAGC,QAAQ;AAAE,EAAU;AAE9G,MAAMG,kBAAkD;IAAC;IAAY;CAAQ;AAC7E,MAAMC,QAAwC;IAAC;IAAY;IAAS;IAAU;IAAW;CAAS;AAElG,MAAMC,OAAO,CAACC,QAA4B,OAAOA,UAAU,WAAWA,MAAMC,IAAI,KAAK;AAErF,kFAAkF,GAClF,MAAMC,gBAA+D;IACnEZ,UAAU;IACVC,OAAO;IACPC,QAAQ;IACRC,SAAS;IACTC,QAAQ;AACV;AAEA,uDAAuD,GACvD,MAAMS,YAAY;AAElB,yFAAyF,GACzF,OAAO,SAASC,mBAAmBC,MAA4D;IAC7F,MAAMC,MAAMD,0BAAAA,MAAQ,CAACnB,yBAAyB;IAC9C,IAAI,CAACqB,MAAMC,OAAO,CAACF,MAAM,OAAO,EAAE;IAClC,MAAMG,WAA8B,EAAE;IACtC,KAAK,MAAMC,SAASJ,IAAK;YAMPK;QALhB,IAAI,CAACD,SAAS,OAAOA,UAAU,UAAU;QACzC,MAAMC,SAASD;QACf,MAAMtB,OAAOuB,MAAM,CAAC,OAAO;QAC3B,IAAI,CAACb,MAAMc,QAAQ,CAACxB,SAASqB,SAASI,IAAI,CAAC,CAACC,UAAYA,QAAQ1B,IAAI,KAAKA,OAAO;QAChF,MAAM2B,OAAOhB,KAAKY,MAAM,CAAC,OAAO;QAChC,MAAMK,UAAUL,sBAAAA,MAAM,CAAC,cAAc,YAArBA,sBAAyB,CAAC;QAC1C,MAAMM,cAAclB,KAAKiB,MAAM,CAAC,QAAQ,EAAEE,KAAK,CAAC,GAAG;QACnD,MAAMC,aAAapB,KAAKiB,MAAM,CAAC,OAAO;QACtC,MAAMI,iBAAiBrB,KAAKY,MAAM,CAAC,iBAAiB;QACpD,8DAA8D;QAC9D,IAAIvB,SAAS,WAAW,CAAC,qBAAqBiC,IAAI,CAACD,iBAAiB;QACpE,MAAME,aAAa,AAACf,CAAAA,MAAMC,OAAO,CAACG,MAAM,CAAC,aAAa,IAAIA,MAAM,CAAC,aAAa,GAAG,EAAE,AAAD,EAAGY,OAAO,CAAC,CAACvB;YAC5F,MAAMwB,YAAaxB,gBAAAA,QAAS,CAAC;YAC7B,MAAMyB,WAAW1B,KAAKyB,SAAS,CAAC,WAAW;YAC3C,MAAME,UAAUF,SAAS,CAAC,UAAU;YACpC,MAAMG,OAAOH,SAAS,CAAC,OAAO;YAC9B,OAAOC,YAAY,OAAOC,YAAY,YAAYE,OAAOC,SAAS,CAACH,YAAYA,WAAW,KAAK7B,gBAAgBe,QAAQ,CAACe,QACpH;gBAAC;oBAAEF;oBAAUC;oBAASC;gBAAK;aAAE,GAC7B,EAAE;QACR;QACAlB,SAASqB,IAAI,CAAC;YACZC,IAAI5C,kBAAkBC;YACtBA;YACA4C,MAAMjC,KAAKY,MAAM,CAAC,OAAO,KAAKT,aAAa,CAACd,KAAK;YACjD,4EAA4E;YAC5E,4EAA4E;YAC5E6C,SACE7C,SAAS,cAAcA,SAAS,UAC5B,EAAE,GACF,AAACmB,CAAAA,MAAMC,OAAO,CAACG,MAAM,CAAC,UAAU,IAAIA,MAAM,CAAC,UAAU,GAAG,EAAE,AAAD,EAAGuB,GAAG,CAACnC,MAAMoC,MAAM,CAACC,SAASlB,KAAK,CAAC,GAAG;WACjGf,UAAUkB,IAAI,CAACN,QAAQ;YAAEA;QAAK,IAAI,CAAC,GACnC3B,SAAS,UAAU;YAAEgC;QAAe,IAAI,CAAC,GACzChC,SAAS,cAAcuB,MAAM,CAAC,OAAO,KAAK,QAAQ;YAAE0B,MAAM;QAAM,IAAI,CAAC,GACrEjD,SAAS,cAAc6B,eAAed,UAAUkB,IAAI,CAACF,cAAc;YAAEmB,aAAa;gBAAEC,OAAOtB;gBAAaF,MAAMI;YAAW;QAAE,IAAI,CAAC;YACpIG;;IAEJ;IACA,OAAOb;AACT;AAEA,4FAA4F,GAC5F,OAAO,SAAS+B,kBACd/B,QAAoC,EACpCgB,QAAmC,EACnCC,OAAe;IAEf,IAAI,CAACD,UAAU,OAAO;IACtB,KAAK,MAAMX,WAAWL,SAAU;QAC9B,MAAMe,YAAYV,QAAQQ,UAAU,CAACmB,IAAI,CAAC,CAAC/B,QAAUA,MAAMe,QAAQ,KAAKA,YAAYf,MAAMgB,OAAO,KAAKA;QACtG,IAAIF,WAAW,OAAO;YAAEV;YAASa,MAAMH,UAAUG,IAAI;QAAC;IACxD;IACA,OAAO;AACT;AAYA,MAAMe,eAAe;AACrB,MAAMC,kBACJ;AACF,MAAMC,eAAe;AACrB,oEAAoE,GACpE,MAAMC,aAAa;AAEnB;;;;;;CAMC,GACD,OAAO,SAASC,wBAAwBC,QAAwD;IAC9F,MAAMC,QAAQD,SAASE,SAAS,CAAC,CAACvB,SAASwB,QAAUA,QAAQ,KAAKP,gBAAgBtB,IAAI,CAACK,QAAQM,IAAI,KAAK,CAACmB,aAAa9B,IAAI,CAACK,QAAQM,IAAI;IACvI,IAAIgB,UAAU,CAAC,GAAG,OAAOA;IACzB,MAAMI,OAAOC,KAAKC,GAAG,CAAC,MAAMP,SAASb,GAAG,CAAC,CAACR,UAAYA,QAAQ6B,KAAK;IACnE,MAAMC,UAAUJ,OAAO,IAAIL,SAASE,SAAS,CAAC,CAACvB,UAAYA,QAAQ6B,KAAK,KAAKH,QAAQ,CAAC;IACtF,OAAOI,YAAY,CAAC,IAAIA,UAAUT,SAASU,MAAM,GAAG,IAAI,IAAI;AAC9D;AAEA;;;;;;;;CAQC,GACD,SAASC,wBAAwBC,MAA6B;IAC5D,2EAA2E;IAC3E,MAAMC,QAAQ,CAAC5B,OACb,AAAC/C,iBAAiB,eAAe+C,SAAS,CAAC/C,iBAAiB,YAAY+C,SACxE/C,iBAAiB,WAAW+C,SAC5B/C,iBAAiB,UAAU+C;IAC7B,MAAM6B,QAAQ,CAACxC,OACbsC,OAAOZ,QAAQ,CAACE,SAAS,CAAC,CAACvB,SAASwB,QAAUA,QAAQ,KAAK7B,KAAKK,QAAQM,IAAI,EAAEN,QAAQ6B,KAAK;IAC7F,KAAK,MAAMlC,QAAQ;QACjB,CAACW,OAAiB/C,iBAAiB,YAAY+C,SAAS,CAAC4B,MAAM5B;QAC/D,CAACA,OAAiBW,gBAAgBtB,IAAI,CAACW,SAAS,CAAC4B,MAAM5B;QACvD,CAACA,MAAcuB,QAAkBA,QAAQ,KAAK,CAACK,MAAM5B;QACrD,CAACA,OAAiBW,gBAAgBtB,IAAI,CAACW;KACxC,CAAE;QACD,MAAM8B,QAAQD,MAAMxC;QACpB,IAAIyC,UAAU,CAAC,GAAG,OAAOA;IAC3B;IACA,OAAO,CAAC;AACV;AAEA,6GAA6G,GAC7G,MAAMX,eAAe;AAErB,gGAAgG,GAChG,OAAO,SAASY,mBAAmBJ,MAAuC;IACxE,OAAO,CAACK,OAAOL,OAAOM,IAAI,KAAKvB,aAAarB,IAAI,CAAC,GAAG6C,aAAaP,OAAOM,IAAI,EAAE,CAAC,EAAEN,OAAOQ,KAAK,EAAE;AACjG;AAEA,MAAMD,eAAe,CAACD,OAAiBA,KAAKhE,IAAI,GAAGmE,OAAO,CAAC,QAAQ,IAAIC,KAAK,CAAC,IAAI,CAAC,EAAE,CAACC,WAAW;AAChG,MAAMN,SAAS,CAACC,OAAiBC,aAAaD,UAAU;AAExD;;;;;;;;;;;;;;;;;CAiBC,GACD,OAAO,SAASM,0BACdnF,IAAyB,EACzBoF,OAAyC;IAEzC,MAAMlD,aAAyC,EAAE;IACjD,MAAMmD,QAAQ,CAACd,QAA+BjC,SAAiBC;QAC7D,IAAI,OAAOgC,OAAO5B,EAAE,KAAK,YAAY4B,OAAO5B,EAAE,IAAIL,WAAW,GAAGJ,WAAWQ,IAAI,CAAC;YAAEL,UAAUkC,OAAO5B,EAAE;YAAEL;YAASC;QAAK;IACvH;IACA,MAAM+C,YAAY,CAACf,QAA+BtC,OAChDsC,OAAOZ,QAAQ,CAACE,SAAS,CAAC,CAACvB,SAASwB,QAAUA,QAAQ,KAAK7B,KAAKK,QAAQM,IAAI,EAAEN,QAAQ6B,KAAK;IAC7F,IAAInE,SAAS,UAAU;QACrB,2EAA2E;QAC3E,0EAA0E;QAC1E,yEAAyE;QACzE,+BAA+B;QAC/B,KAAK,MAAMuE,UAAUa,QAAS;YAC5B,IAAIR,OAAOL,OAAOM,IAAI,GAAG;gBACvBQ,MAAMd,QAAQe,UAAUf,QAAQ,CAAC3B,OAASa,WAAWxB,IAAI,CAACW,QAAQ;gBAClE;YACF;YACA,IAAI,CAACa,WAAWxB,IAAI,CAAC,GAAG6C,aAAaP,OAAOM,IAAI,EAAE,CAAC,EAAEN,OAAOQ,KAAK,EAAE,GAAG;YACtE,MAAMnB,QAAQ0B,UAAUf,QAAQ,CAAC3B,OAASa,WAAWxB,IAAI,CAACW;YAC1DyC,MAAMd,QAAQX,UAAU,CAAC,IAAIA,QAAQW,OAAOZ,QAAQ,CAACU,MAAM,GAAG,IAAI,IAAI,GAAG;QAC3E;QACA,IAAI,CAACnC,WAAWmC,MAAM,EAAE;YACtB,MAAMkB,OAAOH,QAAQ/B,IAAI,CAAC,CAACkB,SAAWK,OAAOL,OAAOM,IAAI;YACxD,IAAIU,MAAMF,MAAME,MAAMA,KAAK5B,QAAQ,CAACU,MAAM,GAAG,IAAI,IAAI,GAAG;QAC1D;QACA,OAAOnC;IACT;IACA,IAAIlC,SAAS,aAAaA,SAAS,UAAU;QAC3C,yEAAyE;QACzE,uEAAuE;QACvE,mEAAmE;QACnE,KAAK,MAAMuE,UAAUa,QAAS;YAC5B,IAAIpF,SAAS,aAAa,CAAC4E,OAAOL,OAAOM,IAAI,GAAG;YAChDQ,MACEd,QACAe,UACEf,QACA,CAAC3B,OAAS/C,iBAAiBG,MAAM4C,QAEnC5C,SAAS,YAAY,aAAa;QAEtC;QACA,OAAOA,SAAS,WACZkC,WAAWa,MAAM,CAAC,CAACX;;gBAERmC;YADT,MAAMA,SAASa,QAAQ/B,IAAI,CAAC,CAAC/B,QAAUA,MAAMqB,EAAE,KAAKP,UAAUC,QAAQ;YACtE,OAAO,CAAC,SAACkC,2BAAAA,qCAAAA,OAAQZ,QAAQ,CAACvB,UAAUE,OAAO,CAAC,qBAAnCiC,mCAAqCiB,IAAI,mBAAI,EAAE,EAAE/D,IAAI,CAAC,CAACgE,MAAQ,QAAQxD,IAAI,CAACwD;QACvF,KACAvD;IACN;IACA,IAAIlC,SAAS,YAAY;QACvB,KAAK,MAAMuE,UAAUa,QAAS;YAC5B,IAAIR,OAAOL,OAAOM,IAAI,GAAG;gBACvBQ,MAAMd,QAAQD,wBAAwBC,SAAS;gBAC/C;YACF;YACA,IAAI,CAACI,mBAAmBJ,SAAS;YACjCc,MAAMd,QAAQb,wBAAwBa,OAAOZ,QAAQ,GAAG;QAC1D;QACA,OAAOzB;IACT;IACA,KAAK,MAAMqC,UAAUa,QAAS;QAC5B,IAAIR,OAAOL,OAAOM,IAAI,GAAG;YACvB,oEAAoE;YACpE,wEAAwE;YACxE,uEAAuE;YACvE,gEAAgE;YAChE,MAAMjB,QAAQ0B,UAAUf,QAAQ,CAAC3B,MAAMuB,QAAUA,QAAQ,KAAKX,aAAavB,IAAI,CAACW;YAChFyC,MAAMd,QAAQX,UAAU,CAAC,IAAIA,QAAQ0B,UAAUf,QAAQ,CAACmB,OAAOvB,QAAUA,QAAQ,IAAI;YACrF;QACF;QACA,IAAI,CAACX,aAAavB,IAAI,CAACsC,OAAOQ,KAAK,GAAG;QACtCM,MAAMd,QAAQe,UAAUf,QAAQ,CAAC3B,OAASY,aAAavB,IAAI,CAACW,QAAQ;IACtE;IACA,OAAOV;AACT"}
@@ -53,6 +53,14 @@ export interface AiLayoutPictureSlot {
53
53
  /** The plan index of the section it sits in, or -1. */
54
54
  sectionIndex: number;
55
55
  role: AiLayoutPictureRole;
56
+ /**
57
+ * A product's photo (AGL-3676): what it is of, most specific first — its
58
+ * name, then what its photo should show — searched for with the shop's
59
+ * category and held to it, so a wick trimmer is never a laptop on a desk.
60
+ */
61
+ product?: {
62
+ subjects: readonly string[];
63
+ };
56
64
  }
57
65
  /** A photo a slot is filled with. */
58
66
  export interface AiLayoutPicturePhoto {
@@ -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'\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. Otherwise, or when the search finds nothing fitting, one of the STARTER\n * PHOTOS every new site is born with (`DEFAULT_SITE_IMAGES`, served at\n * `/_static/starter/` by the tenant and the console alike, credited in the\n * CREDITS file beside them). They are the platform's own files, so a\n * visitor's browser asks no other host for them.\n *\n * The slot keeps the alt text the model wrote whichever source fills it.\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\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\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. */\nexport const AI_LAYOUT_STARTER_PHOTOS = DEFAULT_SITE_IMAGES\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 }\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. A slot given no photo is left as\n * 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 slots.forEach((slot, index) => {\n const photo = photos[index]\n const node = next[slot.imageId]\n if (!photo || !node) return\n next[slot.imageId] = {\n ...node,\n props: {\n ...((node['props'] as Record<string, unknown> | undefined) ?? {}),\n src: photo.src,\n intrinsicWidth: photo.width,\n intrinsicHeight: photo.height,\n loading: slot.role === 'hero' ? 'eager' : 'lazy',\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 one photo or `null` per slot, in order.\n * It may throw; a slot it leaves unfilled takes a starter photo.\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, else from the starter photos. Never throws,\n * and never leaves a page worse than it came: a picture is decoration, and a\n * 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 },\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 const starters = aiLayoutStarterPhotos(slots, input.seed)\n return aiLayoutPlacePictures(\n nodes,\n slots,\n slots.map((_slot, index) => found[index] ?? starters[index]),\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","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","STARTER_POOLS","hero","about","gallery","ALL_STARTERS","Object","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","width","height","aiLayoutPlacePictures","photos","next","intrinsicWidth","intrinsicHeight","loading","aiResolveLayoutPictures","sectionIds","sectionNames","found","source","error","console","warn","starters","_slot"],"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'\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. Otherwise, or when the search finds nothing fitting, one of the STARTER\n * PHOTOS every new site is born with (`DEFAULT_SITE_IMAGES`, served at\n * `/_static/starter/` by the tenant and the console alike, credited in the\n * CREDITS file beside them). They are the platform's own files, so a\n * visitor's browser asks no other host for them.\n *\n * The slot keeps the alt text the model wrote whichever source fills it.\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\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. */\nexport const AI_LAYOUT_STARTER_PHOTOS = DEFAULT_SITE_IMAGES\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 }\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. A slot given no photo is left as\n * 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 slots.forEach((slot, index) => {\n const photo = photos[index]\n const node = next[slot.imageId]\n if (!photo || !node) return\n next[slot.imageId] = {\n ...node,\n props: {\n ...((node['props'] as Record<string, unknown> | undefined) ?? {}),\n src: photo.src,\n intrinsicWidth: photo.width,\n intrinsicHeight: photo.height,\n loading: slot.role === 'hero' ? 'eager' : 'lazy',\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 one photo or `null` per slot, in order.\n * It may throw; a slot it leaves unfilled takes a starter photo.\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, else from the starter photos. Never throws,\n * and never leaves a page worse than it came: a picture is decoration, and a\n * 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 },\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 const starters = aiLayoutStarterPhotos(slots, input.seed)\n return aiLayoutPlacePictures(\n nodes,\n slots,\n slots.map((_slot, index) => found[index] ?? starters[index]),\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","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","STARTER_POOLS","hero","about","gallery","ALL_STARTERS","Object","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","width","height","aiLayoutPlacePictures","photos","next","intrinsicWidth","intrinsicHeight","loading","aiResolveLayoutPictures","sectionIds","sectionNames","found","source","error","console","warn","starters","_slot"],"mappings":"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"}
@@ -0,0 +1,82 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ /**
18
+ * A STORE'S STOREFRONT (AGL-3676, second round).
19
+ *
20
+ * Zach, 2026-10-09, on the beta.237 Willow Wick Candles start (job
21
+ * MtlXWjvt9P): "again this … site does not look like a store front". Its home
22
+ * was a lifestyle hero, ONE row of featured products and four text-only bands
23
+ * ("Why soy, why small batch", "How our candles are made", "Made to be
24
+ * loved", "Gifting and shipping"); its Shop page opened with a tall hero and
25
+ * the grid below the fold.
26
+ *
27
+ * A designer storefront (Shopify's Dawn, Craft, Sense; Squarespace's commerce
28
+ * templates) is composed of a handful of PARTS, in a familiar order: a hero
29
+ * that sells, the store's bestsellers, its collections as picture tiles, the
30
+ * brand's story beside a photo, why to buy here with icons, what customers
31
+ * say, and a newsletter sign-up. This module names those parts by the words a
32
+ * plan gives its sections, so the plan step can make sure a store's home has
33
+ * them and the compiler and the listings can draw each one its way.
34
+ *
35
+ * It is NOT a section library (Zach 10/7): the model still names, orders and
36
+ * writes every section, and designs each in the layout language; what code
37
+ * adds is a part the plan left out — named in plain words for the model to
38
+ * design — and the platform elements that show the store's own records.
39
+ *
40
+ * Pure: names and plans only.
41
+ */
42
+ import type { AiBuildPlanScreen } from '../model/ai-build-plan';
43
+ /** A part a storefront home is composed of. */
44
+ export type AiStorefrontPart = 'featured' | 'collections' | 'story' | 'values' | 'reviews' | 'signup';
45
+ /** The section names that say each part, as a plan or a page heading words it. */
46
+ export declare const AI_STOREFRONT_PART_WORDS: Readonly<Record<AiStorefrontPart, RegExp>>;
47
+ /** Whether a planned section's name says a part. */
48
+ export declare function aiStorefrontSays(part: AiStorefrontPart, name: string): boolean;
49
+ /**
50
+ * A store plan's screens with the storefront a shopper expects (AGL-3676):
51
+ *
52
+ * - THE HOME (paid starts only — the Free wall prices each section, and a
53
+ * Free store sells nothing yet): its hero first, then any part the plan
54
+ * left out, added under plain names while the page has room (eight
55
+ * sections), in order of need: the bestsellers the platform lists,
56
+ * right after the hero; the collections as picture tiles after them; the
57
+ * newsletter sign-up as the page's last band; and customer reviews (the
58
+ * store's real ones, which show only once shoppers leave them) before
59
+ * the close.
60
+ * - THE SHOP PAGE: grid first. Its product grid section moves to second,
61
+ * straight under the page's short title, so the products open above the
62
+ * fold; anything the plan put between them (an intro, a story) follows
63
+ * the grid.
64
+ *
65
+ * Every section the plan named stays, in its order otherwise; nothing is
66
+ * removed, and a page at eight sections is given nothing more.
67
+ */
68
+ export declare function aiStorefrontPlanScreens<S extends Pick<AiBuildPlanScreen, 'slug' | 'title' | 'sections'>>(screens: readonly S[], options: {
69
+ paid: boolean;
70
+ isShopPage: (screen: S) => boolean;
71
+ gridSection: (sections: S['sections']) => number;
72
+ }): S[];
73
+ /**
74
+ * The icon each of a store's reasons to buy shows (AGL-3676): its own where
75
+ * the design named one, else one its words suggest, else the next of a few
76
+ * neutral ones — never the same twice in one row.
77
+ */
78
+ export declare function aiStorefrontItemIcons(items: ReadonlyArray<{
79
+ title: string;
80
+ text: string;
81
+ icon?: string;
82
+ }>, known: (word: string | undefined) => boolean): string[];
@@ -0,0 +1,223 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ /**
17
+ * A STORE'S STOREFRONT (AGL-3676, second round).
18
+ *
19
+ * Zach, 2026-10-09, on the beta.237 Willow Wick Candles start (job
20
+ * MtlXWjvt9P): "again this … site does not look like a store front". Its home
21
+ * was a lifestyle hero, ONE row of featured products and four text-only bands
22
+ * ("Why soy, why small batch", "How our candles are made", "Made to be
23
+ * loved", "Gifting and shipping"); its Shop page opened with a tall hero and
24
+ * the grid below the fold.
25
+ *
26
+ * A designer storefront (Shopify's Dawn, Craft, Sense; Squarespace's commerce
27
+ * templates) is composed of a handful of PARTS, in a familiar order: a hero
28
+ * that sells, the store's bestsellers, its collections as picture tiles, the
29
+ * brand's story beside a photo, why to buy here with icons, what customers
30
+ * say, and a newsletter sign-up. This module names those parts by the words a
31
+ * plan gives its sections, so the plan step can make sure a store's home has
32
+ * them and the compiler and the listings can draw each one its way.
33
+ *
34
+ * It is NOT a section library (Zach 10/7): the model still names, orders and
35
+ * writes every section, and designs each in the layout language; what code
36
+ * adds is a part the plan left out — named in plain words for the model to
37
+ * design — and the platform elements that show the store's own records.
38
+ *
39
+ * Pure: names and plans only.
40
+ */ import { _ as _extends } from "@swc/helpers/_/_extends";
41
+ /** The section names that say each part, as a plan or a page heading words it. */ export const AI_STOREFRONT_PART_WORDS = {
42
+ featured: /\b(bestsellers?|best[- ]sellers?|best[- ]selling|featured|new arrivals?|arrivals|just in|favou?rites?|signature|most loved|top picks?|our (?:candles|products|picks)|shop (?:all|now|the))\b/i,
43
+ collections: /\b(collections?|categor(?:y|ies)|shop by|browse by|by scent|scents?|ranges?|families|occasions?|edits?)\b/i,
44
+ story: /\b(story|about|our craft|craft|making|made|maker|process|how (?:we|it)|behind|studio|workshop)\b/i,
45
+ values: /\b(why|promise|promises|values?|difference|shipping|returns?|guarantee|perks?|benefits?|care|small[- ]batch|ingredients?|sustainab\w*)\b/i,
46
+ reviews: /\b(reviews?|testimonials?|customers? (?:say|love)|what (?:people|customers|our customers) say|kind words|praise|loved by|social proof)\b/i,
47
+ signup: /\b(newsletter|subscribe|sign[- ]?up|mailing list|join (?:us|the list|our list)|stay in touch|in the loop|first to know|inbox|letters? from)\b/i
48
+ };
49
+ /** Whether a planned section's name says a part. */ export function aiStorefrontSays(part, name) {
50
+ return AI_STOREFRONT_PART_WORDS[part].test(name);
51
+ }
52
+ /** The plan entry a part the plan left out is added as: plain words the page step designs. */ const ADDED = {
53
+ featured: {
54
+ name: 'Bestsellers',
55
+ uses: [],
56
+ items: 4
57
+ },
58
+ collections: {
59
+ name: 'Shop by collection',
60
+ uses: [],
61
+ items: 3
62
+ },
63
+ reviews: {
64
+ name: 'Customer reviews',
65
+ uses: [],
66
+ items: 0
67
+ },
68
+ signup: {
69
+ name: 'Newsletter sign-up',
70
+ uses: [],
71
+ items: 0
72
+ }
73
+ };
74
+ /** The most sections one page of a site start holds (`AI_SITE_MAX_SECTIONS`). */ const MOST_SECTIONS = 8;
75
+ const firstSegment = (slug)=>slug.trim().replace(/^\/+/, '').split('/')[0].toLowerCase();
76
+ /** A section that closes a page: its call to action, a contact or a sign-up band. */ const CLOSING = /\b(cta|call to action|closing|close|contact|get in touch|visit)\b/i;
77
+ /**
78
+ * A store plan's screens with the storefront a shopper expects (AGL-3676):
79
+ *
80
+ * - THE HOME (paid starts only — the Free wall prices each section, and a
81
+ * Free store sells nothing yet): its hero first, then any part the plan
82
+ * left out, added under plain names while the page has room (eight
83
+ * sections), in order of need: the bestsellers the platform lists,
84
+ * right after the hero; the collections as picture tiles after them; the
85
+ * newsletter sign-up as the page's last band; and customer reviews (the
86
+ * store's real ones, which show only once shoppers leave them) before
87
+ * the close.
88
+ * - THE SHOP PAGE: grid first. Its product grid section moves to second,
89
+ * straight under the page's short title, so the products open above the
90
+ * fold; anything the plan put between them (an intro, a story) follows
91
+ * the grid.
92
+ *
93
+ * Every section the plan named stays, in its order otherwise; nothing is
94
+ * removed, and a page at eight sections is given nothing more.
95
+ */ export function aiStorefrontPlanScreens(screens, options) {
96
+ return screens.map((screen)=>{
97
+ if (firstSegment(screen.slug) === '') return options.paid ? storefrontHome(screen) : screen;
98
+ if (!options.isShopPage(screen) || screen.sections.length < 3) return screen;
99
+ const grid = options.gridSection(screen.sections);
100
+ if (grid <= 1) return screen;
101
+ const sections = [
102
+ ...screen.sections
103
+ ];
104
+ const [moved] = sections.splice(grid, 1);
105
+ sections.splice(1, 0, moved);
106
+ return _extends({}, screen, {
107
+ sections
108
+ });
109
+ });
110
+ }
111
+ function storefrontHome(screen) {
112
+ const sections = [
113
+ ...screen.sections
114
+ ];
115
+ if (!sections.length) return screen;
116
+ // A section about the collections that also names the bestsellers is the bestsellers'.
117
+ const says = (part, name)=>aiStorefrontSays(part, name) && (part !== 'collections' || !aiStorefrontSays('featured', name));
118
+ const has = (part)=>sections.some((section, index)=>index > 0 && says(part, section.name));
119
+ const room = ()=>sections.length < MOST_SECTIONS;
120
+ // What a shopper meets first matters most, so the parts are added in that
121
+ // order while the page has room: the bestsellers, the collections, the
122
+ // sign-up, then the reviews — which show nothing until a shopper writes one.
123
+ if (!has('featured') && room()) sections.splice(1, 0, _extends({}, ADDED.featured));
124
+ if (!has('collections') && room()) {
125
+ // The collections follow the bestsellers.
126
+ const featured = sections.findIndex((section, index)=>index > 0 && says('featured', section.name));
127
+ sections.splice(featured > 0 ? featured + 1 : 1, 0, _extends({}, ADDED.collections));
128
+ }
129
+ // The newsletter is the page's last band, under any closing call to action.
130
+ if (!has('signup') && room()) sections.push(_extends({}, ADDED.signup));
131
+ if (!has('reviews') && room()) {
132
+ // The reviews come just before the page closes: above a closing sign-up
133
+ // and above a closing call to action.
134
+ let at = sections.length;
135
+ if (at - 1 > 0 && says('signup', sections[at - 1].name)) at -= 1;
136
+ if (at - 1 > 0 && CLOSING.test(sections[at - 1].name)) at -= 1;
137
+ sections.splice(at, 0, _extends({}, ADDED.reviews));
138
+ }
139
+ return sections.length === screen.sections.length ? screen : _extends({}, screen, {
140
+ sections
141
+ });
142
+ }
143
+ // ── Icons for a store's reasons to buy ────────────────────────────────────
144
+ /** Words that suggest an icon from the platform's icon library, first match wins. */ const ICON_WORDS = [
145
+ [
146
+ /\b(ship|shipping|shipped|deliver\w*|dispatch\w*|post(?:age|ed)?|courier)\b/i,
147
+ 'truck'
148
+ ],
149
+ [
150
+ /\b(returns?|refunds?|exchanges?)\b/i,
151
+ 'recycle'
152
+ ],
153
+ [
154
+ /\b(gifts?|gifting|wrapped|wrapping|boxed)\b/i,
155
+ 'gift'
156
+ ],
157
+ [
158
+ /\b(natural|soy|organic|plant|vegan|botanical|clean|non-toxic|eco|sustainab\w*|cotton|wood)\b/i,
159
+ 'leaf'
160
+ ],
161
+ [
162
+ /\b(hand|handmade|hand-poured|poured|small[- ]batch|batch|crafted|made|maker|artisan)\b/i,
163
+ 'care'
164
+ ],
165
+ [
166
+ /\b(guarantee\w*|secure|safe|safety|warranty|protected)\b/i,
167
+ 'shield'
168
+ ],
169
+ [
170
+ /\b(fast|quick|same[- ]day|next[- ]day)\b/i,
171
+ 'fast'
172
+ ],
173
+ [
174
+ /\b(quality|award\w*|premium|finest)\b/i,
175
+ 'award'
176
+ ],
177
+ [
178
+ /\b(scents?|fragrances?|aroma\w*|candles?|glow|light|burn\w*)\b/i,
179
+ 'sparkle'
180
+ ],
181
+ [
182
+ /\b(local|town|city|community)\b/i,
183
+ 'location'
184
+ ],
185
+ [
186
+ /\b(support|help|questions?|chat)\b/i,
187
+ 'chat'
188
+ ]
189
+ ];
190
+ /** The fallbacks an item takes in turn, so a row of icons never repeats one. */ const ICON_FALLBACKS = [
191
+ 'star',
192
+ 'heart',
193
+ 'sparkle',
194
+ 'check',
195
+ 'smile',
196
+ 'award'
197
+ ];
198
+ /**
199
+ * The icon each of a store's reasons to buy shows (AGL-3676): its own where
200
+ * the design named one, else one its words suggest, else the next of a few
201
+ * neutral ones — never the same twice in one row.
202
+ */ export function aiStorefrontItemIcons(items, known) {
203
+ const used = new Set();
204
+ const designed = items.map((item)=>{
205
+ if (item.icon && known(item.icon) && !used.has(item.icon)) {
206
+ used.add(item.icon);
207
+ return item.icon;
208
+ }
209
+ return '';
210
+ });
211
+ return designed.map((icon, index)=>{
212
+ var _ref;
213
+ var _ICON_WORDS_find;
214
+ if (icon) return icon;
215
+ const words = `${items[index].title} ${items[index].text}`;
216
+ const suggested = (_ICON_WORDS_find = ICON_WORDS.find(([pattern, word])=>pattern.test(words) && !used.has(word) && known(word))) == null ? void 0 : _ICON_WORDS_find[1];
217
+ const picked = (_ref = suggested != null ? suggested : ICON_FALLBACKS.find((word)=>!used.has(word) && known(word))) != null ? _ref : 'star';
218
+ used.add(picked);
219
+ return picked;
220
+ });
221
+ }
222
+
223
+ //# sourceMappingURL=ai-layout-storefront.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/layout-language/ai-layout-storefront.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 * A STORE'S STOREFRONT (AGL-3676, second round).\n *\n * Zach, 2026-10-09, on the beta.237 Willow Wick Candles start (job\n * MtlXWjvt9P): \"again this … site does not look like a store front\". Its home\n * was a lifestyle hero, ONE row of featured products and four text-only bands\n * (\"Why soy, why small batch\", \"How our candles are made\", \"Made to be\n * loved\", \"Gifting and shipping\"); its Shop page opened with a tall hero and\n * the grid below the fold.\n *\n * A designer storefront (Shopify's Dawn, Craft, Sense; Squarespace's commerce\n * templates) is composed of a handful of PARTS, in a familiar order: a hero\n * that sells, the store's bestsellers, its collections as picture tiles, the\n * brand's story beside a photo, why to buy here with icons, what customers\n * say, and a newsletter sign-up. This module names those parts by the words a\n * plan gives its sections, so the plan step can make sure a store's home has\n * them and the compiler and the listings can draw each one its way.\n *\n * It is NOT a section library (Zach 10/7): the model still names, orders and\n * writes every section, and designs each in the layout language; what code\n * adds is a part the plan left out — named in plain words for the model to\n * design — and the platform elements that show the store's own records.\n *\n * Pure: names and plans only.\n */\n\nimport type { AiBuildPlanScreen, AiBuildPlanSection } from '../model/ai-build-plan'\n\n/** A part a storefront home is composed of. */\nexport type AiStorefrontPart = 'featured' | 'collections' | 'story' | 'values' | 'reviews' | 'signup'\n\n/** The section names that say each part, as a plan or a page heading words it. */\nexport const AI_STOREFRONT_PART_WORDS: Readonly<Record<AiStorefrontPart, RegExp>> = {\n featured:\n /\\b(bestsellers?|best[- ]sellers?|best[- ]selling|featured|new arrivals?|arrivals|just in|favou?rites?|signature|most loved|top picks?|our (?:candles|products|picks)|shop (?:all|now|the))\\b/i,\n collections: /\\b(collections?|categor(?:y|ies)|shop by|browse by|by scent|scents?|ranges?|families|occasions?|edits?)\\b/i,\n story: /\\b(story|about|our craft|craft|making|made|maker|process|how (?:we|it)|behind|studio|workshop)\\b/i,\n values: /\\b(why|promise|promises|values?|difference|shipping|returns?|guarantee|perks?|benefits?|care|small[- ]batch|ingredients?|sustainab\\w*)\\b/i,\n reviews: /\\b(reviews?|testimonials?|customers? (?:say|love)|what (?:people|customers|our customers) say|kind words|praise|loved by|social proof)\\b/i,\n signup: /\\b(newsletter|subscribe|sign[- ]?up|mailing list|join (?:us|the list|our list)|stay in touch|in the loop|first to know|inbox|letters? from)\\b/i,\n}\n\n/** Whether a planned section's name says a part. */\nexport function aiStorefrontSays(part: AiStorefrontPart, name: string): boolean {\n return AI_STOREFRONT_PART_WORDS[part].test(name)\n}\n\n/** The plan entry a part the plan left out is added as: plain words the page step designs. */\nconst ADDED: Readonly<Record<'featured' | 'collections' | 'reviews' | 'signup', AiBuildPlanSection>> = {\n featured: { name: 'Bestsellers', uses: [], items: 4 },\n collections: { name: 'Shop by collection', uses: [], items: 3 },\n reviews: { name: 'Customer reviews', uses: [], items: 0 },\n signup: { name: 'Newsletter sign-up', uses: [], items: 0 },\n}\n\n/** The most sections one page of a site start holds (`AI_SITE_MAX_SECTIONS`). */\nconst MOST_SECTIONS = 8\n\nconst firstSegment = (slug: string) => slug.trim().replace(/^\\/+/, '').split('/')[0].toLowerCase()\n\n/** A section that closes a page: its call to action, a contact or a sign-up band. */\nconst CLOSING = /\\b(cta|call to action|closing|close|contact|get in touch|visit)\\b/i\n\n/**\n * A store plan's screens with the storefront a shopper expects (AGL-3676):\n *\n * - THE HOME (paid starts only — the Free wall prices each section, and a\n * Free store sells nothing yet): its hero first, then any part the plan\n * left out, added under plain names while the page has room (eight\n * sections), in order of need: the bestsellers the platform lists,\n * right after the hero; the collections as picture tiles after them; the\n * newsletter sign-up as the page's last band; and customer reviews (the\n * store's real ones, which show only once shoppers leave them) before\n * the close.\n * - THE SHOP PAGE: grid first. Its product grid section moves to second,\n * straight under the page's short title, so the products open above the\n * fold; anything the plan put between them (an intro, a story) follows\n * the grid.\n *\n * Every section the plan named stays, in its order otherwise; nothing is\n * removed, and a page at eight sections is given nothing more.\n */\nexport function aiStorefrontPlanScreens<S extends Pick<AiBuildPlanScreen, 'slug' | 'title' | 'sections'>>(\n screens: readonly S[],\n options: { paid: boolean; isShopPage: (screen: S) => boolean; gridSection: (sections: S['sections']) => number },\n): S[] {\n return screens.map((screen) => {\n if (firstSegment(screen.slug) === '') return options.paid ? storefrontHome(screen) : screen\n if (!options.isShopPage(screen) || screen.sections.length < 3) return screen\n const grid = options.gridSection(screen.sections)\n if (grid <= 1) return screen\n const sections = [...screen.sections]\n const [moved] = sections.splice(grid, 1)\n sections.splice(1, 0, moved)\n return { ...screen, sections }\n })\n}\n\nfunction storefrontHome<S extends Pick<AiBuildPlanScreen, 'sections'>>(screen: S): S {\n const sections = [...screen.sections]\n if (!sections.length) return screen\n // A section about the collections that also names the bestsellers is the bestsellers'.\n const says = (part: AiStorefrontPart, name: string) =>\n aiStorefrontSays(part, name) && (part !== 'collections' || !aiStorefrontSays('featured', name))\n const has = (part: AiStorefrontPart) => sections.some((section, index) => index > 0 && says(part, section.name))\n const room = () => sections.length < MOST_SECTIONS\n // What a shopper meets first matters most, so the parts are added in that\n // order while the page has room: the bestsellers, the collections, the\n // sign-up, then the reviews — which show nothing until a shopper writes one.\n if (!has('featured') && room()) sections.splice(1, 0, { ...ADDED.featured })\n if (!has('collections') && room()) {\n // The collections follow the bestsellers.\n const featured = sections.findIndex((section, index) => index > 0 && says('featured', section.name))\n sections.splice(featured > 0 ? featured + 1 : 1, 0, { ...ADDED.collections })\n }\n // The newsletter is the page's last band, under any closing call to action.\n if (!has('signup') && room()) sections.push({ ...ADDED.signup })\n if (!has('reviews') && room()) {\n // The reviews come just before the page closes: above a closing sign-up\n // and above a closing call to action.\n let at = sections.length\n if (at - 1 > 0 && says('signup', sections[at - 1].name)) at -= 1\n if (at - 1 > 0 && CLOSING.test(sections[at - 1].name)) at -= 1\n sections.splice(at, 0, { ...ADDED.reviews })\n }\n return sections.length === screen.sections.length ? screen : { ...screen, sections }\n}\n\n// ── Icons for a store's reasons to buy ────────────────────────────────────\n\n/** Words that suggest an icon from the platform's icon library, first match wins. */\nconst ICON_WORDS: ReadonlyArray<readonly [RegExp, string]> = [\n [/\\b(ship|shipping|shipped|deliver\\w*|dispatch\\w*|post(?:age|ed)?|courier)\\b/i, 'truck'],\n [/\\b(returns?|refunds?|exchanges?)\\b/i, 'recycle'],\n [/\\b(gifts?|gifting|wrapped|wrapping|boxed)\\b/i, 'gift'],\n [/\\b(natural|soy|organic|plant|vegan|botanical|clean|non-toxic|eco|sustainab\\w*|cotton|wood)\\b/i, 'leaf'],\n [/\\b(hand|handmade|hand-poured|poured|small[- ]batch|batch|crafted|made|maker|artisan)\\b/i, 'care'],\n [/\\b(guarantee\\w*|secure|safe|safety|warranty|protected)\\b/i, 'shield'],\n [/\\b(fast|quick|same[- ]day|next[- ]day)\\b/i, 'fast'],\n [/\\b(quality|award\\w*|premium|finest)\\b/i, 'award'],\n [/\\b(scents?|fragrances?|aroma\\w*|candles?|glow|light|burn\\w*)\\b/i, 'sparkle'],\n [/\\b(local|town|city|community)\\b/i, 'location'],\n [/\\b(support|help|questions?|chat)\\b/i, 'chat'],\n]\n\n/** The fallbacks an item takes in turn, so a row of icons never repeats one. */\nconst ICON_FALLBACKS = ['star', 'heart', 'sparkle', 'check', 'smile', 'award'] as const\n\n/**\n * The icon each of a store's reasons to buy shows (AGL-3676): its own where\n * the design named one, else one its words suggest, else the next of a few\n * neutral ones — never the same twice in one row.\n */\nexport function aiStorefrontItemIcons(\n items: ReadonlyArray<{ title: string; text: string; icon?: string }>,\n known: (word: string | undefined) => boolean,\n): string[] {\n const used = new Set<string>()\n const designed = items.map((item) => {\n if (item.icon && known(item.icon) && !used.has(item.icon)) {\n used.add(item.icon)\n return item.icon\n }\n return ''\n })\n return designed.map((icon, index) => {\n if (icon) return icon\n const words = `${items[index].title} ${items[index].text}`\n const suggested = ICON_WORDS.find(([pattern, word]) => pattern.test(words) && !used.has(word) && known(word))?.[1]\n const picked = suggested ?? ICON_FALLBACKS.find((word) => !used.has(word) && known(word)) ?? 'star'\n used.add(picked)\n return picked\n })\n}\n"],"names":["AI_STOREFRONT_PART_WORDS","featured","collections","story","values","reviews","signup","aiStorefrontSays","part","name","test","ADDED","uses","items","MOST_SECTIONS","firstSegment","slug","trim","replace","split","toLowerCase","CLOSING","aiStorefrontPlanScreens","screens","options","map","screen","paid","storefrontHome","isShopPage","sections","length","grid","gridSection","moved","splice","says","has","some","section","index","room","findIndex","push","at","ICON_WORDS","ICON_FALLBACKS","aiStorefrontItemIcons","known","used","Set","designed","item","icon","add","suggested","words","title","text","find","pattern","word","picked"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;CAwBC;AAOD,gFAAgF,GAChF,OAAO,MAAMA,2BAAuE;IAClFC,UACE;IACFC,aAAa;IACbC,OAAO;IACPC,QAAQ;IACRC,SAAS;IACTC,QAAQ;AACV,EAAC;AAED,kDAAkD,GAClD,OAAO,SAASC,iBAAiBC,IAAsB,EAAEC,IAAY;IACnE,OAAOT,wBAAwB,CAACQ,KAAK,CAACE,IAAI,CAACD;AAC7C;AAEA,4FAA4F,GAC5F,MAAME,QAAiG;IACrGV,UAAU;QAAEQ,MAAM;QAAeG,MAAM,EAAE;QAAEC,OAAO;IAAE;IACpDX,aAAa;QAAEO,MAAM;QAAsBG,MAAM,EAAE;QAAEC,OAAO;IAAE;IAC9DR,SAAS;QAAEI,MAAM;QAAoBG,MAAM,EAAE;QAAEC,OAAO;IAAE;IACxDP,QAAQ;QAAEG,MAAM;QAAsBG,MAAM,EAAE;QAAEC,OAAO;IAAE;AAC3D;AAEA,+EAA+E,GAC/E,MAAMC,gBAAgB;AAEtB,MAAMC,eAAe,CAACC,OAAiBA,KAAKC,IAAI,GAAGC,OAAO,CAAC,QAAQ,IAAIC,KAAK,CAAC,IAAI,CAAC,EAAE,CAACC,WAAW;AAEhG,mFAAmF,GACnF,MAAMC,UAAU;AAEhB;;;;;;;;;;;;;;;;;;CAkBC,GACD,OAAO,SAASC,wBACdC,OAAqB,EACrBC,OAAgH;IAEhH,OAAOD,QAAQE,GAAG,CAAC,CAACC;QAClB,IAAIX,aAAaW,OAAOV,IAAI,MAAM,IAAI,OAAOQ,QAAQG,IAAI,GAAGC,eAAeF,UAAUA;QACrF,IAAI,CAACF,QAAQK,UAAU,CAACH,WAAWA,OAAOI,QAAQ,CAACC,MAAM,GAAG,GAAG,OAAOL;QACtE,MAAMM,OAAOR,QAAQS,WAAW,CAACP,OAAOI,QAAQ;QAChD,IAAIE,QAAQ,GAAG,OAAON;QACtB,MAAMI,WAAW;eAAIJ,OAAOI,QAAQ;SAAC;QACrC,MAAM,CAACI,MAAM,GAAGJ,SAASK,MAAM,CAACH,MAAM;QACtCF,SAASK,MAAM,CAAC,GAAG,GAAGD;QACtB,OAAO,aAAKR;YAAQI;;IACtB;AACF;AAEA,SAASF,eAA8DF,MAAS;IAC9E,MAAMI,WAAW;WAAIJ,OAAOI,QAAQ;KAAC;IACrC,IAAI,CAACA,SAASC,MAAM,EAAE,OAAOL;IAC7B,uFAAuF;IACvF,MAAMU,OAAO,CAAC5B,MAAwBC,OACpCF,iBAAiBC,MAAMC,SAAUD,CAAAA,SAAS,iBAAiB,CAACD,iBAAiB,YAAYE,KAAI;IAC/F,MAAM4B,MAAM,CAAC7B,OAA2BsB,SAASQ,IAAI,CAAC,CAACC,SAASC,QAAUA,QAAQ,KAAKJ,KAAK5B,MAAM+B,QAAQ9B,IAAI;IAC9G,MAAMgC,OAAO,IAAMX,SAASC,MAAM,GAAGjB;IACrC,0EAA0E;IAC1E,uEAAuE;IACvE,6EAA6E;IAC7E,IAAI,CAACuB,IAAI,eAAeI,QAAQX,SAASK,MAAM,CAAC,GAAG,GAAG,aAAKxB,MAAMV,QAAQ;IACzE,IAAI,CAACoC,IAAI,kBAAkBI,QAAQ;QACjC,0CAA0C;QAC1C,MAAMxC,WAAW6B,SAASY,SAAS,CAAC,CAACH,SAASC,QAAUA,QAAQ,KAAKJ,KAAK,YAAYG,QAAQ9B,IAAI;QAClGqB,SAASK,MAAM,CAAClC,WAAW,IAAIA,WAAW,IAAI,GAAG,GAAG,aAAKU,MAAMT,WAAW;IAC5E;IACA,4EAA4E;IAC5E,IAAI,CAACmC,IAAI,aAAaI,QAAQX,SAASa,IAAI,CAAC,aAAKhC,MAAML,MAAM;IAC7D,IAAI,CAAC+B,IAAI,cAAcI,QAAQ;QAC7B,wEAAwE;QACxE,sCAAsC;QACtC,IAAIG,KAAKd,SAASC,MAAM;QACxB,IAAIa,KAAK,IAAI,KAAKR,KAAK,UAAUN,QAAQ,CAACc,KAAK,EAAE,CAACnC,IAAI,GAAGmC,MAAM;QAC/D,IAAIA,KAAK,IAAI,KAAKvB,QAAQX,IAAI,CAACoB,QAAQ,CAACc,KAAK,EAAE,CAACnC,IAAI,GAAGmC,MAAM;QAC7Dd,SAASK,MAAM,CAACS,IAAI,GAAG,aAAKjC,MAAMN,OAAO;IAC3C;IACA,OAAOyB,SAASC,MAAM,KAAKL,OAAOI,QAAQ,CAACC,MAAM,GAAGL,SAAS,aAAKA;QAAQI;;AAC5E;AAEA,6EAA6E;AAE7E,mFAAmF,GACnF,MAAMe,aAAuD;IAC3D;QAAC;QAA+E;KAAQ;IACxF;QAAC;QAAuC;KAAU;IAClD;QAAC;QAAgD;KAAO;IACxD;QAAC;QAAiG;KAAO;IACzG;QAAC;QAA2F;KAAO;IACnG;QAAC;QAA6D;KAAS;IACvE;QAAC;QAA6C;KAAO;IACrD;QAAC;QAA0C;KAAQ;IACnD;QAAC;QAAmE;KAAU;IAC9E;QAAC;QAAoC;KAAW;IAChD;QAAC;QAAuC;KAAO;CAChD;AAED,8EAA8E,GAC9E,MAAMC,iBAAiB;IAAC;IAAQ;IAAS;IAAW;IAAS;IAAS;CAAQ;AAE9E;;;;CAIC,GACD,OAAO,SAASC,sBACdlC,KAAoE,EACpEmC,KAA4C;IAE5C,MAAMC,OAAO,IAAIC;IACjB,MAAMC,WAAWtC,MAAMY,GAAG,CAAC,CAAC2B;QAC1B,IAAIA,KAAKC,IAAI,IAAIL,MAAMI,KAAKC,IAAI,KAAK,CAACJ,KAAKZ,GAAG,CAACe,KAAKC,IAAI,GAAG;YACzDJ,KAAKK,GAAG,CAACF,KAAKC,IAAI;YAClB,OAAOD,KAAKC,IAAI;QAClB;QACA,OAAO;IACT;IACA,OAAOF,SAAS1B,GAAG,CAAC,CAAC4B,MAAMb;YAIVe;YADGV;QAFlB,IAAIQ,MAAM,OAAOA;QACjB,MAAMG,QAAQ,GAAG3C,KAAK,CAAC2B,MAAM,CAACiB,KAAK,CAAC,CAAC,EAAE5C,KAAK,CAAC2B,MAAM,CAACkB,IAAI,EAAE;QAC1D,MAAMH,aAAYV,mBAAAA,WAAWc,IAAI,CAAC,CAAC,CAACC,SAASC,KAAK,GAAKD,QAAQlD,IAAI,CAAC8C,UAAU,CAACP,KAAKZ,GAAG,CAACwB,SAASb,MAAMa,2BAArFhB,gBAA6F,CAAC,EAAE;QAClH,MAAMiB,UAASP,OAAAA,oBAAAA,YAAaT,eAAea,IAAI,CAAC,CAACE,OAAS,CAACZ,KAAKZ,GAAG,CAACwB,SAASb,MAAMa,kBAApEN,OAA8E;QAC7FN,KAAKK,GAAG,CAACQ;QACT,OAAOA;IACT;AACF"}
@@ -44,6 +44,14 @@ export declare const AI_BUILD_LIMITS: {
44
44
  /** A Free workspace's pages. */
45
45
  readonly freePages: 2;
46
46
  };
47
+ /**
48
+ * What a Free build's plan card says when it holds as many pages as a Free
49
+ * build makes (AGL-3722): the plan is told the cap and plans the first pages
50
+ * a request asks for, so a request for three — "About us, Custom cakes and
51
+ * Catering", 2026-10-10 — came back as two with nothing saying why. `null`
52
+ * when the plan holds fewer, or on a paid workspace.
53
+ */
54
+ export declare function aiBuildFreePageCapNote(plan: Pick<AiBuildPlan, 'screens'>, freeTaste: boolean): string | null;
47
55
  /** What a build creates through its `create` list; everything else beyond pages is an item. */
48
56
  export declare const AI_BUILD_CREATE_KINDS: readonly AiBuildPlanCreateKind[];
49
57
  /**
@@ -127,7 +135,7 @@ export declare function aiBuildCreditEstimate(plan: AiBuildPlan, options?: {
127
135
  export declare function aiJobCreditEstimate(kind: string, plan: AiBuildPlan): number;
128
136
  /**
129
137
  * What one unit is likely to cost, its p90 and its ceiling (AGL-3722): a page
130
- * by its sections around the measured page, a creation at what its kind
138
+ * as a page written on its own measured (a fixed cost and one per section), a creation at what its kind
131
139
  * measured, an item at the measured pass times the passes its capability
132
140
  * estimates (none for a draft another plugin writes, which asks no model).
133
141
  * The ceiling is `aiBuildUnitCreditEstimate`, unchanged.