@aglyn/plugins-ai 1.0.0-beta.243 → 1.0.0-beta.246
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +17 -17
- package/src/lib/activity/ai-activity-actions.js +3 -1
- package/src/lib/activity/ai-activity-actions.js.map +1 -1
- package/src/lib/components/ai-job-links.js +7 -0
- package/src/lib/components/ai-job-links.js.map +1 -1
- package/src/lib/components/ai-product-zones.d.ts +3 -1
- package/src/lib/components/ai-product-zones.js.map +1 -1
- package/src/lib/components/ai-products-hub-card.component.js +1 -1
- package/src/lib/components/ai-products-hub-card.component.js.map +1 -1
- package/src/lib/components/ai-products-proposals.component.js +17 -9
- package/src/lib/components/ai-products-proposals.component.js.map +1 -1
- package/src/lib/components/ai-site-start-card.component.js +6 -4
- package/src/lib/components/ai-site-start-card.component.js.map +1 -1
- package/src/lib/components/ai-upsell-dialog.component.js +1 -1
- package/src/lib/components/ai-upsell-dialog.component.js.map +1 -1
- package/src/lib/jobs/ai-build-capabilities.js +162 -2
- package/src/lib/jobs/ai-build-capabilities.js.map +1 -1
- package/src/lib/jobs/ai-job-build-step.d.ts +7 -0
- package/src/lib/jobs/ai-job-build-step.js +85 -16
- package/src/lib/jobs/ai-job-build-step.js.map +1 -1
- package/src/lib/jobs/ai-job-duplicate-step.d.ts +123 -0
- package/src/lib/jobs/ai-job-duplicate-step.js +269 -0
- package/src/lib/jobs/ai-job-duplicate-step.js.map +1 -0
- package/src/lib/jobs/ai-job-layout-language.js +24 -6
- package/src/lib/jobs/ai-job-layout-language.js.map +1 -1
- package/src/lib/jobs/ai-job-navigation-step.d.ts +157 -0
- package/src/lib/jobs/ai-job-navigation-step.js +439 -0
- package/src/lib/jobs/ai-job-navigation-step.js.map +1 -0
- package/src/lib/jobs/ai-job-page-language.d.ts +5 -2
- package/src/lib/jobs/ai-job-page-language.js +38 -10
- package/src/lib/jobs/ai-job-page-language.js.map +1 -1
- package/src/lib/jobs/ai-job-page-sections.js +1 -1
- package/src/lib/jobs/ai-job-page-sections.js.map +1 -1
- package/src/lib/jobs/ai-job-page-step.js +11 -4
- package/src/lib/jobs/ai-job-page-step.js.map +1 -1
- package/src/lib/jobs/ai-job-plan-step.d.ts +4 -2
- package/src/lib/jobs/ai-job-plan-step.js +16 -7
- package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
- package/src/lib/jobs/ai-job-products-step.d.ts +2 -2
- package/src/lib/jobs/ai-job-products-step.js +2 -2
- package/src/lib/jobs/ai-job-products-step.js.map +1 -1
- package/src/lib/jobs/ai-job-settings-step.d.ts +272 -0
- package/src/lib/jobs/ai-job-settings-step.js +695 -0
- package/src/lib/jobs/ai-job-settings-step.js.map +1 -0
- package/src/lib/jobs/ai-job-site-content.d.ts +20 -6
- package/src/lib/jobs/ai-job-site-content.js +28 -12
- package/src/lib/jobs/ai-job-site-content.js.map +1 -1
- package/src/lib/jobs/ai-job-site-datasets.d.ts +2 -0
- package/src/lib/jobs/ai-job-site-datasets.js +10 -3
- package/src/lib/jobs/ai-job-site-datasets.js.map +1 -1
- package/src/lib/jobs/ai-job-site-step.d.ts +3 -1
- package/src/lib/jobs/ai-job-site-step.js +86 -12
- package/src/lib/jobs/ai-job-site-step.js.map +1 -1
- package/src/lib/jobs/ai-site-publish.d.ts +76 -4
- package/src/lib/jobs/ai-site-publish.js +231 -59
- package/src/lib/jobs/ai-site-publish.js.map +1 -1
- package/src/lib/layout-language/ai-layout-compiler.d.ts +7 -4
- package/src/lib/layout-language/ai-layout-compiler.js +26 -25
- package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
- package/src/lib/layout-language/ai-layout-frame-hollows.d.ts +41 -0
- package/src/lib/layout-language/ai-layout-frame-hollows.js +330 -0
- package/src/lib/layout-language/ai-layout-frame-hollows.js.map +1 -0
- package/src/lib/layout-language/ai-layout-frame.d.ts +18 -1
- package/src/lib/layout-language/ai-layout-frame.js +83 -13
- package/src/lib/layout-language/ai-layout-frame.js.map +1 -1
- package/src/lib/layout-language/ai-layout-links.js +5 -2
- package/src/lib/layout-language/ai-layout-links.js.map +1 -1
- package/src/lib/layout-language/ai-layout-listings.d.ts +35 -0
- package/src/lib/layout-language/ai-layout-listings.js +34 -0
- package/src/lib/layout-language/ai-layout-listings.js.map +1 -1
- package/src/lib/layout-language/ai-layout-tree.d.ts +2 -0
- package/src/lib/layout-language/ai-layout-tree.js +17 -0
- package/src/lib/layout-language/ai-layout-tree.js.map +1 -1
- package/src/lib/model/ai-build-job.d.ts +23 -0
- package/src/lib/model/ai-build-job.js +102 -13
- package/src/lib/model/ai-build-job.js.map +1 -1
- package/src/lib/model/ai-build-plan.d.ts +25 -0
- package/src/lib/model/ai-build-plan.js +45 -1
- package/src/lib/model/ai-build-plan.js.map +1 -1
- package/src/lib/model/ai-credit-estimate.d.ts +52 -0
- package/src/lib/model/ai-credit-estimate.js +51 -0
- package/src/lib/model/ai-credit-estimate.js.map +1 -1
- package/src/lib/model/ai-job-activity.js +4 -1
- package/src/lib/model/ai-job-activity.js.map +1 -1
- package/src/lib/model/ai-jobs.types.d.ts +10 -2
- package/src/lib/model/ai-jobs.types.js +4 -1
- package/src/lib/model/ai-jobs.types.js.map +1 -1
- package/src/lib/model/ai-layout-menus.d.ts +104 -0
- package/src/lib/model/ai-layout-menus.js +518 -0
- package/src/lib/model/ai-layout-menus.js.map +1 -0
- package/src/lib/model/ai-products.d.ts +31 -1
- package/src/lib/model/ai-products.js +34 -1
- package/src/lib/model/ai-products.js.map +1 -1
- package/src/lib/model/ai-site-inventory.d.ts +15 -0
- package/src/lib/model/ai-site-inventory.js.map +1 -1
- package/src/lib/model/ai-site-job.d.ts +10 -0
- package/src/lib/model/ai-site-job.js +36 -9
- package/src/lib/model/ai-site-job.js.map +1 -1
- package/src/lib/model/ai-site-store-pages.js +1 -1
- package/src/lib/model/ai-site-store-pages.js.map +1 -1
- package/src/lib/runtime/ai-doctrine-validators.d.ts +95 -1
- package/src/lib/runtime/ai-doctrine-validators.js +264 -36
- package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
- package/src/lib/runtime/ai-doctrine.js +7 -2
- package/src/lib/runtime/ai-doctrine.js.map +1 -1
- package/src/lib/runtime/ai-palette.generated.js +1 -1
- package/src/lib/runtime/ai-palette.generated.js.map +1 -1
- package/src/lib/runtime/ai-products-generation.d.ts +4 -2
- package/src/lib/runtime/ai-products-generation.js +12 -6
- package/src/lib/runtime/ai-products-generation.js.map +1 -1
- package/src/lib/runtime/site-inventory.d.ts +12 -0
- package/src/lib/runtime/site-inventory.js +29 -1
- package/src/lib/runtime/site-inventory.js.map +1 -1
- package/src/lib/server.js +12 -0
- package/src/lib/server.js.map +1 -1
- package/src/lib/testing/stand-in-dataset-index.js +37 -3
- package/src/lib/testing/stand-in-dataset-index.js.map +1 -1
- package/src/lib/tools/ai-products-tool.d.ts +3 -1
- package/src/lib/tools/ai-products-tool.js +15 -5
- package/src/lib/tools/ai-products-tool.js.map +1 -1
- package/src/lib/usage/ai-quota-checks.d.ts +13 -3
- package/src/lib/usage/ai-quota-checks.js +46 -8
- package/src/lib/usage/ai-quota-checks.js.map +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/layout-language/ai-layout-listings.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\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 *\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'\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 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}\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 } as const\n\nconst PLACEMENT_ROLES: readonly AiLayoutListingRole[] = ['featured', 'index']\nconst KINDS: readonly AiLayoutListingKind[] = ['products', 'posts', 'tracks', 'reviews', 'signup', 'records']\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}\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 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 // 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'\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 ...(kind === 'products' && record['cart'] === false ? { cart: false } : {}),\n ...(kind === 'products' && actionLabel && SITE_PATH.test(actionHref) ? { emptyAction: { label: actionLabel, href: actionHref } } : {}),\n placements,\n })\n }\n return listings\n}\n\n/** The listing a section of a page places, and how; `null` for a section that places none. */\nexport function aiLayoutListingAt(\n listings: readonly AiLayoutListing[],\n screenId: string | null | undefined,\n section: number,\n): { listing: AiLayoutListing; role: AiLayoutListingRole } | null {\n if (!screenId) return null\n for (const listing of listings) {\n const placement = listing.placements.find((entry) => entry.screenId === screenId && entry.section === section)\n if (placement) return { listing, role: placement.role }\n }\n return null\n}\n\n// ── Where a site's pages place them ────────────────────────────────────\n\n/** The plan of a page, as a placement is chosen from it. */\nexport interface AiLayoutListingScreen {\n id?: string | null\n title: string\n slug: string\n sections: ReadonlyArray<{ name: string; items: number; uses?: readonly string[] }>\n}\n\nconst PRODUCT_PAGE = /\\b(shop|store|products?|catalog(?:ue)?|collections?|range|browse)\\b/i\nconst PRODUCT_SECTION =\n /\\b(shop|store|products?|catalog(?:ue)?|collections?|range|bestsellers?|best[- ]sellers?|new arrivals|arrivals|featured|favou?rites|signature)\\b/i\nconst POST_SECTION = /\\b(blog|posts?|articles?|writing|journal|stories|essays?|latest|recent|news|featured)\\b/i\n/** A music site's page or section about its recordings (AGL-3716). */\nconst MUSIC_PAGE = /\\b(music|listen|tracks?|songs?|releases?|discography|albums?|eps?|singles?|sounds?)\\b/i\n\n/**\n * The section of a shop's own page the whole catalog fills (AGL-3676): the\n * first after its opening whose name says products (\"Product grid\", \"The\n * range\"), else the section planned with the most items, else its second.\n * The live Hearth & Wick start (2026-10-09) planned \"Product range image\n * cards\" — six cards naming kinds of candle, with no product, price or cart.\n */\nexport function aiLayoutShopGridSection(sections: ReadonlyArray<{ name: string; items: number }>): number {\n const named = sections.findIndex((section, index) => index > 0 && PRODUCT_SECTION.test(section.name) && !SHOP_FRAMING.test(section.name))\n if (named !== -1) return named\n const most = Math.max(0, ...sections.map((section) => section.items))\n const byItems = most > 0 ? sections.findIndex((section) => section.items === most) : -1\n return byItems !== -1 ? byItems : sections.length > 1 ? 1 : 0\n}\n\n/**\n * The section of a store's home its bestsellers fill (AGL-3676): the first\n * after the hero whose name says them (\"Bestsellers\", \"Featured candles\", \"New\n * arrivals\"), never one about the store's collections, its reviews or its\n * sign-up; else the first after the hero naming products at all; else the\n * first after the hero planned with items that is none of those. The\n * beta.237 Willow Wick home would otherwise have listed products in its\n * \"Shop by collection\" tiles.\n */\nfunction aiLayoutFeaturedSection(screen: AiLayoutListingScreen): number {\n // A section about the collections that does not also name the bestsellers.\n const other = (name: string) =>\n (aiStorefrontSays('collections', name) && !aiStorefrontSays('featured', name)) ||\n aiStorefrontSays('reviews', name) ||\n aiStorefrontSays('signup', name)\n const first = (test: (name: string, items: number) => boolean) =>\n screen.sections.findIndex((section, index) => index > 0 && test(section.name, section.items))\n for (const test of [\n (name: string) => aiStorefrontSays('featured', name) && !other(name),\n (name: string) => PRODUCT_SECTION.test(name) && !other(name),\n (name: string, items: number) => items > 0 && !other(name),\n (name: string) => PRODUCT_SECTION.test(name),\n ]) {\n const found = first(test)\n if (found !== -1) return found\n }\n return -1\n}\n\n/** A section of a shop page that only opens or closes it: \"Shop intro heading\", \"Gift help call to action\". */\nconst SHOP_FRAMING = /\\b(hero|intro(?:duction)?|heading|banner|cta|call to action|contact)\\b/i\n\n/** Whether a planned page is the store's own Shop page: its address or its name says it sells. */\nexport function aiLayoutIsShopPage(screen: { slug: string; title: string }): boolean {\n return !isHome(screen.slug) && PRODUCT_PAGE.test(`${firstSegment(screen.slug)} ${screen.title}`)\n}\n\nconst firstSegment = (slug: string) => slug.trim().replace(/^\\/+/, '').split('/')[0].toLowerCase()\nconst isHome = (slug: string) => firstSegment(slug) === ''\n\n/**\n * The sections of a site's pages that show a listing, chosen by code from the\n * plan (AGL-3676), never asked of the model:\n *\n * - PRODUCTS. The shop's own page — the page whose address or name says it\n * sells (`/shop`, \"Shop\", \"Products\") — shows the whole catalog in the\n * section the plan gave the most items, else its second; and the home\n * page features a few in its first section after the hero whose name\n * says products (\"Featured candles\", \"Bestsellers\"), else in the first\n * section after the hero planned with items.\n * - POSTS. The home features the latest in its first section of items after\n * the hero whose name says writing (\"Featured writing\", \"Latest posts\"),\n * else its first section of items after the hero; and any other page\n * whose name says it is about the writing does the same. The blog's own\n * index is the platform's.\n *\n * A section placed once is placed for one listing.\n */\nexport function aiLayoutListingPlacements(\n kind: AiLayoutListingKind,\n screens: readonly AiLayoutListingScreen[],\n): AiLayoutListingPlacement[] {\n const placements: AiLayoutListingPlacement[] = []\n const place = (screen: AiLayoutListingScreen, section: number, role: AiLayoutListingRole) => {\n if (typeof screen.id === 'string' && screen.id && section >= 0) placements.push({ screenId: screen.id, section, role })\n }\n const afterHero = (screen: AiLayoutListingScreen, test: (name: string, items: number) => boolean) =>\n screen.sections.findIndex((section, index) => index > 0 && test(section.name, section.items))\n if (kind === 'tracks') {\n // A music site's player (AGL-3716): on its Music page, in the section that\n // says it is about the recordings, else the one after the opening; on the\n // home, only in a section named for them. A site with neither gets it on\n // its home, after the opening.\n for (const screen of screens) {\n if (isHome(screen.slug)) {\n place(screen, afterHero(screen, (name) => MUSIC_PAGE.test(name)), 'featured')\n continue\n }\n if (!MUSIC_PAGE.test(`${firstSegment(screen.slug)} ${screen.title}`)) continue\n const named = afterHero(screen, (name) => MUSIC_PAGE.test(name))\n place(screen, named !== -1 ? named : screen.sections.length > 1 ? 1 : 0, 'index')\n }\n if (!placements.length) {\n const home = screens.find((screen) => isHome(screen.slug))\n if (home) place(home, home.sections.length > 1 ? 1 : 0, 'featured')\n }\n return placements\n }\n if (kind === 'reviews' || kind === 'signup') {\n // A store's own reviews, on its home, in the section named for them; its\n // newsletter sign-up in any page's section named for it that places no\n // form of the plan's (AGL-3676). Neither is ever a page's opening.\n for (const screen of screens) {\n if (kind === 'reviews' && !isHome(screen.slug)) continue\n place(\n screen,\n afterHero(\n screen,\n (name) => aiStorefrontSays(kind, name),\n ),\n kind === 'reviews' ? 'featured' : 'index',\n )\n }\n return kind === 'signup'\n ? placements.filter((placement) => {\n const screen = screens.find((entry) => entry.id === placement.screenId)\n return !(screen?.sections[placement.section]?.uses ?? []).some((use) => /form/i.test(use))\n })\n : placements\n }\n if (kind === 'products') {\n for (const screen of screens) {\n if (isHome(screen.slug)) {\n place(screen, aiLayoutFeaturedSection(screen), 'featured')\n continue\n }\n if (!aiLayoutIsShopPage(screen)) continue\n place(screen, aiLayoutShopGridSection(screen.sections), 'index')\n }\n return placements\n }\n for (const screen of screens) {\n if (isHome(screen.slug)) {\n // On a blog's home the band that names the writing (\"Featured posts\n // cards\", \"Featured journeys\") lists it; failing one, the first band of\n // items after the hero. A live Clay Notes plan (2026-10-09) opened its\n // home with \"What you will find\" before \"Featured posts cards\".\n const named = afterHero(screen, (name, items) => items > 0 && POST_SECTION.test(name))\n place(screen, named !== -1 ? named : afterHero(screen, (_name, items) => items > 0), 'featured')\n continue\n }\n if (!POST_SECTION.test(screen.title)) continue\n place(screen, afterHero(screen, (name) => POST_SECTION.test(name)), 'featured')\n }\n return placements\n}\n\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"],"names":["aiStorefrontSays","AI_LAYOUT_LISTINGS_INPUT","aiLayoutListingId","kind","datasetId","AI_LAYOUT_LISTING_ELEMENTS","products","posts","tracks","reviews","signup","records","AI_LAYOUT_CART_ELEMENT","AI_LAYOUT_FEATURED_RECORDS","PLACEMENT_ROLES","KINDS","text","value","trim","LISTING_NAMES","SITE_PATH","DATASET_ID","FIELD_ID","listingFieldsOf","raw","Array","isArray","flatMap","field","id","test","name","slice","type","aiLayoutListingsOf","inputs","listings","entry","record","fields","imageField","includes","some","listing","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","replace","split","toLowerCase","aiLayoutListingPlacements","screens","place","afterHero","home","uses","use","_name","aiLayoutRecordsPlacements","dataset","forEach","lists","ref"],"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 *\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'\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}\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 } as const\n\nconst PLACEMENT_ROLES: readonly AiLayoutListingRole[] = ['featured', 'index']\nconst KINDS: readonly AiLayoutListingKind[] = ['products', 'posts', 'tracks', 'reviews', 'signup', 'records']\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}\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'\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"],"names":["aiStorefrontSays","AI_LAYOUT_LISTINGS_INPUT","aiLayoutListingId","kind","datasetId","AI_LAYOUT_LISTING_ELEMENTS","products","posts","tracks","reviews","signup","records","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"],"mappings":"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"}
|
|
@@ -57,3 +57,5 @@ export declare class AiLayoutTreeBuilder {
|
|
|
57
57
|
/** The tree rooted at `rootId`. */
|
|
58
58
|
tree(rootId: string): AiLayoutRawTree;
|
|
59
59
|
}
|
|
60
|
+
/** Spans out of twelve for relative widths: each at least three, summing to twelve. */
|
|
61
|
+
export declare function aiLayoutSpans(weights: readonly number[]): number[];
|
|
@@ -80,5 +80,22 @@
|
|
|
80
80
|
this.count = 0;
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
|
+
/** Spans out of twelve for relative widths: each at least three, summing to twelve. */ export function aiLayoutSpans(weights) {
|
|
84
|
+
const total = weights.reduce((sum, weight)=>sum + weight, 0);
|
|
85
|
+
const spans = weights.map((weight)=>Math.max(3, Math.round(weight / total * 12)));
|
|
86
|
+
let over = spans.reduce((sum, span)=>sum + span, 0) - 12;
|
|
87
|
+
while(over !== 0){
|
|
88
|
+
// Take from (or give to) the widest column that can spare it.
|
|
89
|
+
const order = spans.map((span, position)=>({
|
|
90
|
+
span,
|
|
91
|
+
position
|
|
92
|
+
})).sort((a, b)=>over > 0 ? b.span - a.span : a.span - b.span);
|
|
93
|
+
const pick = order.find((entry)=>over > 0 ? entry.span > 3 : true);
|
|
94
|
+
if (!pick) break;
|
|
95
|
+
spans[pick.position] += over > 0 ? -1 : 1;
|
|
96
|
+
over += over > 0 ? -1 : 1;
|
|
97
|
+
}
|
|
98
|
+
return spans;
|
|
99
|
+
}
|
|
83
100
|
|
|
84
101
|
//# sourceMappingURL=ai-layout-tree.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/layout-language/ai-layout-tree.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * The node map the layout language compiler writes (AGL-3660), in the shape a\n * model's answer arrives in — `{ rootId, nodes: { id: { componentId, props,\n * sx, nodes } } }` — so the palette validator and the doctrine read it exactly\n * as they read any other tree, and nothing downstream knows code wrote it.\n */\n\nexport interface AiLayoutRawNode {\n componentId: string\n props?: Record<string, unknown>\n sx?: Record<string, unknown>\n nodes?: string[]\n /**\n * The platform's interactions, written by code: a link to a section of its\n * page scrolls there (AGL-3097). The palette validator strips every\n * interaction from what it stores, so the page step carries these over\n * onto the stored nodes after it.\n */\n interactions?: unknown[]\n}\n\nexport interface AiLayoutRawTree {\n rootId: string\n nodes: Record<string, AiLayoutRawNode>\n}\n\n/** Elements that only frame what they hold, and so mean nothing empty. */\nexport const AI_LAYOUT_FRAMES: ReadonlySet<string> = new Set([\n 'muiStack',\n 'muiBox',\n 'muiGrid',\n 'muiCard',\n 'muiCardContent',\n 'muiContainer',\n 'muiList',\n 'muiListItem',\n 'muiAccordion',\n 'muiAccordionDetails',\n 'muiDrawer',\n 'section',\n])\n\n/** A node map built in document order, with ids that read in a review: `ll3-heading`. */\nexport class AiLayoutTreeBuilder {\n readonly nodes: Record<string, AiLayoutRawNode> = {}\n private count = 0\n\n constructor(private readonly prefix = 'll') {}\n\n /** A node, with only the props, styles and children it was given; returns its id. */\n add(\n componentId: string,\n props?: Record<string, unknown> | null,\n sx?: Record<string, unknown> | null,\n children?: readonly (string | null | undefined)[] | null,\n role = componentId,\n fixedId?: string,\n ): string {\n this.count += 1\n const id =\n fixedId ??\n `${this.prefix}${this.count}-${role.replace(/[^A-Za-z0-9]/g, '').slice(0, 16)}`\n const node: AiLayoutRawNode = { componentId }\n const kept = props\n ? Object.fromEntries(\n Object.entries(props).filter(([, value]) => value !== undefined),\n )\n : {}\n if (Object.keys(kept).length) node.props = kept\n if (sx && Object.keys(sx).length) node.sx = { ...sx }\n const list = (children ?? []).filter(\n (child): child is string => typeof child === 'string',\n )\n if (list.length) node.nodes = list\n this.nodes[id] = node\n return id\n }\n\n /**\n * Takes out every frame left holding nothing — a card whose words were all\n * filler, the cell that held it — from the leaves up, so no element meant\n * to hold content is stored empty (rule 16). `keep` names the ones that\n * stay whatever they hold.\n */\n prune(rootId: string, keep: ReadonlySet<string> = new Set()): void {\n const visit = (id: string): boolean => {\n const node = this.nodes[id]\n if (!node) return false\n if (node.nodes) {\n node.nodes = node.nodes.filter(visit)\n if (!node.nodes.length) delete node.nodes\n }\n if (\n id === rootId ||\n keep.has(id) ||\n node.nodes?.length ||\n !AI_LAYOUT_FRAMES.has(node.componentId)\n )\n return true\n delete this.nodes[id]\n return false\n }\n visit(rootId)\n }\n\n /** The tree rooted at `rootId`. */\n tree(rootId: string): AiLayoutRawTree {\n return { rootId, nodes: this.nodes }\n }\n}\n"],"names":["AI_LAYOUT_FRAMES","Set","AiLayoutTreeBuilder","add","componentId","props","sx","children","role","fixedId","count","id","prefix","replace","slice","node","kept","Object","fromEntries","entries","filter","value","undefined","keys","length","list","child","nodes","prune","rootId","keep","visit","has","tree"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;CAKC;AAqBD,wEAAwE,GACxE,OAAO,MAAMA,mBAAwC,IAAIC,IAAI;IAC3D;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;CACD,EAAC;AAEF,uFAAuF,GACvF,OAAO,MAAMC;IAMX,mFAAmF,GACnFC,IACEC,WAAmB,EACnBC,KAAsC,EACtCC,EAAmC,EACnCC,QAAwD,EACxDC,OAAOJ,WAAW,EAClBK,OAAgB,EACR;QACR,IAAI,CAACC,KAAK,IAAI;QACd,MAAMC,KACJF,kBAAAA,UACA,GAAG,IAAI,CAACG,MAAM,GAAG,IAAI,CAACF,KAAK,CAAC,CAAC,EAAEF,KAAKK,OAAO,CAAC,iBAAiB,IAAIC,KAAK,CAAC,GAAG,KAAK;QACjF,MAAMC,OAAwB;YAAEX;QAAY;QAC5C,MAAMY,OAAOX,QACTY,OAAOC,WAAW,CAChBD,OAAOE,OAAO,CAACd,OAAOe,MAAM,CAAC,CAAC,GAAGC,MAAM,GAAKA,UAAUC,cAExD,CAAC;QACL,IAAIL,OAAOM,IAAI,CAACP,MAAMQ,MAAM,EAAET,KAAKV,KAAK,GAAGW;QAC3C,IAAIV,MAAMW,OAAOM,IAAI,CAACjB,IAAIkB,MAAM,EAAET,KAAKT,EAAE,GAAG,aAAKA;QACjD,MAAMmB,OAAO,CAAClB,mBAAAA,WAAY,EAAE,EAAEa,MAAM,CAClC,CAACM,QAA2B,OAAOA,UAAU;QAE/C,IAAID,KAAKD,MAAM,EAAET,KAAKY,KAAK,GAAGF;QAC9B,IAAI,CAACE,KAAK,CAAChB,GAAG,GAAGI;QACjB,OAAOJ;IACT;IAEA;;;;;GAKC,GACDiB,MAAMC,MAAc,EAAEC,OAA4B,IAAI7B,KAAK,EAAQ;QACjE,MAAM8B,QAAQ,CAACpB;gBAUXI;YATF,MAAMA,OAAO,IAAI,CAACY,KAAK,CAAChB,GAAG;YAC3B,IAAI,CAACI,MAAM,OAAO;YAClB,IAAIA,KAAKY,KAAK,EAAE;gBACdZ,KAAKY,KAAK,GAAGZ,KAAKY,KAAK,CAACP,MAAM,CAACW;gBAC/B,IAAI,CAAChB,KAAKY,KAAK,CAACH,MAAM,EAAE,OAAOT,KAAKY,KAAK;YAC3C;YACA,IACEhB,OAAOkB,UACPC,KAAKE,GAAG,CAACrB,SACTI,cAAAA,KAAKY,KAAK,qBAAVZ,YAAYS,MAAM,KAClB,CAACxB,iBAAiBgC,GAAG,CAACjB,KAAKX,WAAW,GAEtC,OAAO;YACT,OAAO,IAAI,CAACuB,KAAK,CAAChB,GAAG;YACrB,OAAO;QACT;QACAoB,MAAMF;IACR;IAEA,iCAAiC,GACjCI,KAAKJ,MAAc,EAAmB;QACpC,OAAO;YAAEA;YAAQF,OAAO,IAAI,CAACA,KAAK;QAAC;IACrC;IA7DA,YAAY,AAAiBf,SAAS,IAAI,CAAE;aAAfA,SAAAA;aAHpBe,QAAyC,CAAC;aAC3CjB,QAAQ;IAE6B;AA8D/C"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/layout-language/ai-layout-tree.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * The node map the layout language compiler writes (AGL-3660), in the shape a\n * model's answer arrives in — `{ rootId, nodes: { id: { componentId, props,\n * sx, nodes } } }` — so the palette validator and the doctrine read it exactly\n * as they read any other tree, and nothing downstream knows code wrote it.\n */\n\nexport interface AiLayoutRawNode {\n componentId: string\n props?: Record<string, unknown>\n sx?: Record<string, unknown>\n nodes?: string[]\n /**\n * The platform's interactions, written by code: a link to a section of its\n * page scrolls there (AGL-3097). The palette validator strips every\n * interaction from what it stores, so the page step carries these over\n * onto the stored nodes after it.\n */\n interactions?: unknown[]\n}\n\nexport interface AiLayoutRawTree {\n rootId: string\n nodes: Record<string, AiLayoutRawNode>\n}\n\n/** Elements that only frame what they hold, and so mean nothing empty. */\nexport const AI_LAYOUT_FRAMES: ReadonlySet<string> = new Set([\n 'muiStack',\n 'muiBox',\n 'muiGrid',\n 'muiCard',\n 'muiCardContent',\n 'muiContainer',\n 'muiList',\n 'muiListItem',\n 'muiAccordion',\n 'muiAccordionDetails',\n 'muiDrawer',\n 'section',\n])\n\n/** A node map built in document order, with ids that read in a review: `ll3-heading`. */\nexport class AiLayoutTreeBuilder {\n readonly nodes: Record<string, AiLayoutRawNode> = {}\n private count = 0\n\n constructor(private readonly prefix = 'll') {}\n\n /** A node, with only the props, styles and children it was given; returns its id. */\n add(\n componentId: string,\n props?: Record<string, unknown> | null,\n sx?: Record<string, unknown> | null,\n children?: readonly (string | null | undefined)[] | null,\n role = componentId,\n fixedId?: string,\n ): string {\n this.count += 1\n const id =\n fixedId ??\n `${this.prefix}${this.count}-${role.replace(/[^A-Za-z0-9]/g, '').slice(0, 16)}`\n const node: AiLayoutRawNode = { componentId }\n const kept = props\n ? Object.fromEntries(\n Object.entries(props).filter(([, value]) => value !== undefined),\n )\n : {}\n if (Object.keys(kept).length) node.props = kept\n if (sx && Object.keys(sx).length) node.sx = { ...sx }\n const list = (children ?? []).filter(\n (child): child is string => typeof child === 'string',\n )\n if (list.length) node.nodes = list\n this.nodes[id] = node\n return id\n }\n\n /**\n * Takes out every frame left holding nothing — a card whose words were all\n * filler, the cell that held it — from the leaves up, so no element meant\n * to hold content is stored empty (rule 16). `keep` names the ones that\n * stay whatever they hold.\n */\n prune(rootId: string, keep: ReadonlySet<string> = new Set()): void {\n const visit = (id: string): boolean => {\n const node = this.nodes[id]\n if (!node) return false\n if (node.nodes) {\n node.nodes = node.nodes.filter(visit)\n if (!node.nodes.length) delete node.nodes\n }\n if (\n id === rootId ||\n keep.has(id) ||\n node.nodes?.length ||\n !AI_LAYOUT_FRAMES.has(node.componentId)\n )\n return true\n delete this.nodes[id]\n return false\n }\n visit(rootId)\n }\n\n /** The tree rooted at `rootId`. */\n tree(rootId: string): AiLayoutRawTree {\n return { rootId, nodes: this.nodes }\n }\n}\n\n/** Spans out of twelve for relative widths: each at least three, summing to twelve. */\nexport function aiLayoutSpans(weights: readonly number[]): number[] {\n const total = weights.reduce((sum, weight) => sum + weight, 0)\n const spans = weights.map((weight) =>\n Math.max(3, Math.round((weight / total) * 12)),\n )\n let over = spans.reduce((sum, span) => sum + span, 0) - 12\n while (over !== 0) {\n // Take from (or give to) the widest column that can spare it.\n const order = spans\n .map((span, position) => ({ span, position }))\n .sort((a, b) => (over > 0 ? b.span - a.span : a.span - b.span))\n const pick = order.find((entry) => (over > 0 ? entry.span > 3 : true))\n if (!pick) break\n spans[pick.position] += over > 0 ? -1 : 1\n over += over > 0 ? -1 : 1\n }\n return spans\n}\n"],"names":["AI_LAYOUT_FRAMES","Set","AiLayoutTreeBuilder","add","componentId","props","sx","children","role","fixedId","count","id","prefix","replace","slice","node","kept","Object","fromEntries","entries","filter","value","undefined","keys","length","list","child","nodes","prune","rootId","keep","visit","has","tree","aiLayoutSpans","weights","total","reduce","sum","weight","spans","map","Math","max","round","over","span","order","position","sort","a","b","pick","find","entry"],"mappings":"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"}
|
|
@@ -59,6 +59,22 @@ export declare const AI_BUILD_CREATE_KINDS: readonly AiBuildPlanCreateKind[];
|
|
|
59
59
|
* `items`: an item naming one is a plan that put it in the wrong list.
|
|
60
60
|
*/
|
|
61
61
|
export declare const AI_BUILD_STRUCTURAL_OPS: readonly string[];
|
|
62
|
+
/**
|
|
63
|
+
* What a build hands an item it made before it (AGL-3728): its pages, by the
|
|
64
|
+
* title the plan gave each and the id it was written under, and its layouts,
|
|
65
|
+
* by name. An item's `new:<title>` destinations are read against it.
|
|
66
|
+
*/
|
|
67
|
+
export declare const AI_BUILD_MADE_INPUT = "buildMade";
|
|
68
|
+
export interface AiBuildMade {
|
|
69
|
+
pages: Array<{
|
|
70
|
+
title: string;
|
|
71
|
+
id: string;
|
|
72
|
+
}>;
|
|
73
|
+
layouts: Array<{
|
|
74
|
+
name: string;
|
|
75
|
+
id: string;
|
|
76
|
+
}>;
|
|
77
|
+
}
|
|
62
78
|
/** The job input a build's Assist proposal sets when the brief asked to publish. */
|
|
63
79
|
export declare const AI_BUILD_PUBLISH_INPUT = "publish";
|
|
64
80
|
/** A unit of a build: a creation, a page, or an item. */
|
|
@@ -75,6 +91,13 @@ export interface AiBuildUnit {
|
|
|
75
91
|
}
|
|
76
92
|
/** The name a `new:<name>` reference gives, lowercased. */
|
|
77
93
|
export declare function aiBuildRefName(ref: string): string;
|
|
94
|
+
/**
|
|
95
|
+
* The `new:<name>` references an item's arguments make (AGL-3728): an
|
|
96
|
+
* argument that is one, or the destination after `=>` in an entry such as a
|
|
97
|
+
* menu item's `About => new:About`. Read from the arguments, never from the
|
|
98
|
+
* model's `dependsOn`, so what an item links is always built before it.
|
|
99
|
+
*/
|
|
100
|
+
export declare function aiBuildItemArgRefs(args: Readonly<Record<string, unknown>> | null | undefined): string[];
|
|
78
101
|
/** Every reference a page makes: its layout, template, starting page and what its sections place. */
|
|
79
102
|
export declare function aiBuildScreenRefs(screen: AiBuildPlanScreen): string[];
|
|
80
103
|
/**
|
|
@@ -65,6 +65,11 @@ import { AI_CREDIT_RANGE_ZERO, AI_MEASURED_PASS_CREDITS, aiCreditRangeAdd, aiCre
|
|
|
65
65
|
'page',
|
|
66
66
|
...AI_BUILD_CREATE_KINDS
|
|
67
67
|
];
|
|
68
|
+
/**
|
|
69
|
+
* What a build hands an item it made before it (AGL-3728): its pages, by the
|
|
70
|
+
* title the plan gave each and the id it was written under, and its layouts,
|
|
71
|
+
* by name. An item's `new:<title>` destinations are read against it.
|
|
72
|
+
*/ export const AI_BUILD_MADE_INPUT = 'buildMade';
|
|
68
73
|
/** The job input a build's Assist proposal sets when the brief asked to publish. */ export const AI_BUILD_PUBLISH_INPUT = 'publish';
|
|
69
74
|
/** The operation a creation is built as. */ function creationOp(creation) {
|
|
70
75
|
return creation.kind;
|
|
@@ -81,6 +86,30 @@ import { AI_CREDIT_RANGE_ZERO, AI_MEASURED_PASS_CREDITS, aiCreditRangeAdd, aiCre
|
|
|
81
86
|
/** The name a `new:<name>` reference gives, lowercased. */ export function aiBuildRefName(ref) {
|
|
82
87
|
return ref.slice(AI_PLAN_NEW_REF_PREFIX.length).trim().toLowerCase();
|
|
83
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* The `new:<name>` references an item's arguments make (AGL-3728): an
|
|
91
|
+
* argument that is one, or the destination after `=>` in an entry such as a
|
|
92
|
+
* menu item's `About => new:About`. Read from the arguments, never from the
|
|
93
|
+
* model's `dependsOn`, so what an item links is always built before it.
|
|
94
|
+
*/ export function aiBuildItemArgRefs(args) {
|
|
95
|
+
const refs = [];
|
|
96
|
+
const take = (value)=>{
|
|
97
|
+
var _value_split_slice_;
|
|
98
|
+
if (typeof value !== 'string') return;
|
|
99
|
+
for (const part of [
|
|
100
|
+
value,
|
|
101
|
+
(_value_split_slice_ = value.split('=>').slice(-1)[0]) != null ? _value_split_slice_ : ''
|
|
102
|
+
]){
|
|
103
|
+
const ref = part.trim();
|
|
104
|
+
if (isAiPlanNewRef(ref) && !refs.includes(ref)) refs.push(ref);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
for (const value of Object.values(args != null ? args : {})){
|
|
108
|
+
if (Array.isArray(value)) value.forEach(take);
|
|
109
|
+
else take(value);
|
|
110
|
+
}
|
|
111
|
+
return refs;
|
|
112
|
+
}
|
|
84
113
|
/** Every reference a page makes: its layout, template, starting page and what its sections place. */ export function aiBuildScreenRefs(screen) {
|
|
85
114
|
return [
|
|
86
115
|
screen.layout,
|
|
@@ -97,11 +126,19 @@ import { AI_CREDIT_RANGE_ZERO, AI_MEASURED_PASS_CREDITS, aiCreditRangeAdd, aiCre
|
|
|
97
126
|
*/ export function aiBuildUnits(plan) {
|
|
98
127
|
var _plan_items;
|
|
99
128
|
const slots = namedSlots(plan);
|
|
100
|
-
|
|
129
|
+
// A page, by the title the plan gave it: what an item's arguments may link
|
|
130
|
+
// as `new:<title>` (a menu item, AGL-3728), built before the item.
|
|
131
|
+
const pageSlots = new Map();
|
|
132
|
+
plan.screens.forEach((screen, index)=>{
|
|
133
|
+
const title = screen.title.trim().toLowerCase();
|
|
134
|
+
if (title && !pageSlots.has(title)) pageSlots.set(title, `p${index}`);
|
|
135
|
+
});
|
|
136
|
+
const depsOf = (refs, self, pages = false)=>{
|
|
101
137
|
const deps = [];
|
|
102
138
|
for (const ref of refs){
|
|
139
|
+
var _slots_get;
|
|
103
140
|
if (!isAiPlanNewRef(ref)) continue;
|
|
104
|
-
const slot = slots.get(aiBuildRefName(ref));
|
|
141
|
+
const slot = (_slots_get = slots.get(aiBuildRefName(ref))) != null ? _slots_get : pages ? pageSlots.get(aiBuildRefName(ref)) : undefined;
|
|
105
142
|
if (slot && slot !== self && !deps.includes(slot)) deps.push(slot);
|
|
106
143
|
}
|
|
107
144
|
return deps;
|
|
@@ -129,7 +166,11 @@ import { AI_CREDIT_RANGE_ZERO, AI_MEASURED_PASS_CREDITS, aiCreditRangeAdd, aiCre
|
|
|
129
166
|
op: item.op,
|
|
130
167
|
label: item.name,
|
|
131
168
|
item,
|
|
132
|
-
|
|
169
|
+
// What it depends on, and what its arguments link as `new:` (AGL-3728).
|
|
170
|
+
deps: depsOf([
|
|
171
|
+
...item.dependsOn,
|
|
172
|
+
...aiBuildItemArgRefs(item.args)
|
|
173
|
+
], item.slot, true)
|
|
133
174
|
});
|
|
134
175
|
}
|
|
135
176
|
plan.screens.forEach((screen, index)=>{
|
|
@@ -477,9 +518,21 @@ const OPEN = [
|
|
|
477
518
|
continue;
|
|
478
519
|
}
|
|
479
520
|
if (unit.screen) {
|
|
480
|
-
var _capability_pageBlocks;
|
|
521
|
+
var _capability_pageBlocks, _capability_pageBlocks1;
|
|
481
522
|
const section = unit.screen.sections.find((one)=>one.uses.some((ref)=>isAiPlanNewRef(ref) && aiBuildRefName(ref) === dep.label.toLowerCase()));
|
|
482
|
-
|
|
523
|
+
// Another item this section places with the same block was built (one
|
|
524
|
+
// of several events, AGL-3743): the block stays, and only this item is
|
|
525
|
+
// missing from what it shows.
|
|
526
|
+
const blockStays = Boolean(section && (capability == null ? void 0 : (_capability_pageBlocks = capability.pageBlocks) == null ? void 0 : _capability_pageBlocks.length) && aiBuildSectionDeliveredBlocks(section, _extends({}, context, {
|
|
527
|
+
rows,
|
|
528
|
+
except: slot
|
|
529
|
+
})).has(aiBuildBlocksKey(capability.pageBlocks)));
|
|
530
|
+
if (blockStays) {
|
|
531
|
+
result.notes.push(`Built without the ${noun} “${dep.label}”: it could not be created.`);
|
|
532
|
+
result.briefLines.push(`Leave the ${noun} “${dep.label}” out of the “${section == null ? void 0 : section.name}” section: it could not be created.`);
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
const blocks = (capability == null ? void 0 : (_capability_pageBlocks1 = capability.pageBlocks) == null ? void 0 : _capability_pageBlocks1.length) ? ` the ${capability.pageBlocks.join(' or ')} block` : '';
|
|
483
536
|
result.notes.push(`Built without${blocks || ` the ${noun}`}: the ${noun} “${dep.label}” could not be created.`);
|
|
484
537
|
result.briefLines.push(`${section ? `Build the “${section.name}” section` : 'Build the page'} without the ${noun} “${dep.label}”${blocks ? ` and without${blocks}` : ''}: it could not be created.`);
|
|
485
538
|
} else {
|
|
@@ -507,22 +560,58 @@ const OPEN = [
|
|
|
507
560
|
]));
|
|
508
561
|
const lines = [];
|
|
509
562
|
for (const section of unit.screen.sections){
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
if (!(dep == null ? void 0 : dep.item)) continue;
|
|
563
|
+
// One line per block a section places, naming every built item it shows:
|
|
564
|
+
// a section listing four events places the Event List once (AGL-3743).
|
|
565
|
+
const placed = new Map();
|
|
566
|
+
for (const { dep, capability } of aiBuildSectionItems(section, context.units, context.ops)){
|
|
567
|
+
var _ref, _placed_get, _ref1;
|
|
516
568
|
const row = rows.get(dep.slot);
|
|
517
569
|
if (!row || !aiBuildItemDelivered(row)) continue;
|
|
518
|
-
const capability = (_context_ops = context.ops) == null ? void 0 : _context_ops.get(dep.op);
|
|
519
570
|
const blocks = (_ref = capability == null ? void 0 : capability.pageBlocks) != null ? _ref : [];
|
|
520
571
|
if (!blocks.length) continue;
|
|
521
|
-
|
|
572
|
+
const key = aiBuildBlocksKey(blocks);
|
|
573
|
+
const entry = (_placed_get = placed.get(key)) != null ? _placed_get : {
|
|
574
|
+
blocks,
|
|
575
|
+
noun: (_ref1 = capability == null ? void 0 : capability.noun) != null ? _ref1 : dep.op,
|
|
576
|
+
labels: []
|
|
577
|
+
};
|
|
578
|
+
entry.labels.push(`“${dep.label}”`);
|
|
579
|
+
placed.set(key, entry);
|
|
580
|
+
}
|
|
581
|
+
for (const { blocks, noun, labels } of placed.values()){
|
|
582
|
+
const named = labels.length === 1 ? `the ${noun} ${labels[0]}` : `the ${noun}s ${labels.slice(0, -1).join(', ')} and ${labels[labels.length - 1]}`;
|
|
583
|
+
lines.push(`In the “${section.name}” section, place the ${blocks.join(' or ')} block for ${named}.`);
|
|
522
584
|
}
|
|
523
585
|
}
|
|
524
586
|
return lines;
|
|
525
587
|
}
|
|
588
|
+
/** The items a page section places, each with its capability, in the order its uses name them. */ function aiBuildSectionItems(section, units, ops) {
|
|
589
|
+
const found = [];
|
|
590
|
+
for (const ref of section.uses){
|
|
591
|
+
if (!isAiPlanNewRef(ref)) continue;
|
|
592
|
+
const dep = units.find((one)=>one.item && one.item.name.toLowerCase() === aiBuildRefName(ref));
|
|
593
|
+
if (dep == null ? void 0 : dep.item) found.push({
|
|
594
|
+
dep,
|
|
595
|
+
capability: ops == null ? void 0 : ops.get(dep.op)
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
return found;
|
|
599
|
+
}
|
|
600
|
+
/** The blocks a section's built items place, each set as one key; `except` leaves one item out. */ function aiBuildSectionDeliveredBlocks(section, context) {
|
|
601
|
+
const keys = new Set();
|
|
602
|
+
for (const { dep, capability } of aiBuildSectionItems(section, context.units, context.ops)){
|
|
603
|
+
var _capability_pageBlocks;
|
|
604
|
+
if (dep.slot === context.except || !(capability == null ? void 0 : (_capability_pageBlocks = capability.pageBlocks) == null ? void 0 : _capability_pageBlocks.length)) continue;
|
|
605
|
+
const row = context.rows.get(dep.slot);
|
|
606
|
+
if (row && aiBuildItemDelivered(row)) keys.add(aiBuildBlocksKey(capability.pageBlocks));
|
|
607
|
+
}
|
|
608
|
+
return keys;
|
|
609
|
+
}
|
|
610
|
+
function aiBuildBlocksKey(blocks) {
|
|
611
|
+
return [
|
|
612
|
+
...blocks
|
|
613
|
+
].sort().join('|');
|
|
614
|
+
}
|
|
526
615
|
/** What a build came to: whether anything was delivered, and whether anything is left. */ export function aiBuildSettlement(ledger) {
|
|
527
616
|
return {
|
|
528
617
|
delivered: ledger.some(aiBuildItemDelivered),
|