@aglyn/plugins-ai 1.0.0-beta.262 → 1.0.0-beta.264
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +17 -17
- package/src/lib/insights/ai-figure-summaries.d.ts +1 -1
- package/src/lib/insights/ai-figure-summaries.js +55 -38
- package/src/lib/insights/ai-figure-summaries.js.map +1 -1
- package/src/lib/insights/ai-insight-digest.d.ts +2 -0
- package/src/lib/insights/ai-insight-digest.js +7 -4
- package/src/lib/insights/ai-insight-digest.js.map +1 -1
- package/src/lib/jobs/ai-job-insight-step.d.ts +1 -1
- package/src/lib/jobs/ai-job-insight-step.js +5 -3
- package/src/lib/jobs/ai-job-insight-step.js.map +1 -1
- package/src/lib/jobs/ai-job-page-language.d.ts +2 -0
- package/src/lib/jobs/ai-job-page-language.js +23 -2
- package/src/lib/jobs/ai-job-page-language.js.map +1 -1
- package/src/lib/jobs/ai-job-page-sections.js +8 -2
- package/src/lib/jobs/ai-job-page-sections.js.map +1 -1
- package/src/lib/jobs/ai-job-plan-step.d.ts +14 -1
- package/src/lib/jobs/ai-job-plan-step.js +44 -9
- package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
- package/src/lib/jobs/ai-job-site-datasets.d.ts +11 -1
- package/src/lib/jobs/ai-job-site-datasets.js +32 -7
- package/src/lib/jobs/ai-job-site-datasets.js.map +1 -1
- package/src/lib/jobs/ai-jobs-notify.d.ts +8 -1
- package/src/lib/jobs/ai-jobs-notify.js +44 -2
- package/src/lib/jobs/ai-jobs-notify.js.map +1 -1
- package/src/lib/jobs/ai-layout-stock-photos.d.ts +91 -11
- package/src/lib/jobs/ai-layout-stock-photos.js +1046 -48
- package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
- package/src/lib/jobs/fixtures/ai-stock-photo-juniper-records-recordings.d.ts +23 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-juniper-records-recordings.js +163 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-juniper-records-recordings.js.map +1 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-learning-center-recordings.d.ts +23 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-learning-center-recordings.js +53 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-learning-center-recordings.js.map +1 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-synchronicity-recordings.d.ts +21 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-synchronicity-recordings.js +119 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-synchronicity-recordings.js.map +1 -0
- package/src/lib/layout-language/ai-layout-compiler.js +24 -11
- package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
- package/src/lib/layout-language/ai-layout-listings.d.ts +7 -0
- package/src/lib/layout-language/ai-layout-listings.js +12 -0
- package/src/lib/layout-language/ai-layout-listings.js.map +1 -1
- package/src/lib/layout-language/ai-layout-pictures.d.ts +27 -8
- package/src/lib/layout-language/ai-layout-pictures.js +85 -4
- package/src/lib/layout-language/ai-layout-pictures.js.map +1 -1
- package/src/lib/layout-language/ai-layout-promised-copy.d.ts +54 -0
- package/src/lib/layout-language/ai-layout-promised-copy.js +104 -0
- package/src/lib/layout-language/ai-layout-promised-copy.js.map +1 -0
- package/src/lib/model/ai-insight.d.ts +9 -0
- package/src/lib/model/ai-insight.js +12 -0
- package/src/lib/model/ai-insight.js.map +1 -1
- package/src/lib/model/ai-job-notice.d.ts +7 -1
- package/src/lib/model/ai-job-notice.js +10 -9
- package/src/lib/model/ai-job-notice.js.map +1 -1
- package/src/lib/model/ai-plan-said-sections.d.ts +81 -0
- package/src/lib/model/ai-plan-said-sections.js +168 -0
- package/src/lib/model/ai-plan-said-sections.js.map +1 -0
- package/src/lib/model/ai-site-job.d.ts +8 -2
- package/src/lib/model/ai-site-job.js +7 -1
- package/src/lib/model/ai-site-job.js.map +1 -1
- package/src/lib/runtime/ai-doctrine-validators.d.ts +15 -1
- package/src/lib/runtime/ai-doctrine-validators.js +80 -1
- package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
- package/src/lib/runtime/ai-palette.generated.js +11 -3
- package/src/lib/runtime/ai-palette.generated.js.map +1 -1
|
@@ -254,3 +254,10 @@ export declare function aiLayoutRecordsPlacements(dataset: {
|
|
|
254
254
|
* all. Chosen from the plan, never asked of the model.
|
|
255
255
|
*/
|
|
256
256
|
export declare function aiLayoutEventsPlacements(names: readonly string[], screens: readonly AiLayoutListingScreen[]): AiLayoutListingPlacement[];
|
|
257
|
+
/** Whether a field's name says it holds what a record's photo shows (AGL-3856). */
|
|
258
|
+
export declare function aiLayoutIsRecordImageAlt(name: string): boolean;
|
|
259
|
+
/** The field holding what a record's photo shows, by id, or `null` (AGL-3856). */
|
|
260
|
+
export declare function aiLayoutRecordImageAltField(fields: readonly {
|
|
261
|
+
id: string;
|
|
262
|
+
name: string;
|
|
263
|
+
}[]): string | null;
|
|
@@ -370,5 +370,17 @@ const isHome = (slug)=>firstSegment(slug) === '';
|
|
|
370
370
|
}
|
|
371
371
|
return placements;
|
|
372
372
|
}
|
|
373
|
+
/**
|
|
374
|
+
* A field whose name says it holds what a record's photo shows (AGL-3856):
|
|
375
|
+
* the alt text of the record's Image, never words of its card.
|
|
376
|
+
*/ const RECORD_IMAGE_ALT = /^(?:image|photo|picture) (?:description|alt(?: text)?)$/i;
|
|
377
|
+
/** Whether a field's name says it holds what a record's photo shows (AGL-3856). */ export function aiLayoutIsRecordImageAlt(name) {
|
|
378
|
+
return RECORD_IMAGE_ALT.test(name.trim());
|
|
379
|
+
}
|
|
380
|
+
/** The field holding what a record's photo shows, by id, or `null` (AGL-3856). */ export function aiLayoutRecordImageAltField(fields) {
|
|
381
|
+
var _ref;
|
|
382
|
+
var _fields_find;
|
|
383
|
+
return (_ref = (_fields_find = fields.find((field)=>aiLayoutIsRecordImageAlt(field.name))) == null ? void 0 : _fields_find.id) != null ? _ref : null;
|
|
384
|
+
}
|
|
373
385
|
|
|
374
386
|
//# sourceMappingURL=ai-layout-listings.js.map
|
|
@@ -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\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 * - `records` — one of the site's datasets (AGL-3616): a menu, a team, its\n * services, built by the site start and kept by the data plugin. Drawn as\n * a Box that repeats over the dataset (`repeatDataset`, the platform's own\n * repeat), its card binding the record's fields as `{{item.<field>}}`, in\n * the sections whose plan names the dataset in its `uses`. A site may list\n * several, one listing a dataset.\n * - `events` — the site's dated events (AGL-3755), which a guided start of\n * an event-led business drafts through the events-calendar plugin, listed\n * by its Event List (`eventList`) in the section whose plan names them in\n * its `uses`: each published event with its date, place and cover, and\n * its search-engine Event markup.\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' | 'records' | 'events'\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/** One field of a dataset a `records` listing shows, as its card binds it. */\nexport interface AiLayoutListingField {\n /** The field's id, which `{{item.<id>}}` reads. */\n id: string\n name: string\n /** The data plugin's stored type: `text`, `float`, `int32`, `bool`, `sorted`. */\n type: string\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 /** The dataset a records listing repeats over, by id (AGL-3616). */\n datasetId?: string\n /** The dataset's fields, in its order, which the card binds. */\n fields?: AiLayoutListingField[]\n /**\n * The field holding each record's photo (AGL-3616), which the card leads\n * with as its Image's `src`, so a portfolio stays a gallery. Never among\n * `fields`: it is the card's picture, not its words.\n */\n imageField?: string\n /**\n * Where each record's own page lives (AGL-3727): the base path of the\n * dataset's record template (AGL-3475), without slashes at either end —\n * `services` for `/services/roofing`. Set by code where the plan makes or\n * the site keeps a record template for the dataset, never by the model.\n */\n recordBase?: string\n /**\n * The record's page-address field (AGL-3727), which the record's page is\n * served at under {@link recordBase}. Never among `fields`: it is the card's\n * link, not its words.\n */\n addressField?: 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; a records listing is one dataset's. */\nexport function aiLayoutListingId(kind: AiLayoutListingKind, datasetId?: string): string {\n return kind === 'records' && datasetId ? `listing:records:${datasetId}` : `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 // The platform's own Box, repeating its card over the dataset.\n records: 'muiBox',\n events: 'eventList',\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, records: 3, events: 3 } as const\n\nconst PLACEMENT_ROLES: readonly AiLayoutListingRole[] = ['featured', 'index']\nconst KINDS: readonly AiLayoutListingKind[] = ['products', 'posts', 'tracks', 'reviews', 'signup', 'records', 'events']\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 records: 'the list',\n events: 'the events',\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 dataset id and a field id, as the data plugin mints them. */\nconst DATASET_ID = /^[A-Za-z0-9_-]{1,128}$/\nconst FIELD_ID = /^[A-Za-z][A-Za-z0-9_]{0,63}$/\n\n/** A record template's base: one to four URL-safe segments, as the data plugin keeps it. */\nconst RECORD_BASE = /^[a-z0-9]+(?:-[a-z0-9]+)*(?:\\/[a-z0-9]+(?:-[a-z0-9]+)*){0,3}$/\n\n/**\n * The link each card of a records listing opens (AGL-3727): the record's own\n * page, `/{base}/{{item.<address field>}}`, which the page fills per record\n * as it repeats the card. `null` where the dataset has no record page, or the\n * listing names no address to reach it by.\n */\nexport function aiLayoutRecordHref(listing: Pick<AiLayoutListing, 'kind' | 'recordBase' | 'addressField'>): string | null {\n if (listing.kind !== 'records') return null\n const base = text(listing.recordBase).replace(/^\\/+|\\/+$/g, '')\n const field = text(listing.addressField)\n return RECORD_BASE.test(base) && FIELD_ID.test(field) ? `/${base}/{{item.${field}}}` : null\n}\n\n/** A records listing's fields as its input carries them; nothing that does not read is kept. */\nfunction listingFieldsOf(raw: unknown): AiLayoutListingField[] {\n if (!Array.isArray(raw)) return []\n return raw\n .flatMap((value) => {\n const field = (value ?? {}) as Record<string, unknown>\n const id = text(field['id'])\n return FIELD_ID.test(id) ? [{ id, name: text(field['name']).slice(0, 60) || id, type: text(field['type']) || 'text' }] : []\n })\n .slice(0, 16)\n}\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 // A records listing is one dataset's (AGL-3616): a site may list several.\n const datasetId = kind === 'records' ? text(record['datasetId']) : ''\n const fields = kind === 'records' ? listingFieldsOf(record['fields']) : []\n const imageField = kind === 'records' ? text(record['imageField']) : ''\n const recordBase = kind === 'records' ? text(record['recordBase']).replace(/^\\/+|\\/+$/g, '') : ''\n const addressField = kind === 'records' ? text(record['addressField']) : ''\n // Its fields may be left for the page to read off the site's inventory.\n if (kind === 'records' && !DATASET_ID.test(datasetId)) continue\n if (\n !KINDS.includes(kind) ||\n listings.some((listing) => listing.kind === kind && (kind !== 'records' || listing.datasetId === datasetId))\n ) {\n continue\n }\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, datasetId),\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' && kind !== 'records' && kind !== 'events'\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 === 'records' ? { datasetId, fields } : {}),\n ...(kind === 'records' && FIELD_ID.test(imageField) ? { imageField } : {}),\n // A record page's base, and the address field where the unit knew it; a\n // build's page reads the field off the site's inventory (AGL-3727).\n ...(kind === 'records' && RECORD_BASE.test(recordBase) ? { recordBase } : {}),\n ...(kind === 'records' && RECORD_BASE.test(recordBase) && FIELD_ID.test(addressField) ? { addressField } : {}),\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 /** The dataset this page is the record template of, and its base (AGL-3475); absent for a page that is one page. */\n record?: { dataset: string; base: string } | null\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\n/**\n * Where a dataset's records have pages of their own (AGL-3727): the base of\n * the page the plan makes its record template (AGL-3475), named by its id or\n * as `new:<its name>`; `null` where no page is. Read off the plan, never\n * asked of the model, so every card that lists the dataset links its record.\n */\nexport function aiLayoutRecordBaseOf(\n dataset: { id: string; name: string },\n screens: readonly AiLayoutListingScreen[],\n): string | null {\n const named = `new:${dataset.name}`.trim().toLowerCase()\n for (const screen of screens) {\n const ref = text(screen.record?.dataset)\n if (!ref || (ref !== dataset.id && ref.toLowerCase() !== named)) continue\n const base = text(screen.record?.base).replace(/^\\/+|\\/+$/g, '')\n if (RECORD_BASE.test(base)) return base\n }\n return null\n}\n\n/**\n * The sections that list one of the site's datasets (AGL-3616): every section\n * whose plan names it in its `uses` — `new:<its name>` as the plan wrote it,\n * or its id once built. The home page features a few of its records; any\n * other page lists them all. Chosen from the plan, never asked of the model.\n */\nexport function aiLayoutRecordsPlacements(\n dataset: { id: string; name: string },\n screens: readonly AiLayoutListingScreen[],\n): AiLayoutListingPlacement[] {\n const named = `new:${dataset.name}`.trim().toLowerCase()\n const placements: AiLayoutListingPlacement[] = []\n for (const screen of screens) {\n if (typeof screen.id !== 'string' || !screen.id) continue\n screen.sections.forEach((section, index) => {\n const lists = (section.uses ?? []).some((ref) => ref === dataset.id || ref.trim().toLowerCase() === named)\n if (lists) placements.push({ screenId: screen.id as string, section: index, role: isHome(screen.slug) ? 'featured' : 'index' })\n })\n }\n return placements\n}\n\n/**\n * The sections that list the site's events (AGL-3755): every section whose\n * plan names one of them in its `uses` (`new:<its name>`), once however many\n * it names. The home page features the next few; any other page lists them\n * all. Chosen from the plan, never asked of the model.\n */\nexport function aiLayoutEventsPlacements(\n names: readonly string[],\n screens: readonly AiLayoutListingScreen[],\n): AiLayoutListingPlacement[] {\n const named = new Set(names.map((name) => `new:${name}`.trim().toLowerCase()))\n const placements: AiLayoutListingPlacement[] = []\n for (const screen of screens) {\n if (typeof screen.id !== 'string' || !screen.id) continue\n screen.sections.forEach((section, index) => {\n if ((section.uses ?? []).some((ref) => named.has(ref.trim().toLowerCase()))) {\n placements.push({ screenId: screen.id as string, section: index, role: isHome(screen.slug) ? 'featured' : 'index' })\n }\n })\n }\n return placements\n}\n"],"names":["aiStorefrontSays","AI_LAYOUT_LISTINGS_INPUT","aiLayoutListingId","kind","datasetId","AI_LAYOUT_LISTING_ELEMENTS","products","posts","tracks","reviews","signup","records","events","AI_LAYOUT_CART_ELEMENT","AI_LAYOUT_FEATURED_RECORDS","PLACEMENT_ROLES","KINDS","text","value","trim","LISTING_NAMES","SITE_PATH","DATASET_ID","FIELD_ID","RECORD_BASE","aiLayoutRecordHref","listing","base","recordBase","replace","field","addressField","test","listingFieldsOf","raw","Array","isArray","flatMap","id","name","slice","type","aiLayoutListingsOf","inputs","listings","entry","record","fields","imageField","includes","some","href","action","actionLabel","actionHref","collectionSlug","placements","placement","screenId","section","role","Number","isInteger","push","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","split","toLowerCase","aiLayoutListingPlacements","screens","place","afterHero","home","uses","use","_name","aiLayoutRecordBaseOf","dataset","ref","aiLayoutRecordsPlacements","forEach","lists","aiLayoutEventsPlacements","names","Set","has"],"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 * - `records` — one of the site's datasets (AGL-3616): a menu, a team, its\n * services, built by the site start and kept by the data plugin. Drawn as\n * a Box that repeats over the dataset (`repeatDataset`, the platform's own\n * repeat), its card binding the record's fields as `{{item.<field>}}`, in\n * the sections whose plan names the dataset in its `uses`. A site may list\n * several, one listing a dataset.\n * - `events` — the site's dated events (AGL-3755), which a guided start of\n * an event-led business drafts through the events-calendar plugin, listed\n * by its Event List (`eventList`) in the section whose plan names them in\n * its `uses`: each published event with its date, place and cover, and\n * its search-engine Event markup.\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' | 'records' | 'events'\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/** One field of a dataset a `records` listing shows, as its card binds it. */\nexport interface AiLayoutListingField {\n /** The field's id, which `{{item.<id>}}` reads. */\n id: string\n name: string\n /** The data plugin's stored type: `text`, `float`, `int32`, `bool`, `sorted`. */\n type: string\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 /** The dataset a records listing repeats over, by id (AGL-3616). */\n datasetId?: string\n /** The dataset's fields, in its order, which the card binds. */\n fields?: AiLayoutListingField[]\n /**\n * The field holding each record's photo (AGL-3616), which the card leads\n * with as its Image's `src`, so a portfolio stays a gallery. Never among\n * `fields`: it is the card's picture, not its words.\n */\n imageField?: string\n /**\n * Where each record's own page lives (AGL-3727): the base path of the\n * dataset's record template (AGL-3475), without slashes at either end —\n * `services` for `/services/roofing`. Set by code where the plan makes or\n * the site keeps a record template for the dataset, never by the model.\n */\n recordBase?: string\n /**\n * The record's page-address field (AGL-3727), which the record's page is\n * served at under {@link recordBase}. Never among `fields`: it is the card's\n * link, not its words.\n */\n addressField?: 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; a records listing is one dataset's. */\nexport function aiLayoutListingId(kind: AiLayoutListingKind, datasetId?: string): string {\n return kind === 'records' && datasetId ? `listing:records:${datasetId}` : `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 // The platform's own Box, repeating its card over the dataset.\n records: 'muiBox',\n events: 'eventList',\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, records: 3, events: 3 } as const\n\nconst PLACEMENT_ROLES: readonly AiLayoutListingRole[] = ['featured', 'index']\nconst KINDS: readonly AiLayoutListingKind[] = ['products', 'posts', 'tracks', 'reviews', 'signup', 'records', 'events']\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 records: 'the list',\n events: 'the events',\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 dataset id and a field id, as the data plugin mints them. */\nconst DATASET_ID = /^[A-Za-z0-9_-]{1,128}$/\nconst FIELD_ID = /^[A-Za-z][A-Za-z0-9_]{0,63}$/\n\n/** A record template's base: one to four URL-safe segments, as the data plugin keeps it. */\nconst RECORD_BASE = /^[a-z0-9]+(?:-[a-z0-9]+)*(?:\\/[a-z0-9]+(?:-[a-z0-9]+)*){0,3}$/\n\n/**\n * The link each card of a records listing opens (AGL-3727): the record's own\n * page, `/{base}/{{item.<address field>}}`, which the page fills per record\n * as it repeats the card. `null` where the dataset has no record page, or the\n * listing names no address to reach it by.\n */\nexport function aiLayoutRecordHref(listing: Pick<AiLayoutListing, 'kind' | 'recordBase' | 'addressField'>): string | null {\n if (listing.kind !== 'records') return null\n const base = text(listing.recordBase).replace(/^\\/+|\\/+$/g, '')\n const field = text(listing.addressField)\n return RECORD_BASE.test(base) && FIELD_ID.test(field) ? `/${base}/{{item.${field}}}` : null\n}\n\n/** A records listing's fields as its input carries them; nothing that does not read is kept. */\nfunction listingFieldsOf(raw: unknown): AiLayoutListingField[] {\n if (!Array.isArray(raw)) return []\n return raw\n .flatMap((value) => {\n const field = (value ?? {}) as Record<string, unknown>\n const id = text(field['id'])\n return FIELD_ID.test(id) ? [{ id, name: text(field['name']).slice(0, 60) || id, type: text(field['type']) || 'text' }] : []\n })\n .slice(0, 16)\n}\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 // A records listing is one dataset's (AGL-3616): a site may list several.\n const datasetId = kind === 'records' ? text(record['datasetId']) : ''\n const fields = kind === 'records' ? listingFieldsOf(record['fields']) : []\n const imageField = kind === 'records' ? text(record['imageField']) : ''\n const recordBase = kind === 'records' ? text(record['recordBase']).replace(/^\\/+|\\/+$/g, '') : ''\n const addressField = kind === 'records' ? text(record['addressField']) : ''\n // Its fields may be left for the page to read off the site's inventory.\n if (kind === 'records' && !DATASET_ID.test(datasetId)) continue\n if (\n !KINDS.includes(kind) ||\n listings.some((listing) => listing.kind === kind && (kind !== 'records' || listing.datasetId === datasetId))\n ) {\n continue\n }\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, datasetId),\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' && kind !== 'records' && kind !== 'events'\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 === 'records' ? { datasetId, fields } : {}),\n ...(kind === 'records' && FIELD_ID.test(imageField) ? { imageField } : {}),\n // A record page's base, and the address field where the unit knew it; a\n // build's page reads the field off the site's inventory (AGL-3727).\n ...(kind === 'records' && RECORD_BASE.test(recordBase) ? { recordBase } : {}),\n ...(kind === 'records' && RECORD_BASE.test(recordBase) && FIELD_ID.test(addressField) ? { addressField } : {}),\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 /** The dataset this page is the record template of, and its base (AGL-3475); absent for a page that is one page. */\n record?: { dataset: string; base: string } | null\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\n/**\n * Where a dataset's records have pages of their own (AGL-3727): the base of\n * the page the plan makes its record template (AGL-3475), named by its id or\n * as `new:<its name>`; `null` where no page is. Read off the plan, never\n * asked of the model, so every card that lists the dataset links its record.\n */\nexport function aiLayoutRecordBaseOf(\n dataset: { id: string; name: string },\n screens: readonly AiLayoutListingScreen[],\n): string | null {\n const named = `new:${dataset.name}`.trim().toLowerCase()\n for (const screen of screens) {\n const ref = text(screen.record?.dataset)\n if (!ref || (ref !== dataset.id && ref.toLowerCase() !== named)) continue\n const base = text(screen.record?.base).replace(/^\\/+|\\/+$/g, '')\n if (RECORD_BASE.test(base)) return base\n }\n return null\n}\n\n/**\n * The sections that list one of the site's datasets (AGL-3616): every section\n * whose plan names it in its `uses` — `new:<its name>` as the plan wrote it,\n * or its id once built. The home page features a few of its records; any\n * other page lists them all. Chosen from the plan, never asked of the model.\n */\nexport function aiLayoutRecordsPlacements(\n dataset: { id: string; name: string },\n screens: readonly AiLayoutListingScreen[],\n): AiLayoutListingPlacement[] {\n const named = `new:${dataset.name}`.trim().toLowerCase()\n const placements: AiLayoutListingPlacement[] = []\n for (const screen of screens) {\n if (typeof screen.id !== 'string' || !screen.id) continue\n screen.sections.forEach((section, index) => {\n const lists = (section.uses ?? []).some((ref) => ref === dataset.id || ref.trim().toLowerCase() === named)\n if (lists) placements.push({ screenId: screen.id as string, section: index, role: isHome(screen.slug) ? 'featured' : 'index' })\n })\n }\n return placements\n}\n\n/**\n * The sections that list the site's events (AGL-3755): every section whose\n * plan names one of them in its `uses` (`new:<its name>`), once however many\n * it names. The home page features the next few; any other page lists them\n * all. Chosen from the plan, never asked of the model.\n */\nexport function aiLayoutEventsPlacements(\n names: readonly string[],\n screens: readonly AiLayoutListingScreen[],\n): AiLayoutListingPlacement[] {\n const named = new Set(names.map((name) => `new:${name}`.trim().toLowerCase()))\n const placements: AiLayoutListingPlacement[] = []\n for (const screen of screens) {\n if (typeof screen.id !== 'string' || !screen.id) continue\n screen.sections.forEach((section, index) => {\n if ((section.uses ?? []).some((ref) => named.has(ref.trim().toLowerCase()))) {\n placements.push({ screenId: screen.id as string, section: index, role: isHome(screen.slug) ? 'featured' : 'index' })\n }\n })\n }\n return placements\n}\n\n/**\n * A field whose name says it holds what a record's photo shows (AGL-3856):\n * the alt text of the record's Image, never words of its card.\n */\nconst RECORD_IMAGE_ALT = /^(?:image|photo|picture) (?:description|alt(?: text)?)$/i\n\n/** Whether a field's name says it holds what a record's photo shows (AGL-3856). */\nexport function aiLayoutIsRecordImageAlt(name: string): boolean {\n return RECORD_IMAGE_ALT.test(name.trim())\n}\n\n/** The field holding what a record's photo shows, by id, or `null` (AGL-3856). */\nexport function aiLayoutRecordImageAltField(fields: readonly { id: string; name: string }[]): string | null {\n return fields.find((field) => aiLayoutIsRecordImageAlt(field.name))?.id ?? null\n}\n"],"names":["aiStorefrontSays","AI_LAYOUT_LISTINGS_INPUT","aiLayoutListingId","kind","datasetId","AI_LAYOUT_LISTING_ELEMENTS","products","posts","tracks","reviews","signup","records","events","AI_LAYOUT_CART_ELEMENT","AI_LAYOUT_FEATURED_RECORDS","PLACEMENT_ROLES","KINDS","text","value","trim","LISTING_NAMES","SITE_PATH","DATASET_ID","FIELD_ID","RECORD_BASE","aiLayoutRecordHref","listing","base","recordBase","replace","field","addressField","test","listingFieldsOf","raw","Array","isArray","flatMap","id","name","slice","type","aiLayoutListingsOf","inputs","listings","entry","record","fields","imageField","includes","some","href","action","actionLabel","actionHref","collectionSlug","placements","placement","screenId","section","role","Number","isInteger","push","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","split","toLowerCase","aiLayoutListingPlacements","screens","place","afterHero","home","uses","use","_name","aiLayoutRecordBaseOf","dataset","ref","aiLayoutRecordsPlacements","forEach","lists","aiLayoutEventsPlacements","names","Set","has","RECORD_IMAGE_ALT","aiLayoutIsRecordImageAlt","aiLayoutRecordImageAltField"],"mappings":"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"}
|
|
@@ -38,12 +38,17 @@ import type { NodesMap } from '@aglyn/aglyn/types/nodes';
|
|
|
38
38
|
* starter reads better than no picture when there is no library.
|
|
39
39
|
*
|
|
40
40
|
* Where the library answered and nothing in it fits a slot
|
|
41
|
-
* ({@link AI_LAYOUT_NO_PHOTO}), the slot
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* (
|
|
45
|
-
*
|
|
46
|
-
*
|
|
41
|
+
* ({@link AI_LAYOUT_NO_PHOTO}), the slot never takes a starter: the beta.257
|
|
42
|
+
* Copper Kettle Catering start (2026-10-15) opened a barbecue caterer's home
|
|
43
|
+
* page on the starters' pottery workbench (AGL-3831). Nor does it ship empty
|
|
44
|
+
* (AGL-3856): its picture is taken out, frame and all
|
|
45
|
+
* ({@link aiLayoutDropEmptyPictures}). A cover's frame is a black scrim laid
|
|
46
|
+
* over the whole section, and with nothing in it the beta.261 Synchronicity
|
|
47
|
+
* Gym hero read as a solid black block; a split's picture card is a rounded
|
|
48
|
+
* gray box, and the beta.262 Adult Hybrid Learning Center hero read as
|
|
49
|
+
* broken. Without them the section shows its own band, a cover's section
|
|
50
|
+
* still pinned to the site's dark scheme so the words written for a dark
|
|
51
|
+
* photo stay readable, and a split's words take the row's width.
|
|
47
52
|
*
|
|
48
53
|
* A slot keeps the alt text the model wrote only when it describes the photo
|
|
49
54
|
* placed: a stock photo that is not what it says is described by what it is
|
|
@@ -178,6 +183,20 @@ export declare function aiLayoutStarterPhotos(slots: readonly AiLayoutPictureSlo
|
|
|
178
183
|
* words. A slot given no photo is left as the compiler wrote it.
|
|
179
184
|
*/
|
|
180
185
|
export declare function aiLayoutPlacePictures(nodes: NodesMap, slots: readonly AiLayoutPictureSlot[], photos: ReadonlyArray<AiLayoutPicturePhoto | null>): NodesMap;
|
|
186
|
+
/**
|
|
187
|
+
* The page without the pictures left with no photo (AGL-3856). A slot whose
|
|
188
|
+
* photo is missing loses its image, its icon and its frame where the frame
|
|
189
|
+
* holds nothing else, and so does every wrapper that holds nothing once it
|
|
190
|
+
* is gone, up to its section. A cover's black scrim over nothing read as a
|
|
191
|
+
* solid black block (the beta.261 Synchronicity Gym hero), and an empty
|
|
192
|
+
* rounded gray card as a broken picture (the beta.262 Adult Hybrid Learning
|
|
193
|
+
* Center hero). A column of a split row taken out leaves the words' column
|
|
194
|
+
* the row's full width. A cover's section stays pinned to the site's dark
|
|
195
|
+
* scheme, which paints that scheme's background and text colors, so the
|
|
196
|
+
* words written for a dark photo stay readable. A slot a photo fills, a
|
|
197
|
+
* starter's included, is kept.
|
|
198
|
+
*/
|
|
199
|
+
export declare function aiLayoutDropEmptyPictures(nodes: NodesMap, slots: readonly AiLayoutPictureSlot[], photos: ReadonlyArray<AiLayoutPicturePhoto | null>, sectionIds?: readonly string[]): NodesMap;
|
|
181
200
|
/**
|
|
182
201
|
* A source of photos found for the slots themselves — a stock search copied
|
|
183
202
|
* into the site's library — answering per slot, in order: a photo; or
|
|
@@ -190,8 +209,8 @@ export type AiLayoutPictureSource = (slots: readonly AiLayoutPictureSlot[]) => P
|
|
|
190
209
|
/**
|
|
191
210
|
* Fills a stored page's empty picture slots (AGL-3660): from `source` where
|
|
192
211
|
* one is given and it answers; a slot it answers with nothing fitting stays
|
|
193
|
-
* without a photo
|
|
194
|
-
* own words (AGL-3831). Never throws, and never leaves a page worse than it
|
|
212
|
+
* without a photo and is taken out ({@link aiLayoutDropEmptyPictures}); every
|
|
213
|
+
* other slot takes a starter photo, described by its own words (AGL-3831). Never throws, and never leaves a page worse than it
|
|
195
214
|
* came: a picture is decoration, and a job never fails over one.
|
|
196
215
|
*/
|
|
197
216
|
export declare function aiResolveLayoutPictures(nodes: NodesMap, input: {
|
|
@@ -271,11 +271,91 @@ function rotate(list, by) {
|
|
|
271
271
|
});
|
|
272
272
|
return next;
|
|
273
273
|
}
|
|
274
|
+
/** Whether a node is a grid row: a `muiGrid` container. */ const isGridRow = (node)=>{
|
|
275
|
+
var _node_props;
|
|
276
|
+
return (node == null ? void 0 : node['componentId']) === 'muiGrid' && ((_node_props = node['props']) == null ? void 0 : _node_props['container']) === true;
|
|
277
|
+
};
|
|
278
|
+
/** Whether a node is a column of a grid row: a sized `muiGrid` item. */ const isGridColumn = (node)=>(node == null ? void 0 : node['componentId']) === 'muiGrid' && !isGridRow(node);
|
|
279
|
+
/**
|
|
280
|
+
* The page without the pictures left with no photo (AGL-3856). A slot whose
|
|
281
|
+
* photo is missing loses its image, its icon and its frame where the frame
|
|
282
|
+
* holds nothing else, and so does every wrapper that holds nothing once it
|
|
283
|
+
* is gone, up to its section. A cover's black scrim over nothing read as a
|
|
284
|
+
* solid black block (the beta.261 Synchronicity Gym hero), and an empty
|
|
285
|
+
* rounded gray card as a broken picture (the beta.262 Adult Hybrid Learning
|
|
286
|
+
* Center hero). A column of a split row taken out leaves the words' column
|
|
287
|
+
* the row's full width. A cover's section stays pinned to the site's dark
|
|
288
|
+
* scheme, which paints that scheme's background and text colors, so the
|
|
289
|
+
* words written for a dark photo stay readable. A slot a photo fills, a
|
|
290
|
+
* starter's included, is kept.
|
|
291
|
+
*/ export function aiLayoutDropEmptyPictures(nodes, slots, photos, sectionIds = []) {
|
|
292
|
+
const map = nodes;
|
|
293
|
+
const empty = slots.filter((slot, index)=>{
|
|
294
|
+
var _photos_index, _map_slot_imageId_props, _map_slot_imageId;
|
|
295
|
+
if ((_photos_index = photos[index]) == null ? void 0 : _photos_index.src) return false;
|
|
296
|
+
const src = (_map_slot_imageId = map[slot.imageId]) == null ? void 0 : (_map_slot_imageId_props = _map_slot_imageId['props']) == null ? void 0 : _map_slot_imageId_props['src'];
|
|
297
|
+
return Boolean(map[slot.imageId]) && !(typeof src === 'string' && src.trim());
|
|
298
|
+
});
|
|
299
|
+
if (!empty.length) return nodes;
|
|
300
|
+
const next = _extends({}, map);
|
|
301
|
+
const children = (id)=>{
|
|
302
|
+
var _ref;
|
|
303
|
+
var _next_id;
|
|
304
|
+
return (_ref = (_next_id = next[id]) == null ? void 0 : _next_id['nodes']) != null ? _ref : [];
|
|
305
|
+
};
|
|
306
|
+
const parentOf = (id)=>{
|
|
307
|
+
var _Object_keys_find;
|
|
308
|
+
return (_Object_keys_find = Object.keys(next).find((key)=>children(key).includes(id))) != null ? _Object_keys_find : null;
|
|
309
|
+
};
|
|
310
|
+
const remove = (id)=>{
|
|
311
|
+
const parent = parentOf(id);
|
|
312
|
+
if (parent) next[parent] = _extends({}, next[parent], {
|
|
313
|
+
nodes: children(parent).filter((child)=>child !== id)
|
|
314
|
+
});
|
|
315
|
+
delete next[id];
|
|
316
|
+
return parent;
|
|
317
|
+
};
|
|
318
|
+
const sections = new Set(sectionIds);
|
|
319
|
+
for (const slot of empty){
|
|
320
|
+
const own = new Set([
|
|
321
|
+
slot.imageId,
|
|
322
|
+
slot.iconId
|
|
323
|
+
].filter(Boolean));
|
|
324
|
+
const frameHoldsOnly = Boolean(slot.frameId && next[slot.frameId] && children(slot.frameId).every((child)=>own.has(child)));
|
|
325
|
+
let parent = remove(slot.imageId);
|
|
326
|
+
if (slot.iconId && next[slot.iconId]) remove(slot.iconId);
|
|
327
|
+
if (!frameHoldsOnly) continue;
|
|
328
|
+
// The frame, then each wrapper it leaves empty: never a section, nor the page's root.
|
|
329
|
+
let at = slot.frameId;
|
|
330
|
+
while(at && next[at] && !children(at).length && !sections.has(at) && parentOf(at) !== null){
|
|
331
|
+
const column = isGridColumn(next[at]);
|
|
332
|
+
parent = remove(at);
|
|
333
|
+
at = column ? null : parent;
|
|
334
|
+
}
|
|
335
|
+
// A split row left with one column gives its words the row's width.
|
|
336
|
+
if (parent && isGridRow(next[parent])) {
|
|
337
|
+
const [only, ...rest] = children(parent);
|
|
338
|
+
if (only && !rest.length && isGridColumn(next[only])) {
|
|
339
|
+
var _column_sx, _column_props;
|
|
340
|
+
const column = next[only];
|
|
341
|
+
const sx = _extends({}, (_column_sx = column['sx']) != null ? _column_sx : {});
|
|
342
|
+
delete sx['order'];
|
|
343
|
+
next[only] = _extends({}, column, {
|
|
344
|
+
props: _extends({}, (_column_props = column['props']) != null ? _column_props : {}, {
|
|
345
|
+
size: 'xs:12'
|
|
346
|
+
}),
|
|
347
|
+
sx: Object.keys(sx).length ? sx : null
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return next;
|
|
353
|
+
}
|
|
274
354
|
/**
|
|
275
355
|
* Fills a stored page's empty picture slots (AGL-3660): from `source` where
|
|
276
356
|
* one is given and it answers; a slot it answers with nothing fitting stays
|
|
277
|
-
* without a photo
|
|
278
|
-
* own words (AGL-3831). Never throws, and never leaves a page worse than it
|
|
357
|
+
* without a photo and is taken out ({@link aiLayoutDropEmptyPictures}); every
|
|
358
|
+
* other slot takes a starter photo, described by its own words (AGL-3831). Never throws, and never leaves a page worse than it
|
|
279
359
|
* came: a picture is decoration, and a job never fails over one.
|
|
280
360
|
*/ export async function aiResolveLayoutPictures(nodes, input) {
|
|
281
361
|
try {
|
|
@@ -301,12 +381,13 @@ function rotate(list, by) {
|
|
|
301
381
|
const starters = aiLayoutStarterPhotos(slots, input.seed).map((photo)=>otherLanguage ? _extends({}, photo, {
|
|
302
382
|
alt: aiLayoutLabel('illustrativePhoto', input.language)
|
|
303
383
|
}) : photo);
|
|
304
|
-
|
|
384
|
+
const photos = slots.map((_slot, index)=>{
|
|
305
385
|
var _starters_index;
|
|
306
386
|
const answer = found[index];
|
|
307
387
|
if (answer) return answer.src ? answer : null;
|
|
308
388
|
return (_starters_index = starters[index]) != null ? _starters_index : null;
|
|
309
|
-
})
|
|
389
|
+
});
|
|
390
|
+
return aiLayoutDropEmptyPictures(aiLayoutPlacePictures(nodes, slots, photos), slots, photos, input.sectionIds);
|
|
310
391
|
} catch (error) {
|
|
311
392
|
console.error('ai layout pictures: the page keeps its empty picture slots', {
|
|
312
393
|
error
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/layout-language/ai-layout-pictures.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { DEFAULT_SITE_IMAGES } from '@aglyn/aglyn/app-utils/default-site'\nimport type { NodesMap } from '@aglyn/aglyn/types/nodes'\nimport { aiLayoutLabel, aiLayoutSpeaksOtherThanEnglish } from './ai-layout-labels'\n\n/**\n * THE PICTURES OF A LANGUAGE PAGE (AGL-3660).\n *\n * The compiler writes a picture slot as a frame holding an icon and an\n * `image` with alt text and no source (`ai-layout-compiler.ts`), and stays\n * pure: it never looks a photo up. This module is what runs after it, on the\n * stored page — it finds each empty slot, decides what kind of picture the\n * slot is, and fills it with a photo the site can serve from its own origin.\n *\n * Two sources fill a slot, in order:\n *\n * 1. A stock photo searched for by the slot's own description and copied into\n * the site's media library (`jobs/ai-layout-stock-photos.ts`), when the\n * deployment has a stock-photo key.\n * 2. Where the library cannot be asked at all — no stock-photo key, the\n * library down, refusing or out of time before it answered — one of the\n * STARTER PHOTOS every new site is born with (`DEFAULT_SITE_IMAGES`,\n * served at `/_static/starter/` by the tenant and the console alike,\n * credited in the CREDITS file beside them). They are the platform's own\n * files, so a visitor's browser asks no other host for them, and a\n * starter reads better than no picture when there is no library.\n *\n * Where the library answered and nothing in it fits a slot\n * ({@link AI_LAYOUT_NO_PHOTO}), the slot keeps the compiler's no-photo\n * treatment — its frame with its icon, or its section's own band behind a\n * cover — and never a starter: the beta.257 Copper Kettle Catering start\n * (2026-10-15) opened a barbecue caterer's home page on the starters'\n * pottery workbench (AGL-3831). An empty picture draws nothing on the\n * published page, and the Besigner shows the owner where to choose one.\n *\n * A slot keeps the alt text the model wrote only when it describes the photo\n * placed: a stock photo that is not what it says is described by what it is\n * (`aiStockPhotoAlt`), and a starter photo always by its own words, since\n * the model never wrote for it (AGL-3831). A stand-in the compiler drew where\n * the model described no picture is decorative while empty; the photo placed\n * there is described by its own words and is no longer decorative (AGL-3856).\n */\n\n/** What a picture slot is for, read from where it sits and what it says. */\nexport type AiLayoutPictureRole = 'hero' | 'about' | 'gallery'\n\n/** One empty picture slot of a stored page, in document order. */\nexport interface AiLayoutPictureSlot {\n /** The `image` node. */\n imageId: string\n /** The frame around it, which sets the slot's shape. */\n frameId: string | null\n /** The placeholder icon sharing the frame, taken out once a photo fills it. */\n iconId: string | null\n alt: string\n /** The frame's shape as width over height; 1 when it names none. */\n aspect: number\n /** The plan index of the section it sits in, or -1. */\n sectionIndex: number\n role: AiLayoutPictureRole\n /**\n * A product's photo (AGL-3676): what it is of, most specific first — its\n * name, then what its photo should show — searched for with the shop's\n * category and held to it, so a wick trimmer is never a laptop on a desk.\n */\n product?: { subjects: readonly string[] }\n /**\n * A stand-in the compiler drew where the model described no picture\n * (AGL-3856): its words are what a search looks for, never what a photo\n * shows, so a photo placed there is described by its own, and is no longer\n * decorative once it is.\n */\n standIn?: boolean\n}\n\n/** A photo a slot is filled with. */\nexport interface AiLayoutPicturePhoto {\n /** The site-served address: a starter path or the media library's own. */\n src: string\n width: number\n height: number\n /**\n * What the photo shows, where the slot's own alt text does not say it: the\n * slot takes it in place of the words the model wrote before the photo was\n * found. Absent, the slot's words describe it.\n */\n alt?: string\n}\n\n/** Words that say a picture shows people, or a section is about them. */\nconst PEOPLE =\n /\\b(about|team|story|owner|owners|founder|founders|staff|crew|people|person|portrait|meet|family|families|who we are|our (?:team|crew|people|story)|therapist|counsel(?:l)?or|coach|doctor|dentist|stylist|trainer|smil(?:e|es|ing)|headshot)\\b/i\n\n/**\n * An `aspectRatio` (`'16 / 9'`) as a number, or `null` where none is named.\n * A responsive one (`{ xs: '4 / 3', md: '4 / 5' }`, as a designed page's\n * pictures carry, AGL-3660) reads at the desktop breakpoint first: one photo\n * serves every width, and the wide screen is where its shape shows most.\n */\nfunction aspectOf(value: unknown): number | null {\n if (value && typeof value === 'object') {\n const byBreakpoint = value as Record<string, unknown>\n for (const breakpoint of ['md', 'lg', 'xl', 'sm', 'xs']) {\n const ratio = aspectOf(byBreakpoint[breakpoint])\n if (ratio !== null) return ratio\n }\n return null\n }\n const match = /^\\s*(\\d+(?:\\.\\d+)?)\\s*\\/\\s*(\\d+(?:\\.\\d+)?)\\s*$/.exec(String(value ?? ''))\n if (!match) return null\n const ratio = Number(match[1]) / Number(match[2])\n return Number.isFinite(ratio) && ratio > 0 ? ratio : null\n}\n\n/** The shape a full-bleed cover's photo is searched for: it fills a wide band. */\nconst COVER_ASPECT = 16 / 9\n\n/**\n * A slot's shape (AGL-3660): the image's own `aspectRatio` (a designed\n * picture card sets it on the image), else its frame's, else — for a frame\n * laid absolutely over its whole section, a designed photo cover — a wide\n * band's. 1 where nothing says.\n */\nfunction slotAspect(image: { sx?: Record<string, unknown> } | undefined, frame: { sx?: Record<string, unknown> } | undefined): number {\n const own = aspectOf(image?.sx?.['aspectRatio']) ?? aspectOf(frame?.sx?.['aspectRatio'])\n if (own !== null) return own\n const sx = frame?.sx\n if (sx?.['position'] === 'absolute' && sx['width'] === '100%' && sx['height'] === '100%') return COVER_ASPECT\n return 1\n}\n\n/** A slot's role: the first section's picture is the hero; people make an about picture. */\nexport function aiLayoutPictureRole(input: {\n sectionIndex: number\n sectionName: string\n alt: string\n}): AiLayoutPictureRole {\n if (input.sectionIndex === 0) return 'hero'\n if (PEOPLE.test(input.sectionName) || PEOPLE.test(input.alt)) return 'about'\n return 'gallery'\n}\n\n/**\n * Every empty picture slot of a stored page, walked from its root in\n * document order. A slot that already has a source — one the owner placed, or\n * an earlier pass filled — is not a slot.\n */\nexport function aiLayoutPictureSlots(\n nodes: NodesMap,\n rootId: string,\n sections: { ids: readonly string[]; names: readonly string[] },\n): AiLayoutPictureSlot[] {\n const map = nodes as unknown as Record<\n string,\n { componentId?: string; props?: Record<string, unknown>; sx?: Record<string, unknown>; nodes?: string[] }\n >\n const slots: AiLayoutPictureSlot[] = []\n const seen = new Set<string>()\n const visit = (id: string, parentId: string | null, sectionIndex: number) => {\n if (seen.has(id)) return\n seen.add(id)\n const node = map[id]\n if (!node) return\n const index = sections.ids.indexOf(id)\n const here = index >= 0 ? index : sectionIndex\n if (node.componentId === 'image') {\n const src = node.props?.['src']\n const alt = typeof node.props?.['alt'] === 'string' ? (node.props['alt'] as string).trim() : ''\n if ((typeof src !== 'string' || !src.trim()) && alt) {\n const frame = parentId ? map[parentId] : undefined\n const iconId =\n frame?.nodes?.find((child) => child !== id && map[child]?.componentId === 'icon') ?? null\n slots.push({\n imageId: id,\n frameId: frame ? parentId : null,\n iconId,\n alt,\n aspect: slotAspect(node, frame),\n sectionIndex: here,\n ...(node.props?.['decorative'] === true ? { standIn: true } : {}),\n role: aiLayoutPictureRole({\n sectionIndex: here,\n sectionName: here >= 0 ? (sections.names[here] ?? '') : '',\n alt,\n }),\n })\n }\n }\n for (const child of node.nodes ?? []) visit(child, id, here)\n }\n visit(rootId, null, -1)\n return slots\n}\n\n/** The starter photos, by name, each with the words that describe it. */\nexport const AI_LAYOUT_STARTER_PHOTOS = DEFAULT_SITE_IMAGES\n\n/**\n * A source's answer for a slot the library answered and nothing in it fits\n * (AGL-3831): the slot keeps its no-photo treatment rather than take a\n * starter. Its source is empty, so a caller reading `src` to fill something\n * that must hold a photo (a product's, a record's) falls back as it would on\n * `null`.\n */\nexport const AI_LAYOUT_NO_PHOTO: Readonly<AiLayoutPicturePhoto> = Object.freeze({ src: '', width: 0, height: 0 })\n\ntype StarterName = keyof typeof AI_LAYOUT_STARTER_PHOTOS\n\n/**\n * The starter photos each role draws from, best first. A hero draws from the\n * wide photos that read as a page's opening, an about picture leads with the\n * owner, and the rest are the gallery's.\n */\nconst STARTER_POOLS: Readonly<Record<AiLayoutPictureRole, readonly StarterName[]>> = {\n hero: ['hero', 'desk', 'craft'],\n about: ['about', 'hero'],\n gallery: ['desk', 'plants', 'craft'],\n}\n\nconst ALL_STARTERS = Object.keys(AI_LAYOUT_STARTER_PHOTOS) as StarterName[]\n\n/** A small stable number from a seed, so the same job always picks the same photos. */\nexport function aiLayoutSeedNumber(seed: string): number {\n let hash = 2166136261\n for (let index = 0; index < seed.length; index += 1) {\n hash ^= seed.charCodeAt(index)\n hash = Math.imul(hash, 16777619)\n }\n return hash >>> 0\n}\n\nfunction rotate<T>(list: readonly T[], by: number): T[] {\n if (!list.length) return []\n const start = by % list.length\n return [...list.slice(start), ...list.slice(0, start)]\n}\n\n/**\n * A starter photo for each slot (AGL-3660). No photo repeats on a page while\n * an unused one remains, and every pool turns by the seed, so sites do not\n * all open with the same photo while one job always picks the same.\n *\n * The slots are served by what they are for, not where they sit: the hero\n * first, then the pictures of people (an about picture leads with the owner),\n * then the rest. The live Juniper Clay start (2026-10-08) had six slots and\n * five starters, and the owner's photo went to a stoneware card before the\n * About section that described the artist asked for it. Only a page with more\n * slots than starters repeats one: the photo placed longest ago, never the\n * one in a slot beside it. The owner's portrait goes only to a picture of\n * people; a gallery picture never takes it, even to avoid a repeat.\n */\nexport function aiLayoutStarterPhotos(\n slots: readonly AiLayoutPictureSlot[],\n seed: string,\n): AiLayoutPicturePhoto[] {\n const turn = aiLayoutSeedNumber(seed)\n const order: AiLayoutPictureRole[] = ['hero', 'about', 'gallery']\n const served = slots\n .map((slot, index) => ({ slot, index }))\n .sort((a, b) => order.indexOf(a.slot.role) - order.indexOf(b.slot.role) || a.index - b.index)\n const names: Array<StarterName | undefined> = slots.map(() => undefined)\n /** When each starter was last placed, by its turn in the serving order. */\n const placedAt = new Map<StarterName, number>()\n served.forEach(({ slot, index }, step) => {\n // An about picture leads with the owner on every site; the others turn.\n const pool = slot.role === 'about' ? [...STARTER_POOLS.about] : rotate(STARTER_POOLS[slot.role], turn)\n const rest = rotate(\n ALL_STARTERS.filter((name) => !pool.includes(name)),\n turn,\n )\n // The owner's portrait is a picture of a person, and only a picture of\n // people takes it: never a vase in a gallery (AGL-3660).\n const candidates = [...pool, ...rest].filter((name) => slot.role === 'about' || name !== 'about')\n const unused = candidates.find((candidate) => !placedAt.has(candidate))\n const beside = new Set([names[index - 1], names[index + 1]].filter(Boolean))\n const name =\n unused ??\n [...candidates]\n .sort((a, b) => (placedAt.get(a) ?? -1) - (placedAt.get(b) ?? -1))\n .find((candidate) => !beside.has(candidate)) ??\n candidates[0]\n names[index] = name\n placedAt.set(name, step)\n })\n return names.map((name) => {\n const photo = AI_LAYOUT_STARTER_PHOTOS[name as StarterName]\n return { src: photo.src, width: photo.width, height: photo.height, alt: photo.alt }\n })\n}\n\n/**\n * The page with each slot filled by its photo: the `image` given its source,\n * its intrinsic size and how it loads (the hero eagerly, as the starter home\n * page loads its own), and the placeholder icon taken out of the frame. The\n * alt text is kept as the model wrote it unless the photo brings its own\n * words. A slot given no photo is left as the compiler wrote it.\n */\nexport function aiLayoutPlacePictures(\n nodes: NodesMap,\n slots: readonly AiLayoutPictureSlot[],\n photos: ReadonlyArray<AiLayoutPicturePhoto | null>,\n): NodesMap {\n const next = { ...(nodes as unknown as Record<string, Record<string, unknown>>) }\n // Only the page's first picture loads eagerly (rule 16, AGL-3660): a hero\n // of several photos loaded them all eagerly, and a live Portfolio page was\n // refused for it (2026-10-10). The slots are in document order; every later\n // picture leaves `loading` unset, which the renderer loads when reached.\n const lead = slots.findIndex((_, index) => Boolean(photos[index] && next[slots[index].imageId]))\n slots.forEach((slot, index) => {\n const photo = photos[index]\n const node = next[slot.imageId]\n if (!photo || !node) return\n const props = { ...((node['props'] as Record<string, unknown> | undefined) ?? {}) }\n delete props['loading']\n // A photo placed is described by its words, never hidden from a screen\n // reader: a stand-in was decorative only while it showed nothing (AGL-3856).\n if (photo.alt?.trim()) delete props['decorative']\n next[slot.imageId] = {\n ...node,\n props: {\n ...props,\n src: photo.src,\n intrinsicWidth: photo.width,\n intrinsicHeight: photo.height,\n ...(photo.alt?.trim() ? { alt: photo.alt.trim() } : {}),\n ...(index === lead && slot.role === 'hero' ? { loading: 'eager' } : {}),\n },\n }\n if (slot.iconId && slot.frameId && next[slot.frameId]) {\n const frame = next[slot.frameId]\n next[slot.frameId] = {\n ...frame,\n nodes: ((frame['nodes'] as string[] | undefined) ?? []).filter((child) => child !== slot.iconId),\n }\n delete next[slot.iconId]\n }\n })\n return next as unknown as NodesMap\n}\n\n/**\n * A source of photos found for the slots themselves — a stock search copied\n * into the site's library — answering per slot, in order: a photo; or\n * {@link AI_LAYOUT_NO_PHOTO} where the library answered and nothing fits,\n * which leaves the slot without a photo; or `null` where the library could\n * not be asked, which takes a starter photo. It may throw, and then every\n * slot takes a starter.\n */\nexport type AiLayoutPictureSource = (\n slots: readonly AiLayoutPictureSlot[],\n) => Promise<ReadonlyArray<AiLayoutPicturePhoto | null>>\n\n/**\n * Fills a stored page's empty picture slots (AGL-3660): from `source` where\n * one is given and it answers; a slot it answers with nothing fitting stays\n * without a photo; every other slot takes a starter photo, described by its\n * own words (AGL-3831). Never throws, and never leaves a page worse than it\n * came: a picture is decoration, and a job never fails over one.\n */\nexport async function aiResolveLayoutPictures(\n nodes: NodesMap,\n input: {\n rootId: string\n sectionIds: readonly string[]\n sectionNames: readonly string[]\n /** A per-job word, so two sites do not open with the same photo. */\n seed: string\n source?: AiLayoutPictureSource | null\n /** The site's language (AGL-3750); a starter photo's English words are not its own. */\n language?: string\n },\n): Promise<NodesMap> {\n try {\n const slots = aiLayoutPictureSlots(nodes, input.rootId, {\n ids: input.sectionIds,\n names: input.sectionNames,\n })\n if (!slots.length) return nodes\n let found: ReadonlyArray<AiLayoutPicturePhoto | null> = []\n if (input.source) {\n try {\n found = await input.source(slots)\n } catch (error) {\n console.warn('ai layout pictures: the stock source failed; starter photos fill the page', { error })\n found = []\n }\n }\n // The starters' words are English; a site in another language says it\n // shows an illustrative photo, true of any photo, as a stock photo's does.\n const otherLanguage = aiLayoutSpeaksOtherThanEnglish(input.language)\n const starters = aiLayoutStarterPhotos(slots, input.seed).map((photo) =>\n otherLanguage ? { ...photo, alt: aiLayoutLabel('illustrativePhoto', input.language) } : photo,\n )\n return aiLayoutPlacePictures(\n nodes,\n slots,\n slots.map((_slot, index) => {\n const answer = found[index]\n if (answer) return answer.src ? answer : null\n return starters[index] ?? null\n }),\n )\n } catch (error) {\n console.error('ai layout pictures: the page keeps its empty picture slots', { error })\n return nodes\n }\n}\n"],"names":["DEFAULT_SITE_IMAGES","aiLayoutLabel","aiLayoutSpeaksOtherThanEnglish","PEOPLE","aspectOf","value","byBreakpoint","breakpoint","ratio","match","exec","String","Number","isFinite","COVER_ASPECT","slotAspect","image","frame","own","sx","aiLayoutPictureRole","input","sectionIndex","test","sectionName","alt","aiLayoutPictureSlots","nodes","rootId","sections","map","slots","seen","Set","visit","id","parentId","node","has","add","index","ids","indexOf","here","componentId","src","props","trim","undefined","iconId","find","child","push","imageId","frameId","aspect","standIn","role","names","AI_LAYOUT_STARTER_PHOTOS","AI_LAYOUT_NO_PHOTO","Object","freeze","width","height","STARTER_POOLS","hero","about","gallery","ALL_STARTERS","keys","aiLayoutSeedNumber","seed","hash","length","charCodeAt","Math","imul","rotate","list","by","start","slice","aiLayoutStarterPhotos","turn","order","served","slot","sort","a","b","placedAt","Map","forEach","step","unused","pool","rest","filter","name","includes","candidates","candidate","beside","Boolean","get","set","photo","aiLayoutPlacePictures","photos","next","lead","findIndex","_","intrinsicWidth","intrinsicHeight","loading","aiResolveLayoutPictures","sectionIds","sectionNames","found","source","error","console","warn","otherLanguage","language","starters","_slot","answer"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/layout-language/ai-layout-pictures.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { DEFAULT_SITE_IMAGES } from '@aglyn/aglyn/app-utils/default-site'\nimport type { NodesMap } from '@aglyn/aglyn/types/nodes'\nimport { aiLayoutLabel, aiLayoutSpeaksOtherThanEnglish } from './ai-layout-labels'\n\n/**\n * THE PICTURES OF A LANGUAGE PAGE (AGL-3660).\n *\n * The compiler writes a picture slot as a frame holding an icon and an\n * `image` with alt text and no source (`ai-layout-compiler.ts`), and stays\n * pure: it never looks a photo up. This module is what runs after it, on the\n * stored page — it finds each empty slot, decides what kind of picture the\n * slot is, and fills it with a photo the site can serve from its own origin.\n *\n * Two sources fill a slot, in order:\n *\n * 1. A stock photo searched for by the slot's own description and copied into\n * the site's media library (`jobs/ai-layout-stock-photos.ts`), when the\n * deployment has a stock-photo key.\n * 2. Where the library cannot be asked at all — no stock-photo key, the\n * library down, refusing or out of time before it answered — one of the\n * STARTER PHOTOS every new site is born with (`DEFAULT_SITE_IMAGES`,\n * served at `/_static/starter/` by the tenant and the console alike,\n * credited in the CREDITS file beside them). They are the platform's own\n * files, so a visitor's browser asks no other host for them, and a\n * starter reads better than no picture when there is no library.\n *\n * Where the library answered and nothing in it fits a slot\n * ({@link AI_LAYOUT_NO_PHOTO}), the slot never takes a starter: the beta.257\n * Copper Kettle Catering start (2026-10-15) opened a barbecue caterer's home\n * page on the starters' pottery workbench (AGL-3831). Nor does it ship empty\n * (AGL-3856): its picture is taken out, frame and all\n * ({@link aiLayoutDropEmptyPictures}). A cover's frame is a black scrim laid\n * over the whole section, and with nothing in it the beta.261 Synchronicity\n * Gym hero read as a solid black block; a split's picture card is a rounded\n * gray box, and the beta.262 Adult Hybrid Learning Center hero read as\n * broken. Without them the section shows its own band, a cover's section\n * still pinned to the site's dark scheme so the words written for a dark\n * photo stay readable, and a split's words take the row's width.\n *\n * A slot keeps the alt text the model wrote only when it describes the photo\n * placed: a stock photo that is not what it says is described by what it is\n * (`aiStockPhotoAlt`), and a starter photo always by its own words, since\n * the model never wrote for it (AGL-3831). A stand-in the compiler drew where\n * the model described no picture is decorative while empty; the photo placed\n * there is described by its own words and is no longer decorative (AGL-3856).\n */\n\n/** What a picture slot is for, read from where it sits and what it says. */\nexport type AiLayoutPictureRole = 'hero' | 'about' | 'gallery'\n\n/** One empty picture slot of a stored page, in document order. */\nexport interface AiLayoutPictureSlot {\n /** The `image` node. */\n imageId: string\n /** The frame around it, which sets the slot's shape. */\n frameId: string | null\n /** The placeholder icon sharing the frame, taken out once a photo fills it. */\n iconId: string | null\n alt: string\n /** The frame's shape as width over height; 1 when it names none. */\n aspect: number\n /** The plan index of the section it sits in, or -1. */\n sectionIndex: number\n role: AiLayoutPictureRole\n /**\n * A product's photo (AGL-3676): what it is of, most specific first — its\n * name, then what its photo should show — searched for with the shop's\n * category and held to it, so a wick trimmer is never a laptop on a desk.\n */\n product?: { subjects: readonly string[] }\n /**\n * A stand-in the compiler drew where the model described no picture\n * (AGL-3856): its words are what a search looks for, never what a photo\n * shows, so a photo placed there is described by its own, and is no longer\n * decorative once it is.\n */\n standIn?: boolean\n}\n\n/** A photo a slot is filled with. */\nexport interface AiLayoutPicturePhoto {\n /** The site-served address: a starter path or the media library's own. */\n src: string\n width: number\n height: number\n /**\n * What the photo shows, where the slot's own alt text does not say it: the\n * slot takes it in place of the words the model wrote before the photo was\n * found. Absent, the slot's words describe it.\n */\n alt?: string\n}\n\n/** Words that say a picture shows people, or a section is about them. */\nconst PEOPLE =\n /\\b(about|team|story|owner|owners|founder|founders|staff|crew|people|person|portrait|meet|family|families|who we are|our (?:team|crew|people|story)|therapist|counsel(?:l)?or|coach|doctor|dentist|stylist|trainer|smil(?:e|es|ing)|headshot)\\b/i\n\n/**\n * An `aspectRatio` (`'16 / 9'`) as a number, or `null` where none is named.\n * A responsive one (`{ xs: '4 / 3', md: '4 / 5' }`, as a designed page's\n * pictures carry, AGL-3660) reads at the desktop breakpoint first: one photo\n * serves every width, and the wide screen is where its shape shows most.\n */\nfunction aspectOf(value: unknown): number | null {\n if (value && typeof value === 'object') {\n const byBreakpoint = value as Record<string, unknown>\n for (const breakpoint of ['md', 'lg', 'xl', 'sm', 'xs']) {\n const ratio = aspectOf(byBreakpoint[breakpoint])\n if (ratio !== null) return ratio\n }\n return null\n }\n const match = /^\\s*(\\d+(?:\\.\\d+)?)\\s*\\/\\s*(\\d+(?:\\.\\d+)?)\\s*$/.exec(String(value ?? ''))\n if (!match) return null\n const ratio = Number(match[1]) / Number(match[2])\n return Number.isFinite(ratio) && ratio > 0 ? ratio : null\n}\n\n/** The shape a full-bleed cover's photo is searched for: it fills a wide band. */\nconst COVER_ASPECT = 16 / 9\n\n/**\n * A slot's shape (AGL-3660): the image's own `aspectRatio` (a designed\n * picture card sets it on the image), else its frame's, else — for a frame\n * laid absolutely over its whole section, a designed photo cover — a wide\n * band's. 1 where nothing says.\n */\nfunction slotAspect(image: { sx?: Record<string, unknown> } | undefined, frame: { sx?: Record<string, unknown> } | undefined): number {\n const own = aspectOf(image?.sx?.['aspectRatio']) ?? aspectOf(frame?.sx?.['aspectRatio'])\n if (own !== null) return own\n const sx = frame?.sx\n if (sx?.['position'] === 'absolute' && sx['width'] === '100%' && sx['height'] === '100%') return COVER_ASPECT\n return 1\n}\n\n/** A slot's role: the first section's picture is the hero; people make an about picture. */\nexport function aiLayoutPictureRole(input: {\n sectionIndex: number\n sectionName: string\n alt: string\n}): AiLayoutPictureRole {\n if (input.sectionIndex === 0) return 'hero'\n if (PEOPLE.test(input.sectionName) || PEOPLE.test(input.alt)) return 'about'\n return 'gallery'\n}\n\n/**\n * Every empty picture slot of a stored page, walked from its root in\n * document order. A slot that already has a source — one the owner placed, or\n * an earlier pass filled — is not a slot.\n */\nexport function aiLayoutPictureSlots(\n nodes: NodesMap,\n rootId: string,\n sections: { ids: readonly string[]; names: readonly string[] },\n): AiLayoutPictureSlot[] {\n const map = nodes as unknown as Record<\n string,\n { componentId?: string; props?: Record<string, unknown>; sx?: Record<string, unknown>; nodes?: string[] }\n >\n const slots: AiLayoutPictureSlot[] = []\n const seen = new Set<string>()\n const visit = (id: string, parentId: string | null, sectionIndex: number) => {\n if (seen.has(id)) return\n seen.add(id)\n const node = map[id]\n if (!node) return\n const index = sections.ids.indexOf(id)\n const here = index >= 0 ? index : sectionIndex\n if (node.componentId === 'image') {\n const src = node.props?.['src']\n const alt = typeof node.props?.['alt'] === 'string' ? (node.props['alt'] as string).trim() : ''\n if ((typeof src !== 'string' || !src.trim()) && alt) {\n const frame = parentId ? map[parentId] : undefined\n const iconId =\n frame?.nodes?.find((child) => child !== id && map[child]?.componentId === 'icon') ?? null\n slots.push({\n imageId: id,\n frameId: frame ? parentId : null,\n iconId,\n alt,\n aspect: slotAspect(node, frame),\n sectionIndex: here,\n ...(node.props?.['decorative'] === true ? { standIn: true } : {}),\n role: aiLayoutPictureRole({\n sectionIndex: here,\n sectionName: here >= 0 ? (sections.names[here] ?? '') : '',\n alt,\n }),\n })\n }\n }\n for (const child of node.nodes ?? []) visit(child, id, here)\n }\n visit(rootId, null, -1)\n return slots\n}\n\n/** The starter photos, by name, each with the words that describe it. */\nexport const AI_LAYOUT_STARTER_PHOTOS = DEFAULT_SITE_IMAGES\n\n/**\n * A source's answer for a slot the library answered and nothing in it fits\n * (AGL-3831): the slot keeps its no-photo treatment rather than take a\n * starter. Its source is empty, so a caller reading `src` to fill something\n * that must hold a photo (a product's, a record's) falls back as it would on\n * `null`.\n */\nexport const AI_LAYOUT_NO_PHOTO: Readonly<AiLayoutPicturePhoto> = Object.freeze({ src: '', width: 0, height: 0 })\n\ntype StarterName = keyof typeof AI_LAYOUT_STARTER_PHOTOS\n\n/**\n * The starter photos each role draws from, best first. A hero draws from the\n * wide photos that read as a page's opening, an about picture leads with the\n * owner, and the rest are the gallery's.\n */\nconst STARTER_POOLS: Readonly<Record<AiLayoutPictureRole, readonly StarterName[]>> = {\n hero: ['hero', 'desk', 'craft'],\n about: ['about', 'hero'],\n gallery: ['desk', 'plants', 'craft'],\n}\n\nconst ALL_STARTERS = Object.keys(AI_LAYOUT_STARTER_PHOTOS) as StarterName[]\n\n/** A small stable number from a seed, so the same job always picks the same photos. */\nexport function aiLayoutSeedNumber(seed: string): number {\n let hash = 2166136261\n for (let index = 0; index < seed.length; index += 1) {\n hash ^= seed.charCodeAt(index)\n hash = Math.imul(hash, 16777619)\n }\n return hash >>> 0\n}\n\nfunction rotate<T>(list: readonly T[], by: number): T[] {\n if (!list.length) return []\n const start = by % list.length\n return [...list.slice(start), ...list.slice(0, start)]\n}\n\n/**\n * A starter photo for each slot (AGL-3660). No photo repeats on a page while\n * an unused one remains, and every pool turns by the seed, so sites do not\n * all open with the same photo while one job always picks the same.\n *\n * The slots are served by what they are for, not where they sit: the hero\n * first, then the pictures of people (an about picture leads with the owner),\n * then the rest. The live Juniper Clay start (2026-10-08) had six slots and\n * five starters, and the owner's photo went to a stoneware card before the\n * About section that described the artist asked for it. Only a page with more\n * slots than starters repeats one: the photo placed longest ago, never the\n * one in a slot beside it. The owner's portrait goes only to a picture of\n * people; a gallery picture never takes it, even to avoid a repeat.\n */\nexport function aiLayoutStarterPhotos(\n slots: readonly AiLayoutPictureSlot[],\n seed: string,\n): AiLayoutPicturePhoto[] {\n const turn = aiLayoutSeedNumber(seed)\n const order: AiLayoutPictureRole[] = ['hero', 'about', 'gallery']\n const served = slots\n .map((slot, index) => ({ slot, index }))\n .sort((a, b) => order.indexOf(a.slot.role) - order.indexOf(b.slot.role) || a.index - b.index)\n const names: Array<StarterName | undefined> = slots.map(() => undefined)\n /** When each starter was last placed, by its turn in the serving order. */\n const placedAt = new Map<StarterName, number>()\n served.forEach(({ slot, index }, step) => {\n // An about picture leads with the owner on every site; the others turn.\n const pool = slot.role === 'about' ? [...STARTER_POOLS.about] : rotate(STARTER_POOLS[slot.role], turn)\n const rest = rotate(\n ALL_STARTERS.filter((name) => !pool.includes(name)),\n turn,\n )\n // The owner's portrait is a picture of a person, and only a picture of\n // people takes it: never a vase in a gallery (AGL-3660).\n const candidates = [...pool, ...rest].filter((name) => slot.role === 'about' || name !== 'about')\n const unused = candidates.find((candidate) => !placedAt.has(candidate))\n const beside = new Set([names[index - 1], names[index + 1]].filter(Boolean))\n const name =\n unused ??\n [...candidates]\n .sort((a, b) => (placedAt.get(a) ?? -1) - (placedAt.get(b) ?? -1))\n .find((candidate) => !beside.has(candidate)) ??\n candidates[0]\n names[index] = name\n placedAt.set(name, step)\n })\n return names.map((name) => {\n const photo = AI_LAYOUT_STARTER_PHOTOS[name as StarterName]\n return { src: photo.src, width: photo.width, height: photo.height, alt: photo.alt }\n })\n}\n\n/**\n * The page with each slot filled by its photo: the `image` given its source,\n * its intrinsic size and how it loads (the hero eagerly, as the starter home\n * page loads its own), and the placeholder icon taken out of the frame. The\n * alt text is kept as the model wrote it unless the photo brings its own\n * words. A slot given no photo is left as the compiler wrote it.\n */\nexport function aiLayoutPlacePictures(\n nodes: NodesMap,\n slots: readonly AiLayoutPictureSlot[],\n photos: ReadonlyArray<AiLayoutPicturePhoto | null>,\n): NodesMap {\n const next = { ...(nodes as unknown as Record<string, Record<string, unknown>>) }\n // Only the page's first picture loads eagerly (rule 16, AGL-3660): a hero\n // of several photos loaded them all eagerly, and a live Portfolio page was\n // refused for it (2026-10-10). The slots are in document order; every later\n // picture leaves `loading` unset, which the renderer loads when reached.\n const lead = slots.findIndex((_, index) => Boolean(photos[index] && next[slots[index].imageId]))\n slots.forEach((slot, index) => {\n const photo = photos[index]\n const node = next[slot.imageId]\n if (!photo || !node) return\n const props = { ...((node['props'] as Record<string, unknown> | undefined) ?? {}) }\n delete props['loading']\n // A photo placed is described by its words, never hidden from a screen\n // reader: a stand-in was decorative only while it showed nothing (AGL-3856).\n if (photo.alt?.trim()) delete props['decorative']\n next[slot.imageId] = {\n ...node,\n props: {\n ...props,\n src: photo.src,\n intrinsicWidth: photo.width,\n intrinsicHeight: photo.height,\n ...(photo.alt?.trim() ? { alt: photo.alt.trim() } : {}),\n ...(index === lead && slot.role === 'hero' ? { loading: 'eager' } : {}),\n },\n }\n if (slot.iconId && slot.frameId && next[slot.frameId]) {\n const frame = next[slot.frameId]\n next[slot.frameId] = {\n ...frame,\n nodes: ((frame['nodes'] as string[] | undefined) ?? []).filter((child) => child !== slot.iconId),\n }\n delete next[slot.iconId]\n }\n })\n return next as unknown as NodesMap\n}\n\n/** Whether a node is a grid row: a `muiGrid` container. */\nconst isGridRow = (node: Record<string, unknown> | undefined) =>\n node?.['componentId'] === 'muiGrid' && (node['props'] as Record<string, unknown> | undefined)?.['container'] === true\n\n/** Whether a node is a column of a grid row: a sized `muiGrid` item. */\nconst isGridColumn = (node: Record<string, unknown> | undefined) =>\n node?.['componentId'] === 'muiGrid' && !isGridRow(node)\n\n/**\n * The page without the pictures left with no photo (AGL-3856). A slot whose\n * photo is missing loses its image, its icon and its frame where the frame\n * holds nothing else, and so does every wrapper that holds nothing once it\n * is gone, up to its section. A cover's black scrim over nothing read as a\n * solid black block (the beta.261 Synchronicity Gym hero), and an empty\n * rounded gray card as a broken picture (the beta.262 Adult Hybrid Learning\n * Center hero). A column of a split row taken out leaves the words' column\n * the row's full width. A cover's section stays pinned to the site's dark\n * scheme, which paints that scheme's background and text colors, so the\n * words written for a dark photo stay readable. A slot a photo fills, a\n * starter's included, is kept.\n */\nexport function aiLayoutDropEmptyPictures(\n nodes: NodesMap,\n slots: readonly AiLayoutPictureSlot[],\n photos: ReadonlyArray<AiLayoutPicturePhoto | null>,\n sectionIds: readonly string[] = [],\n): NodesMap {\n const map = nodes as unknown as Record<string, Record<string, unknown>>\n const empty = slots.filter((slot, index) => {\n if (photos[index]?.src) return false\n const src = (map[slot.imageId]?.['props'] as Record<string, unknown> | undefined)?.['src']\n return Boolean(map[slot.imageId]) && !(typeof src === 'string' && src.trim())\n })\n if (!empty.length) return nodes\n const next = { ...map }\n const children = (id: string) => (next[id]?.['nodes'] as string[] | undefined) ?? []\n const parentOf = (id: string) => Object.keys(next).find((key) => children(key).includes(id)) ?? null\n const remove = (id: string) => {\n const parent = parentOf(id)\n if (parent) next[parent] = { ...next[parent], nodes: children(parent).filter((child) => child !== id) }\n delete next[id]\n return parent\n }\n const sections = new Set(sectionIds)\n for (const slot of empty) {\n const own = new Set([slot.imageId, slot.iconId].filter(Boolean) as string[])\n const frameHoldsOnly = Boolean(slot.frameId && next[slot.frameId] && children(slot.frameId).every((child) => own.has(child)))\n let parent = remove(slot.imageId)\n if (slot.iconId && next[slot.iconId]) remove(slot.iconId)\n if (!frameHoldsOnly) continue\n // The frame, then each wrapper it leaves empty: never a section, nor the page's root.\n let at: string | null = slot.frameId\n while (at && next[at] && !children(at).length && !sections.has(at) && parentOf(at) !== null) {\n const column = isGridColumn(next[at])\n parent = remove(at)\n at = column ? null : parent\n }\n // A split row left with one column gives its words the row's width.\n if (parent && isGridRow(next[parent])) {\n const [only, ...rest] = children(parent)\n if (only && !rest.length && isGridColumn(next[only])) {\n const column = next[only] as Record<string, unknown>\n const sx = { ...((column['sx'] as Record<string, unknown> | undefined) ?? {}) }\n delete sx['order']\n next[only] = {\n ...column,\n props: { ...((column['props'] as Record<string, unknown> | undefined) ?? {}), size: 'xs:12' },\n sx: Object.keys(sx).length ? sx : null,\n }\n }\n }\n }\n return next as unknown as NodesMap\n}\n\n/**\n * A source of photos found for the slots themselves — a stock search copied\n * into the site's library — answering per slot, in order: a photo; or\n * {@link AI_LAYOUT_NO_PHOTO} where the library answered and nothing fits,\n * which leaves the slot without a photo; or `null` where the library could\n * not be asked, which takes a starter photo. It may throw, and then every\n * slot takes a starter.\n */\nexport type AiLayoutPictureSource = (\n slots: readonly AiLayoutPictureSlot[],\n) => Promise<ReadonlyArray<AiLayoutPicturePhoto | null>>\n\n/**\n * Fills a stored page's empty picture slots (AGL-3660): from `source` where\n * one is given and it answers; a slot it answers with nothing fitting stays\n * without a photo and is taken out ({@link aiLayoutDropEmptyPictures}); every\n * other slot takes a starter photo, described by its own words (AGL-3831). Never throws, and never leaves a page worse than it\n * came: a picture is decoration, and a job never fails over one.\n */\nexport async function aiResolveLayoutPictures(\n nodes: NodesMap,\n input: {\n rootId: string\n sectionIds: readonly string[]\n sectionNames: readonly string[]\n /** A per-job word, so two sites do not open with the same photo. */\n seed: string\n source?: AiLayoutPictureSource | null\n /** The site's language (AGL-3750); a starter photo's English words are not its own. */\n language?: string\n },\n): Promise<NodesMap> {\n try {\n const slots = aiLayoutPictureSlots(nodes, input.rootId, {\n ids: input.sectionIds,\n names: input.sectionNames,\n })\n if (!slots.length) return nodes\n let found: ReadonlyArray<AiLayoutPicturePhoto | null> = []\n if (input.source) {\n try {\n found = await input.source(slots)\n } catch (error) {\n console.warn('ai layout pictures: the stock source failed; starter photos fill the page', { error })\n found = []\n }\n }\n // The starters' words are English; a site in another language says it\n // shows an illustrative photo, true of any photo, as a stock photo's does.\n const otherLanguage = aiLayoutSpeaksOtherThanEnglish(input.language)\n const starters = aiLayoutStarterPhotos(slots, input.seed).map((photo) =>\n otherLanguage ? { ...photo, alt: aiLayoutLabel('illustrativePhoto', input.language) } : photo,\n )\n const photos = slots.map((_slot, index) => {\n const answer = found[index]\n if (answer) return answer.src ? answer : null\n return starters[index] ?? null\n })\n return aiLayoutDropEmptyPictures(aiLayoutPlacePictures(nodes, slots, photos), slots, photos, input.sectionIds)\n } catch (error) {\n console.error('ai layout pictures: the page keeps its empty picture slots', { error })\n return nodes\n }\n}\n"],"names":["DEFAULT_SITE_IMAGES","aiLayoutLabel","aiLayoutSpeaksOtherThanEnglish","PEOPLE","aspectOf","value","byBreakpoint","breakpoint","ratio","match","exec","String","Number","isFinite","COVER_ASPECT","slotAspect","image","frame","own","sx","aiLayoutPictureRole","input","sectionIndex","test","sectionName","alt","aiLayoutPictureSlots","nodes","rootId","sections","map","slots","seen","Set","visit","id","parentId","node","has","add","index","ids","indexOf","here","componentId","src","props","trim","undefined","iconId","find","child","push","imageId","frameId","aspect","standIn","role","names","AI_LAYOUT_STARTER_PHOTOS","AI_LAYOUT_NO_PHOTO","Object","freeze","width","height","STARTER_POOLS","hero","about","gallery","ALL_STARTERS","keys","aiLayoutSeedNumber","seed","hash","length","charCodeAt","Math","imul","rotate","list","by","start","slice","aiLayoutStarterPhotos","turn","order","served","slot","sort","a","b","placedAt","Map","forEach","step","unused","pool","rest","filter","name","includes","candidates","candidate","beside","Boolean","get","set","photo","aiLayoutPlacePictures","photos","next","lead","findIndex","_","intrinsicWidth","intrinsicHeight","loading","isGridRow","isGridColumn","aiLayoutDropEmptyPictures","sectionIds","empty","children","parentOf","key","remove","parent","frameHoldsOnly","every","at","column","only","size","aiResolveLayoutPictures","sectionNames","found","source","error","console","warn","otherLanguage","language","starters","_slot","answer"],"mappings":"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"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import type { NodesMap } from '@aglyn/aglyn/types/nodes';
|
|
18
|
+
import type { AiDoctrineViolation } from '../runtime/ai-doctrine-validators';
|
|
19
|
+
/**
|
|
20
|
+
* No section that promises its own content later (AGL-3887). A guided start
|
|
21
|
+
* publishes what it builds, so a section whose words say it is still being
|
|
22
|
+
* filled — "We are building this space for the words of families we have
|
|
23
|
+
* worked with", the live Whitmore Lane Family Law home of 2026-10-16 — is a
|
|
24
|
+
* stand-in a visitor reads as an unfinished site. A page answer holding one
|
|
25
|
+
* is asked again; the last answer a generation takes has the section taken
|
|
26
|
+
* out instead (`aiLayoutPageCheck`), its first section aside.
|
|
27
|
+
*
|
|
28
|
+
* Scoped to the page speaking of itself: "this space", "this section", the
|
|
29
|
+
* site's own reviews, photos or portfolio promised for later. "Coming soon"
|
|
30
|
+
* alone is never one: an event, a menu item or a new location is genuinely
|
|
31
|
+
* coming soon on a dated list. A coming-soon site, whose whole point is what
|
|
32
|
+
* is coming, is not held to it.
|
|
33
|
+
*/
|
|
34
|
+
/** The violation code a section that promises its content later is named by. */
|
|
35
|
+
export declare const AI_LAYOUT_PROMISED_COPY_CODE = "layout-promised-copy";
|
|
36
|
+
/** Whether a line promises the page's own content later, rather than saying it. */
|
|
37
|
+
export declare function aiLayoutPromisesContent(text: string): boolean;
|
|
38
|
+
/**
|
|
39
|
+
* The plan indices of a built page's sections that promise their content
|
|
40
|
+
* later, each with the line that does, in page order.
|
|
41
|
+
*/
|
|
42
|
+
export declare function aiLayoutPromisedSections(nodes: NodesMap, sectionIds: readonly string[]): Array<{
|
|
43
|
+
index: number;
|
|
44
|
+
line: string;
|
|
45
|
+
}>;
|
|
46
|
+
/** The violation that asks again for the sections that promise their content later. */
|
|
47
|
+
export declare function aiLayoutPromisedCopyViolations(screen: {
|
|
48
|
+
sections: ReadonlyArray<{
|
|
49
|
+
name: string;
|
|
50
|
+
}>;
|
|
51
|
+
}, found: ReadonlyArray<{
|
|
52
|
+
index: number;
|
|
53
|
+
line: string;
|
|
54
|
+
}>): AiDoctrineViolation[];
|