@aglyn/plugins-ai 1.0.0-beta.243 → 1.0.0-beta.244

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.
@@ -173,6 +173,28 @@ function navEntriesOf(outputs) {
173
173
  }
174
174
  return changed ? map : null;
175
175
  }
176
+ /**
177
+ * A retry's publish merged into the start's first (AGL-3730): the pages it
178
+ * put live join the published list, a page it published leaves the drafts,
179
+ * and a page it left a draft is reported with its newest reason.
180
+ */ export function aiMergeSitePublish(previous, retried) {
181
+ var _retried_liveUrl;
182
+ const now = new Set([
183
+ ...retried.published,
184
+ ...retried.drafts
185
+ ].map((page)=>page.id));
186
+ return {
187
+ liveUrl: (_retried_liveUrl = retried.liveUrl) != null ? _retried_liveUrl : previous.liveUrl,
188
+ published: [
189
+ ...previous.published.filter((page)=>!now.has(page.id)),
190
+ ...retried.published
191
+ ],
192
+ drafts: [
193
+ ...previous.drafts.filter((page)=>!now.has(page.id)),
194
+ ...retried.drafts
195
+ ]
196
+ };
197
+ }
176
198
  /**
177
199
  * Publishes the pages a guided site start built. Never throws for a page; a
178
200
  * Firestore failure of the publish write itself reports every page as a
@@ -199,7 +221,9 @@ function navEntriesOf(outputs) {
199
221
  };
200
222
  if (!host.exists || !pages.length) return result;
201
223
  const before = _extends({}, (_host_get = host.get('screens')) != null ? _host_get : {});
202
- const placeholder = host.get('defaultHomeScreenId') || null;
224
+ const starter = host.get('defaultHomeScreenId') || null;
225
+ // A retry never takes `/` from the starter (AGL-3730): it holds it like any page.
226
+ const placeholder = input.retry ? null : starter;
203
227
  const routing = _extends({}, before);
204
228
  const entries = {};
205
229
  const accepted = [];
@@ -382,8 +406,9 @@ function navEntriesOf(outputs) {
382
406
  }
383
407
  }));
384
408
  if (dropped.complete) await outboxRef.delete().catch(()=>undefined);
385
- // A site that came alive with nobody's browser on the publish (AGL-1589).
386
- await sendSitePublished({
409
+ // A site that came alive with nobody's browser on the publish (AGL-1589),
410
+ // announced once: a retry's pages join a site already announced (AGL-3730).
411
+ if (!input.retry) await sendSitePublished({
387
412
  hostId,
388
413
  firstPublish: isFirstPublishedRoute(before, placeholder)
389
414
  });
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-site-publish.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 { isFirstPublishedRoute } from '@aglyn/aglyn/app-utils/analytics-events'\nimport { createResourceUid } from '@aglyn/aglyn/app-utils/create-resource-uid'\nimport { hostPublicOrigin } from '@aglyn/aglyn/app-utils/host-naming'\nimport {\n blockingRouteOwner,\n normalizeScreenSlug,\n SCREEN_ROOT_PATH,\n screenRoutePathToUrl,\n} from '@aglyn/aglyn/app-utils/screen-route'\nimport { decodeStoredNodes, encodeStoredNodes } from '@aglyn/aglyn/app-utils/stored-nodes'\nimport { dropPluginSiteCache } from '@aglyn/aglyn/plugin-manager/plugin-site-cache'\nimport type { NodesMap } from '@aglyn/aglyn/types/nodes'\nimport { sendGa4SitePublished } from '@aglyn/tenant-data-admin/server/ga4-measurement-protocol'\nimport { FieldValue, type Firestore } from 'firebase-admin/firestore'\nimport type { AiJob, AiJobOutput, AiJobSitePublish } from '../model/ai-jobs.types'\nimport { AI_SITE_BLOG_SLUGS } from '../model/ai-site-job'\nimport { aiLayoutWithStoreLinks, type AiStoreLayoutLinks } from '../model/ai-layout-store-links'\nimport { AI_STORE_LINKS_SOURCE_FIELD as STORE_LINKS_SOURCE_FIELD } from './ai-job-store-links-layout'\n\n/**\n * A store's links a build drafted into the site's own layout (AGL-3676): the\n * draft version, the links in it, and the site's page addresses they were\n * checked against. Its publish makes that version the live one, or — where\n * the layout changed since the draft was made — adds the same links to the\n * version that is live now.\n */\nexport interface AiSitePublishStoreLinks {\n layoutId: string\n versionId: string\n links: AiStoreLayoutLinks\n screenPaths?: Readonly<Record<string, string>>\n}\n\n/**\n * A guided site start publishes what it built (AGL-3596).\n *\n * A person who asked for \"a website\" in the guided start and watched it build\n * was then handed drafts: a site whose address still answered with the\n * starter, or with nothing. A `site` job started from the guided start (its\n * plan confirmed on the person's behalf, `inputs.autoConfirm`) therefore ends\n * by putting every page it built on the site, through the same writes a\n * person's Publish makes:\n *\n * - each page's address in the host's routing map, `publishedAt` on the page,\n * in ONE batch with the publish outbox entry that drops the live site's\n * cached pages (AGL-2575), the drop itself attempted at once;\n * - the home page takes `/` from the starter home page the site was created\n * with (AGL-3408), and an untouched starter goes to the trash rather than\n * staying beside it as a second \"Home\";\n * - the navigation entries the pages proposed are added to the header of\n * the layout they render inside, as a new live version of that layout;\n * - `site_published` once, server-side, as the scheduled publish sends it.\n *\n * A page that cannot be published — deleted meanwhile, no address, an address\n * another page holds — stays a draft and is reported with the plain sentence\n * for why. It never fails the job: the site is still built, and the person\n * fixes that one page in Pages. Every other job kind keeps producing drafts.\n *\n * The allowance is respected by construction: a draft is counted against the\n * plan's pages when it is written (`aiDraftBandRefusal`), so publishing\n * drafts adds none, and retiring the starter frees the one it held.\n */\n\nexport interface AiSitePublishDeps {\n sendSitePublished?: typeof sendGa4SitePublished\n dropCache?: typeof dropPluginSiteCache\n}\n\n/**\n * Whether a build puts its pages live (AGL-3616): only where its request\n * asked to (`publish`, set by Assist's proposal) AND the member ticked the\n * plan card's box when confirming (`publishConfirmed`, set by the resume\n * door). Every other build leaves drafts.\n */\nexport function aiJobPublishesBuild(job: Pick<AiJob, 'kind' | 'inputs'>): boolean {\n return job.kind === 'build' && job.inputs?.['publish'] === true && job.inputs?.['publishConfirmed'] === true\n}\n\n/** Whether a finished job of this shape publishes what it built. */\nexport function aiJobPublishesSite(job: Pick<AiJob, 'kind' | 'inputs'>): boolean {\n return job.kind === 'site' && job.inputs?.['autoConfirm'] === true\n}\n\n/**\n * Where a site answers: its custom domain, else its subdomain under the\n * deployment's tenant apex — never a hard-coded platform host, so a\n * self-hosted deployment links to its own sites.\n */\nexport function aiSiteLiveUrl(host: { cname?: unknown; subdomain?: unknown }): string | null {\n const text = (value: unknown) => (typeof value === 'string' ? value.trim() : '')\n const origin = hostPublicOrigin({ cname: text(host.cname) || null, subdomain: text(host.subdomain) || null })\n return origin ? `${origin}/` : null\n}\n\n/** The sentences a page that stayed a draft is reported with. */\nexport const AI_SITE_PUBLISH_DELETED = 'It was deleted before it could be published.'\nexport const AI_SITE_PUBLISH_NO_ADDRESS = 'It has no address yet. Give it one in Pages and publish it.'\nexport const aiSitePublishTakenCopy = (path: string): string =>\n `Another page is already published at ${screenRoutePathToUrl(path)}. Give this one another address in Pages and publish it.`\nexport const AI_SITE_PUBLISH_FAILED = 'It could not be published just now. Publish it from Pages.'\n\n/** A navigation entry a page proposed, as the page step reports it. */\ninterface NavEntry {\n screenId: string\n label: string\n}\n\nfunction navEntriesOf(outputs: readonly AiJobOutput[]): NavEntry[] {\n const entries: NavEntry[] = []\n for (const output of outputs) {\n if (output.resource !== 'screen') continue\n const navigation = output.proposal?.['navigation'] as { label?: unknown } | undefined\n const label = typeof navigation?.label === 'string' ? navigation.label.trim() : ''\n if (label && !entries.some((entry) => entry.screenId === output.id)) {\n entries.push({ screenId: output.id, label })\n }\n }\n return entries\n}\n\ntype MutableNode = {\n $id: string\n componentId?: string\n parentId?: string\n props?: Record<string, unknown>\n nodes?: string[]\n [key: string]: unknown\n}\n\n/**\n * The blog paths a start's layout links and must not publish (AGL-3660): its\n * header, phone menu and footer link the blog by path while the posts part is\n * still owed, so a start whose posts part then failed or was skipped drops\n * every address the blog could have taken. None while the blog was written.\n */\nexport function aiSiteUnwrittenBlogHrefs(blogUnwritten: boolean | undefined): string[] {\n return blogUnwritten ? AI_SITE_BLOG_SLUGS.map((slug) => `/${slug}`) : []\n}\n\n/**\n * The layout's nodes with its links to the retired starter home pointed at\n * the new home, and a link for each proposed entry the header does not\n * already carry, placed in the header's toolbar after its existing links.\n * `null` when nothing changes, or when the layout has no toolbar to hold an\n * entry and no link to repoint.\n */\nexport function aiLayoutWithNavigation(\n nodes: NodesMap,\n input: {\n entries: readonly NavEntry[]\n retiredHomeId?: string | null\n homeId?: string | null\n /** Paths whose links come out: a blog the start linked and never wrote (AGL-3660). */\n droppedHrefs?: readonly string[]\n },\n): NodesMap | null {\n const map: Record<string, MutableNode> = {}\n for (const [id, node] of Object.entries(nodes as unknown as Record<string, MutableNode>)) {\n map[id] = { ...node, ...(node.props ? { props: { ...node.props } } : {}), ...(node.nodes ? { nodes: [...node.nodes] } : {}) }\n }\n let changed = false\n // The header, the phone menu and the footer link a blog by its path before\n // its posts are written (AGL-3660); a start whose posts were not written\n // publishes none of those links to a page that does not exist.\n const dropped = new Set(input.droppedHrefs ?? [])\n if (dropped.size) {\n const gone = new Set(\n Object.entries(map)\n .filter(([, node]) => node.componentId === 'muiScreenLink' && dropped.has(String(node.props?.['href'] ?? '')))\n .map(([id]) => id),\n )\n if (gone.size) {\n for (const id of gone) delete map[id]\n for (const node of Object.values(map)) {\n if (node.nodes?.some((child) => gone.has(child))) node.nodes = node.nodes.filter((child) => !gone.has(child))\n }\n changed = true\n }\n }\n const links = Object.values(map).filter((node) => node.componentId === 'muiScreenLink')\n if (input.retiredHomeId && input.homeId) {\n for (const link of links) {\n if (link.props?.['screenId'] === input.retiredHomeId) {\n link.props = { ...link.props, screenId: input.homeId }\n changed = true\n }\n }\n }\n const linked = new Set(links.map((link) => link.props?.['screenId']).filter(Boolean))\n const toolbar = Object.values(map).find((node) => node.componentId === 'muiToolbar')\n const missing = input.entries.filter((entry) => !linked.has(entry.screenId))\n if (toolbar && missing.length) {\n const children = toolbar.nodes ?? []\n // After the last link already in the toolbar (the brand), before the\n // search, the mode switch and the call to action.\n const lastLink = children.reduce(\n (at, id, index) => (map[id]?.componentId === 'muiScreenLink' ? index : at),\n -1,\n )\n const added = missing.map((entry) => {\n const id = `ai_nav_${createResourceUid()}`\n map[id] = {\n $id: id,\n componentId: 'muiScreenLink',\n pluginId: 'mui',\n parentId: toolbar.$id,\n props: { children: entry.label, screenId: entry.screenId, renderAs: 'link', color: 'inherit' },\n sx: { textDecoration: 'none', whiteSpace: 'nowrap' },\n nodes: [],\n }\n return id\n })\n toolbar.nodes = [...children.slice(0, lastLink + 1), ...added, ...children.slice(lastLink + 1)]\n changed = true\n }\n return changed ? (map as unknown as NodesMap) : null\n}\n\n/**\n * Publishes the pages a guided site start built. Never throws for a page; a\n * Firestore failure of the publish write itself reports every page as a\n * draft, for the same reason, and is logged.\n */\nexport async function aiPublishGuidedSite(\n firestore: Firestore,\n input: {\n job: Pick<AiJob, '$id' | 'orgId' | 'hostId'>\n outputs: readonly AiJobOutput[]\n now: Date\n /** The start owed its blog's posts and wrote none (AGL-3660): its links to the blog come out. */\n blogUnwritten?: boolean\n /** Paths the layout links that the start did not write, such as a store page (AGL-3676): their links come out. */\n unwrittenHrefs?: readonly string[]\n /** A store's links a build drafted into the site's own layout (AGL-3676), which go live with its pages. */\n storeLinks?: AiSitePublishStoreLinks | null\n },\n deps: AiSitePublishDeps = {},\n): Promise<AiJobSitePublish> {\n const sendSitePublished = deps.sendSitePublished ?? sendGa4SitePublished\n const dropCache = deps.dropCache ?? dropPluginSiteCache\n const { job, now } = input\n const hostId = job.hostId ?? ''\n const pages = input.outputs.filter(\n (output, index, all) =>\n output.resource === 'screen' &&\n (output.hostId ?? hostId) === hostId &&\n all.findIndex((other) => other.resource === 'screen' && other.id === output.id) === index,\n )\n const hostRef = firestore.collection('hosts').doc(hostId)\n const host = await hostRef.get()\n const result: AiJobSitePublish = {\n liveUrl: aiSiteLiveUrl({ cname: host.get('cname'), subdomain: host.get('subdomain') }),\n published: [],\n drafts: [],\n }\n if (!host.exists || !pages.length) return result\n\n const before = { ...((host.get('screens') ?? {}) as Record<string, string>) }\n const placeholder = (host.get('defaultHomeScreenId') as string | undefined) || null\n const routing = { ...before }\n const entries: Record<string, string> = {}\n const accepted: Array<{ id: string; label: string; path: string; slug: string }> = []\n for (const page of pages) {\n const snapshot = await hostRef.collection('screens').doc(page.id).get()\n const label = String(snapshot.get('displayName') ?? page.label ?? '') || page.label\n if (!snapshot.exists || snapshot.get('deletedAt') != null) {\n result.drafts.push({ id: page.id, label, reason: AI_SITE_PUBLISH_DELETED })\n continue\n }\n const slug = normalizeScreenSlug(String(snapshot.get('slug') ?? ''))\n if (!slug) {\n result.drafts.push({ id: page.id, label, reason: AI_SITE_PUBLISH_NO_ADDRESS })\n continue\n }\n const owner = blockingRouteOwner(routing, slug, placeholder)\n if (owner && owner !== page.id) {\n result.drafts.push({ id: page.id, label, reason: aiSitePublishTakenCopy(slug) })\n continue\n }\n routing[page.id] = slug\n entries[page.id] = slug\n accepted.push({ id: page.id, label, path: slug, slug })\n }\n if (!accepted.length) return result\n\n // The first real home page replaces the placeholder in the same write.\n const home = accepted.find((page) => page.path === SCREEN_ROOT_PATH) ?? null\n const releases =\n home && placeholder && placeholder !== home.id && before[placeholder] === SCREEN_ROOT_PATH\n ? placeholder\n : null\n // An untouched starter home (still its one first version) is retired to the\n // trash; one its owner edited stays in Pages as their draft.\n const retire = releases\n ? (await hostRef.collection('screens').doc(releases).collection('versions').limit(2).get()).size <= 1\n : false\n\n // The header of the layout the pages render inside learns their entries.\n const layoutWrite = await aiNavigationLayoutWrite(firestore, {\n hostId,\n pageIds: accepted.map((page) => page.id),\n entries: navEntriesOf(pages).filter((entry) => entries[entry.screenId]),\n droppedHrefs: [...aiSiteUnwrittenBlogHrefs(input.blogUnwritten), ...(input.unwrittenHrefs ?? [])],\n retiredHomeId: releases,\n homeId: home?.id ?? null,\n jobId: job.$id,\n now,\n storeLinks: input.storeLinks ?? null,\n }).catch((error: unknown) => {\n console.error('ai site publish: navigation not applied', { hostId, jobId: job.$id, error })\n return null\n })\n // A store's links drafted into a layout the pages' navigation did not touch go live on their own.\n const storeLinks = input.storeLinks ?? null\n const storeLinksWrite =\n storeLinks && layoutWrite?.layoutRef.id !== storeLinks.layoutId\n ? await aiLayoutVersionWrite(firestore, {\n hostId,\n layoutId: storeLinks.layoutId,\n transform: () => null,\n storeLinks,\n jobId: job.$id,\n now,\n }).catch((error: unknown) => {\n console.error('ai site publish: store links not applied', { hostId, jobId: job.$id, error })\n return null\n })\n : null\n const layoutWrites = [layoutWrite, storeLinksWrite].filter((write): write is LayoutWrite => Boolean(write))\n\n const paths = [\n ...new Set(\n [\n ...Object.values(entries),\n ...(releases ? [SCREEN_ROOT_PATH] : []),\n ].map((path) => screenRoutePathToUrl(path)),\n ),\n ]\n const batch = firestore.batch()\n const hostUpdates: Record<string, unknown> = { updatedAt: now }\n for (const [id, path] of Object.entries(entries)) hostUpdates[`screens.${id}`] = path\n if (releases) {\n hostUpdates[`screens.${releases}`] = FieldValue.delete()\n hostUpdates['defaultHomeScreenId'] = FieldValue.delete()\n batch.set(\n hostRef.collection('screens').doc(releases),\n { publishedAt: FieldValue.delete(), ...(retire ? { deletedAt: now } : {}) },\n { merge: true },\n )\n } else if (placeholder && entries[placeholder]) {\n hostUpdates['defaultHomeScreenId'] = FieldValue.delete()\n }\n batch.update(hostRef, hostUpdates)\n for (const page of accepted) {\n batch.set(hostRef.collection('screens').doc(page.id), { slug: page.slug, publishedAt: now }, { merge: true })\n }\n for (const write of layoutWrites) {\n if (write.version) batch.create(write.versionRef, write.version)\n batch.set(write.layoutRef, { versionId: write.versionRef.id, updatedAt: now }, { merge: true })\n }\n // The announce, written down in the publish's own batch (AGL-2575): a\n // layout change reaches every page, so it asks for the whole site.\n const outboxRef = firestore.collection('publishOutbox').doc(createResourceUid())\n batch.set(outboxRef, {\n hostId,\n paths: paths.length ? paths : [SCREEN_ROOT_PATH],\n createdAt: FieldValue.serverTimestamp(),\n attempts: 0,\n ...(layoutWrites.length ? { entireHost: true } : {}),\n })\n try {\n await batch.commit()\n } catch (error) {\n console.error('ai site publish failed', { hostId, jobId: job.$id, error })\n for (const page of accepted) result.drafts.push({ id: page.id, label: page.label, reason: AI_SITE_PUBLISH_FAILED })\n return result\n }\n result.published = accepted.map(({ id, label, path }) => ({ id, label, path: screenRoutePathToUrl(path) }))\n\n // The live site's cached pages go now; the outbox entry stays for the\n // drain unless the drop certainly happened.\n const dropped = await dropCache({\n hostIds: [hostId],\n reason: 'guided AI site start published',\n ...(layoutWrites.length ? {} : { paths: { [hostId]: paths } }),\n })\n if (dropped.complete) await outboxRef.delete().catch(() => undefined)\n // A site that came alive with nobody's browser on the publish (AGL-1589).\n await sendSitePublished({ hostId, firstPublish: isFirstPublishedRoute(before, placeholder) })\n return result\n}\n\ninterface LayoutWrite {\n layoutRef: FirebaseFirestore.DocumentReference\n versionRef: FirebaseFirestore.DocumentReference\n /** The new version to create; `null` where the live version becomes one already written (a store's links draft). */\n version: Record<string, unknown> | null\n}\n\n/**\n * The new live version of the layout the published pages render inside, with\n * their navigation entries and the home link moved off a retired starter;\n * `null` when there is nothing to change or no one layout to change.\n */\nasync function aiNavigationLayoutWrite(\n firestore: Firestore,\n input: {\n hostId: string\n pageIds: readonly string[]\n entries: readonly NavEntry[]\n droppedHrefs: readonly string[]\n retiredHomeId: string | null\n homeId: string | null\n jobId: string\n now: Date\n storeLinks?: AiSitePublishStoreLinks | null\n },\n): Promise<LayoutWrite | null> {\n if (!input.entries.length && !input.retiredHomeId && !input.droppedHrefs.length && !input.storeLinks) return null\n const hostRef = firestore.collection('hosts').doc(input.hostId)\n const layoutIds = new Set<string>()\n for (const id of input.pageIds) {\n const screen = await hostRef.collection('screens').doc(id).get()\n const versionId = screen.get('versionId')\n const version =\n typeof versionId === 'string' && versionId\n ? await hostRef.collection('screens').doc(id).collection('versions').doc(versionId).get()\n : null\n const layoutId = version?.get('layoutId') ?? screen.get('layoutId')\n if (typeof layoutId === 'string' && layoutId) layoutIds.add(layoutId)\n }\n if (layoutIds.size !== 1) return null\n const [layoutId] = [...layoutIds]\n return aiLayoutVersionWrite(firestore, {\n hostId: input.hostId,\n layoutId,\n transform: (nodes) => aiLayoutWithNavigation(nodes, input),\n storeLinks: input.storeLinks ?? null,\n jobId: input.jobId,\n now: input.now,\n })\n}\n\n/**\n * The new live version of one layout: `transform` applied to the version\n * live now. A store's links drafted into this layout (AGL-3676) start it from\n * that draft while the draft was made from the live version, and are added\n * to the live version otherwise; a draft with nothing more to add becomes\n * the live version as it is. `null` when nothing changes.\n */\nasync function aiLayoutVersionWrite(\n firestore: Firestore,\n input: {\n hostId: string\n layoutId: string\n transform: (nodes: NodesMap) => NodesMap | null\n storeLinks: AiSitePublishStoreLinks | null\n jobId: string\n now: Date\n },\n): Promise<LayoutWrite | null> {\n const { layoutId } = input\n const layoutRef = firestore.collection('hosts').doc(input.hostId).collection('layouts').doc(layoutId)\n const layout = await layoutRef.get()\n const currentVersionId = layout.get('versionId')\n if (!layout.exists || layout.get('deletedAt') != null || typeof currentVersionId !== 'string') return null\n const current = await layoutRef.collection('versions').doc(currentVersionId).get()\n const links = input.storeLinks?.layoutId === layoutId ? input.storeLinks : null\n const draftRef = links ? layoutRef.collection('versions').doc(links.versionId) : null\n const draft = draftRef ? await draftRef.get() : null\n const fromDraft = Boolean(draft?.exists && draft.get(STORE_LINKS_SOURCE_FIELD) === currentVersionId)\n const base = fromDraft && draft ? draft : current\n const nodes = base.exists ? decodeStoredNodes<NodesMap>(base.get('nodes')) : null\n if (!nodes) return null\n let next = input.transform(nodes)\n if (links && !fromDraft) {\n const added = aiLayoutWithStoreLinks((next ?? nodes) as never, links.links, links.screenPaths)\n next = (added.nodes as NodesMap | null) ?? next\n }\n if (!next) return fromDraft && draftRef ? { layoutRef, versionRef: draftRef, version: null } : null\n const packed = encodeStoredNodes(next)\n if (!packed) return null\n const versionRef = layoutRef.collection('versions').doc(createResourceUid())\n return {\n layoutRef,\n versionRef,\n version: {\n layoutId,\n hostId: input.hostId,\n displayName: 'Navigation for the new pages',\n nodes: Buffer.from(packed),\n createdAt: input.now,\n updatedAt: input.now,\n aiJobId: input.jobId,\n },\n }\n}\n"],"names":["isFirstPublishedRoute","createResourceUid","hostPublicOrigin","blockingRouteOwner","normalizeScreenSlug","SCREEN_ROOT_PATH","screenRoutePathToUrl","decodeStoredNodes","encodeStoredNodes","dropPluginSiteCache","sendGa4SitePublished","FieldValue","AI_SITE_BLOG_SLUGS","aiLayoutWithStoreLinks","AI_STORE_LINKS_SOURCE_FIELD","STORE_LINKS_SOURCE_FIELD","aiJobPublishesBuild","job","kind","inputs","aiJobPublishesSite","aiSiteLiveUrl","host","text","value","trim","origin","cname","subdomain","AI_SITE_PUBLISH_DELETED","AI_SITE_PUBLISH_NO_ADDRESS","aiSitePublishTakenCopy","path","AI_SITE_PUBLISH_FAILED","navEntriesOf","outputs","entries","output","resource","navigation","proposal","label","some","entry","screenId","id","push","aiSiteUnwrittenBlogHrefs","blogUnwritten","map","slug","aiLayoutWithNavigation","nodes","input","node","Object","props","changed","dropped","Set","droppedHrefs","size","gone","filter","componentId","has","String","values","child","links","retiredHomeId","homeId","link","linked","Boolean","toolbar","find","missing","length","children","lastLink","reduce","at","index","added","$id","pluginId","parentId","renderAs","color","sx","textDecoration","whiteSpace","slice","aiPublishGuidedSite","firestore","deps","accepted","sendSitePublished","dropCache","now","hostId","pages","all","findIndex","other","hostRef","collection","doc","get","result","liveUrl","published","drafts","exists","before","placeholder","routing","page","snapshot","reason","owner","home","releases","retire","limit","layoutWrite","aiNavigationLayoutWrite","pageIds","unwrittenHrefs","jobId","storeLinks","catch","error","console","storeLinksWrite","layoutRef","layoutId","aiLayoutVersionWrite","transform","layoutWrites","write","paths","batch","hostUpdates","updatedAt","delete","set","publishedAt","deletedAt","merge","update","version","create","versionRef","versionId","outboxRef","createdAt","serverTimestamp","attempts","entireHost","commit","hostIds","complete","undefined","firstPublish","layoutIds","screen","add","layout","currentVersionId","current","draftRef","draft","fromDraft","base","next","screenPaths","packed","displayName","Buffer","from","aiJobId"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-site-publish.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 { isFirstPublishedRoute } from '@aglyn/aglyn/app-utils/analytics-events'\nimport { createResourceUid } from '@aglyn/aglyn/app-utils/create-resource-uid'\nimport { hostPublicOrigin } from '@aglyn/aglyn/app-utils/host-naming'\nimport {\n blockingRouteOwner,\n normalizeScreenSlug,\n SCREEN_ROOT_PATH,\n screenRoutePathToUrl,\n} from '@aglyn/aglyn/app-utils/screen-route'\nimport { decodeStoredNodes, encodeStoredNodes } from '@aglyn/aglyn/app-utils/stored-nodes'\nimport { dropPluginSiteCache } from '@aglyn/aglyn/plugin-manager/plugin-site-cache'\nimport type { NodesMap } from '@aglyn/aglyn/types/nodes'\nimport { sendGa4SitePublished } from '@aglyn/tenant-data-admin/server/ga4-measurement-protocol'\nimport { FieldValue, type Firestore } from 'firebase-admin/firestore'\nimport type { AiJob, AiJobOutput, AiJobSitePublish } from '../model/ai-jobs.types'\nimport { AI_SITE_BLOG_SLUGS } from '../model/ai-site-job'\nimport { aiLayoutWithStoreLinks, type AiStoreLayoutLinks } from '../model/ai-layout-store-links'\nimport { AI_STORE_LINKS_SOURCE_FIELD as STORE_LINKS_SOURCE_FIELD } from './ai-job-store-links-layout'\n\n/**\n * A store's links a build drafted into the site's own layout (AGL-3676): the\n * draft version, the links in it, and the site's page addresses they were\n * checked against. Its publish makes that version the live one, or — where\n * the layout changed since the draft was made — adds the same links to the\n * version that is live now.\n */\nexport interface AiSitePublishStoreLinks {\n layoutId: string\n versionId: string\n links: AiStoreLayoutLinks\n screenPaths?: Readonly<Record<string, string>>\n}\n\n/**\n * A guided site start publishes what it built (AGL-3596).\n *\n * A person who asked for \"a website\" in the guided start and watched it build\n * was then handed drafts: a site whose address still answered with the\n * starter, or with nothing. A `site` job started from the guided start (its\n * plan confirmed on the person's behalf, `inputs.autoConfirm`) therefore ends\n * by putting every page it built on the site, through the same writes a\n * person's Publish makes:\n *\n * - each page's address in the host's routing map, `publishedAt` on the page,\n * in ONE batch with the publish outbox entry that drops the live site's\n * cached pages (AGL-2575), the drop itself attempted at once;\n * - the home page takes `/` from the starter home page the site was created\n * with (AGL-3408), and an untouched starter goes to the trash rather than\n * staying beside it as a second \"Home\";\n * - the navigation entries the pages proposed are added to the header of\n * the layout they render inside, as a new live version of that layout;\n * - `site_published` once, server-side, as the scheduled publish sends it.\n *\n * A page that cannot be published — deleted meanwhile, no address, an address\n * another page holds — stays a draft and is reported with the plain sentence\n * for why. It never fails the job: the site is still built, and the person\n * fixes that one page in Pages. Every other job kind keeps producing drafts.\n *\n * The allowance is respected by construction: a draft is counted against the\n * plan's pages when it is written (`aiDraftBandRefusal`), so publishing\n * drafts adds none, and retiring the starter frees the one it held.\n */\n\nexport interface AiSitePublishDeps {\n sendSitePublished?: typeof sendGa4SitePublished\n dropCache?: typeof dropPluginSiteCache\n}\n\n/**\n * Whether a build puts its pages live (AGL-3616): only where its request\n * asked to (`publish`, set by Assist's proposal) AND the member ticked the\n * plan card's box when confirming (`publishConfirmed`, set by the resume\n * door). Every other build leaves drafts.\n */\nexport function aiJobPublishesBuild(job: Pick<AiJob, 'kind' | 'inputs'>): boolean {\n return job.kind === 'build' && job.inputs?.['publish'] === true && job.inputs?.['publishConfirmed'] === true\n}\n\n/** Whether a finished job of this shape publishes what it built. */\nexport function aiJobPublishesSite(job: Pick<AiJob, 'kind' | 'inputs'>): boolean {\n return job.kind === 'site' && job.inputs?.['autoConfirm'] === true\n}\n\n/**\n * Where a site answers: its custom domain, else its subdomain under the\n * deployment's tenant apex — never a hard-coded platform host, so a\n * self-hosted deployment links to its own sites.\n */\nexport function aiSiteLiveUrl(host: { cname?: unknown; subdomain?: unknown }): string | null {\n const text = (value: unknown) => (typeof value === 'string' ? value.trim() : '')\n const origin = hostPublicOrigin({ cname: text(host.cname) || null, subdomain: text(host.subdomain) || null })\n return origin ? `${origin}/` : null\n}\n\n/** The sentences a page that stayed a draft is reported with. */\nexport const AI_SITE_PUBLISH_DELETED = 'It was deleted before it could be published.'\nexport const AI_SITE_PUBLISH_NO_ADDRESS = 'It has no address yet. Give it one in Pages and publish it.'\nexport const aiSitePublishTakenCopy = (path: string): string =>\n `Another page is already published at ${screenRoutePathToUrl(path)}. Give this one another address in Pages and publish it.`\nexport const AI_SITE_PUBLISH_FAILED = 'It could not be published just now. Publish it from Pages.'\n\n/** A navigation entry a page proposed, as the page step reports it. */\ninterface NavEntry {\n screenId: string\n label: string\n}\n\nfunction navEntriesOf(outputs: readonly AiJobOutput[]): NavEntry[] {\n const entries: NavEntry[] = []\n for (const output of outputs) {\n if (output.resource !== 'screen') continue\n const navigation = output.proposal?.['navigation'] as { label?: unknown } | undefined\n const label = typeof navigation?.label === 'string' ? navigation.label.trim() : ''\n if (label && !entries.some((entry) => entry.screenId === output.id)) {\n entries.push({ screenId: output.id, label })\n }\n }\n return entries\n}\n\ntype MutableNode = {\n $id: string\n componentId?: string\n parentId?: string\n props?: Record<string, unknown>\n nodes?: string[]\n [key: string]: unknown\n}\n\n/**\n * The blog paths a start's layout links and must not publish (AGL-3660): its\n * header, phone menu and footer link the blog by path while the posts part is\n * still owed, so a start whose posts part then failed or was skipped drops\n * every address the blog could have taken. None while the blog was written.\n */\nexport function aiSiteUnwrittenBlogHrefs(blogUnwritten: boolean | undefined): string[] {\n return blogUnwritten ? AI_SITE_BLOG_SLUGS.map((slug) => `/${slug}`) : []\n}\n\n/**\n * The layout's nodes with its links to the retired starter home pointed at\n * the new home, and a link for each proposed entry the header does not\n * already carry, placed in the header's toolbar after its existing links.\n * `null` when nothing changes, or when the layout has no toolbar to hold an\n * entry and no link to repoint.\n */\nexport function aiLayoutWithNavigation(\n nodes: NodesMap,\n input: {\n entries: readonly NavEntry[]\n retiredHomeId?: string | null\n homeId?: string | null\n /** Paths whose links come out: a blog the start linked and never wrote (AGL-3660). */\n droppedHrefs?: readonly string[]\n },\n): NodesMap | null {\n const map: Record<string, MutableNode> = {}\n for (const [id, node] of Object.entries(nodes as unknown as Record<string, MutableNode>)) {\n map[id] = { ...node, ...(node.props ? { props: { ...node.props } } : {}), ...(node.nodes ? { nodes: [...node.nodes] } : {}) }\n }\n let changed = false\n // The header, the phone menu and the footer link a blog by its path before\n // its posts are written (AGL-3660); a start whose posts were not written\n // publishes none of those links to a page that does not exist.\n const dropped = new Set(input.droppedHrefs ?? [])\n if (dropped.size) {\n const gone = new Set(\n Object.entries(map)\n .filter(([, node]) => node.componentId === 'muiScreenLink' && dropped.has(String(node.props?.['href'] ?? '')))\n .map(([id]) => id),\n )\n if (gone.size) {\n for (const id of gone) delete map[id]\n for (const node of Object.values(map)) {\n if (node.nodes?.some((child) => gone.has(child))) node.nodes = node.nodes.filter((child) => !gone.has(child))\n }\n changed = true\n }\n }\n const links = Object.values(map).filter((node) => node.componentId === 'muiScreenLink')\n if (input.retiredHomeId && input.homeId) {\n for (const link of links) {\n if (link.props?.['screenId'] === input.retiredHomeId) {\n link.props = { ...link.props, screenId: input.homeId }\n changed = true\n }\n }\n }\n const linked = new Set(links.map((link) => link.props?.['screenId']).filter(Boolean))\n const toolbar = Object.values(map).find((node) => node.componentId === 'muiToolbar')\n const missing = input.entries.filter((entry) => !linked.has(entry.screenId))\n if (toolbar && missing.length) {\n const children = toolbar.nodes ?? []\n // After the last link already in the toolbar (the brand), before the\n // search, the mode switch and the call to action.\n const lastLink = children.reduce(\n (at, id, index) => (map[id]?.componentId === 'muiScreenLink' ? index : at),\n -1,\n )\n const added = missing.map((entry) => {\n const id = `ai_nav_${createResourceUid()}`\n map[id] = {\n $id: id,\n componentId: 'muiScreenLink',\n pluginId: 'mui',\n parentId: toolbar.$id,\n props: { children: entry.label, screenId: entry.screenId, renderAs: 'link', color: 'inherit' },\n sx: { textDecoration: 'none', whiteSpace: 'nowrap' },\n nodes: [],\n }\n return id\n })\n toolbar.nodes = [...children.slice(0, lastLink + 1), ...added, ...children.slice(lastLink + 1)]\n changed = true\n }\n return changed ? (map as unknown as NodesMap) : null\n}\n\n/**\n * A retry's publish merged into the start's first (AGL-3730): the pages it\n * put live join the published list, a page it published leaves the drafts,\n * and a page it left a draft is reported with its newest reason.\n */\nexport function aiMergeSitePublish(previous: AiJobSitePublish, retried: AiJobSitePublish): AiJobSitePublish {\n const now = new Set([...retried.published, ...retried.drafts].map((page) => page.id))\n return {\n liveUrl: retried.liveUrl ?? previous.liveUrl,\n published: [...previous.published.filter((page) => !now.has(page.id)), ...retried.published],\n drafts: [...previous.drafts.filter((page) => !now.has(page.id)), ...retried.drafts],\n }\n}\n\n/**\n * Publishes the pages a guided site start built. Never throws for a page; a\n * Firestore failure of the publish write itself reports every page as a\n * draft, for the same reason, and is logged.\n */\nexport async function aiPublishGuidedSite(\n firestore: Firestore,\n input: {\n job: Pick<AiJob, '$id' | 'orgId' | 'hostId'>\n outputs: readonly AiJobOutput[]\n now: Date\n /** The start owed its blog's posts and wrote none (AGL-3660): its links to the blog come out. */\n blogUnwritten?: boolean\n /** Paths the layout links that the start did not write, such as a store page (AGL-3676): their links come out. */\n unwrittenHrefs?: readonly string[]\n /** A store's links a build drafted into the site's own layout (AGL-3676), which go live with its pages. */\n storeLinks?: AiSitePublishStoreLinks | null\n /**\n * The start published once already, and Try again rebuilt what failed\n * (AGL-3730): only `outputs` go live, beside what is. The home address is\n * never taken from the starter here, the starter is never retired, and\n * `site_published` is not sent again; a retried page at `/` that the\n * starter still holds stays a draft and says so.\n */\n retry?: boolean\n },\n deps: AiSitePublishDeps = {},\n): Promise<AiJobSitePublish> {\n const sendSitePublished = deps.sendSitePublished ?? sendGa4SitePublished\n const dropCache = deps.dropCache ?? dropPluginSiteCache\n const { job, now } = input\n const hostId = job.hostId ?? ''\n const pages = input.outputs.filter(\n (output, index, all) =>\n output.resource === 'screen' &&\n (output.hostId ?? hostId) === hostId &&\n all.findIndex((other) => other.resource === 'screen' && other.id === output.id) === index,\n )\n const hostRef = firestore.collection('hosts').doc(hostId)\n const host = await hostRef.get()\n const result: AiJobSitePublish = {\n liveUrl: aiSiteLiveUrl({ cname: host.get('cname'), subdomain: host.get('subdomain') }),\n published: [],\n drafts: [],\n }\n if (!host.exists || !pages.length) return result\n\n const before = { ...((host.get('screens') ?? {}) as Record<string, string>) }\n const starter = (host.get('defaultHomeScreenId') as string | undefined) || null\n // A retry never takes `/` from the starter (AGL-3730): it holds it like any page.\n const placeholder = input.retry ? null : starter\n const routing = { ...before }\n const entries: Record<string, string> = {}\n const accepted: Array<{ id: string; label: string; path: string; slug: string }> = []\n for (const page of pages) {\n const snapshot = await hostRef.collection('screens').doc(page.id).get()\n const label = String(snapshot.get('displayName') ?? page.label ?? '') || page.label\n if (!snapshot.exists || snapshot.get('deletedAt') != null) {\n result.drafts.push({ id: page.id, label, reason: AI_SITE_PUBLISH_DELETED })\n continue\n }\n const slug = normalizeScreenSlug(String(snapshot.get('slug') ?? ''))\n if (!slug) {\n result.drafts.push({ id: page.id, label, reason: AI_SITE_PUBLISH_NO_ADDRESS })\n continue\n }\n const owner = blockingRouteOwner(routing, slug, placeholder)\n if (owner && owner !== page.id) {\n result.drafts.push({ id: page.id, label, reason: aiSitePublishTakenCopy(slug) })\n continue\n }\n routing[page.id] = slug\n entries[page.id] = slug\n accepted.push({ id: page.id, label, path: slug, slug })\n }\n if (!accepted.length) return result\n\n // The first real home page replaces the placeholder in the same write.\n const home = accepted.find((page) => page.path === SCREEN_ROOT_PATH) ?? null\n const releases =\n home && placeholder && placeholder !== home.id && before[placeholder] === SCREEN_ROOT_PATH\n ? placeholder\n : null\n // An untouched starter home (still its one first version) is retired to the\n // trash; one its owner edited stays in Pages as their draft.\n const retire = releases\n ? (await hostRef.collection('screens').doc(releases).collection('versions').limit(2).get()).size <= 1\n : false\n\n // The header of the layout the pages render inside learns their entries.\n const layoutWrite = await aiNavigationLayoutWrite(firestore, {\n hostId,\n pageIds: accepted.map((page) => page.id),\n entries: navEntriesOf(pages).filter((entry) => entries[entry.screenId]),\n droppedHrefs: [...aiSiteUnwrittenBlogHrefs(input.blogUnwritten), ...(input.unwrittenHrefs ?? [])],\n retiredHomeId: releases,\n homeId: home?.id ?? null,\n jobId: job.$id,\n now,\n storeLinks: input.storeLinks ?? null,\n }).catch((error: unknown) => {\n console.error('ai site publish: navigation not applied', { hostId, jobId: job.$id, error })\n return null\n })\n // A store's links drafted into a layout the pages' navigation did not touch go live on their own.\n const storeLinks = input.storeLinks ?? null\n const storeLinksWrite =\n storeLinks && layoutWrite?.layoutRef.id !== storeLinks.layoutId\n ? await aiLayoutVersionWrite(firestore, {\n hostId,\n layoutId: storeLinks.layoutId,\n transform: () => null,\n storeLinks,\n jobId: job.$id,\n now,\n }).catch((error: unknown) => {\n console.error('ai site publish: store links not applied', { hostId, jobId: job.$id, error })\n return null\n })\n : null\n const layoutWrites = [layoutWrite, storeLinksWrite].filter((write): write is LayoutWrite => Boolean(write))\n\n const paths = [\n ...new Set(\n [\n ...Object.values(entries),\n ...(releases ? [SCREEN_ROOT_PATH] : []),\n ].map((path) => screenRoutePathToUrl(path)),\n ),\n ]\n const batch = firestore.batch()\n const hostUpdates: Record<string, unknown> = { updatedAt: now }\n for (const [id, path] of Object.entries(entries)) hostUpdates[`screens.${id}`] = path\n if (releases) {\n hostUpdates[`screens.${releases}`] = FieldValue.delete()\n hostUpdates['defaultHomeScreenId'] = FieldValue.delete()\n batch.set(\n hostRef.collection('screens').doc(releases),\n { publishedAt: FieldValue.delete(), ...(retire ? { deletedAt: now } : {}) },\n { merge: true },\n )\n } else if (placeholder && entries[placeholder]) {\n hostUpdates['defaultHomeScreenId'] = FieldValue.delete()\n }\n batch.update(hostRef, hostUpdates)\n for (const page of accepted) {\n batch.set(hostRef.collection('screens').doc(page.id), { slug: page.slug, publishedAt: now }, { merge: true })\n }\n for (const write of layoutWrites) {\n if (write.version) batch.create(write.versionRef, write.version)\n batch.set(write.layoutRef, { versionId: write.versionRef.id, updatedAt: now }, { merge: true })\n }\n // The announce, written down in the publish's own batch (AGL-2575): a\n // layout change reaches every page, so it asks for the whole site.\n const outboxRef = firestore.collection('publishOutbox').doc(createResourceUid())\n batch.set(outboxRef, {\n hostId,\n paths: paths.length ? paths : [SCREEN_ROOT_PATH],\n createdAt: FieldValue.serverTimestamp(),\n attempts: 0,\n ...(layoutWrites.length ? { entireHost: true } : {}),\n })\n try {\n await batch.commit()\n } catch (error) {\n console.error('ai site publish failed', { hostId, jobId: job.$id, error })\n for (const page of accepted) result.drafts.push({ id: page.id, label: page.label, reason: AI_SITE_PUBLISH_FAILED })\n return result\n }\n result.published = accepted.map(({ id, label, path }) => ({ id, label, path: screenRoutePathToUrl(path) }))\n\n // The live site's cached pages go now; the outbox entry stays for the\n // drain unless the drop certainly happened.\n const dropped = await dropCache({\n hostIds: [hostId],\n reason: 'guided AI site start published',\n ...(layoutWrites.length ? {} : { paths: { [hostId]: paths } }),\n })\n if (dropped.complete) await outboxRef.delete().catch(() => undefined)\n // A site that came alive with nobody's browser on the publish (AGL-1589),\n // announced once: a retry's pages join a site already announced (AGL-3730).\n if (!input.retry) await sendSitePublished({ hostId, firstPublish: isFirstPublishedRoute(before, placeholder) })\n return result\n}\n\ninterface LayoutWrite {\n layoutRef: FirebaseFirestore.DocumentReference\n versionRef: FirebaseFirestore.DocumentReference\n /** The new version to create; `null` where the live version becomes one already written (a store's links draft). */\n version: Record<string, unknown> | null\n}\n\n/**\n * The new live version of the layout the published pages render inside, with\n * their navigation entries and the home link moved off a retired starter;\n * `null` when there is nothing to change or no one layout to change.\n */\nasync function aiNavigationLayoutWrite(\n firestore: Firestore,\n input: {\n hostId: string\n pageIds: readonly string[]\n entries: readonly NavEntry[]\n droppedHrefs: readonly string[]\n retiredHomeId: string | null\n homeId: string | null\n jobId: string\n now: Date\n storeLinks?: AiSitePublishStoreLinks | null\n },\n): Promise<LayoutWrite | null> {\n if (!input.entries.length && !input.retiredHomeId && !input.droppedHrefs.length && !input.storeLinks) return null\n const hostRef = firestore.collection('hosts').doc(input.hostId)\n const layoutIds = new Set<string>()\n for (const id of input.pageIds) {\n const screen = await hostRef.collection('screens').doc(id).get()\n const versionId = screen.get('versionId')\n const version =\n typeof versionId === 'string' && versionId\n ? await hostRef.collection('screens').doc(id).collection('versions').doc(versionId).get()\n : null\n const layoutId = version?.get('layoutId') ?? screen.get('layoutId')\n if (typeof layoutId === 'string' && layoutId) layoutIds.add(layoutId)\n }\n if (layoutIds.size !== 1) return null\n const [layoutId] = [...layoutIds]\n return aiLayoutVersionWrite(firestore, {\n hostId: input.hostId,\n layoutId,\n transform: (nodes) => aiLayoutWithNavigation(nodes, input),\n storeLinks: input.storeLinks ?? null,\n jobId: input.jobId,\n now: input.now,\n })\n}\n\n/**\n * The new live version of one layout: `transform` applied to the version\n * live now. A store's links drafted into this layout (AGL-3676) start it from\n * that draft while the draft was made from the live version, and are added\n * to the live version otherwise; a draft with nothing more to add becomes\n * the live version as it is. `null` when nothing changes.\n */\nasync function aiLayoutVersionWrite(\n firestore: Firestore,\n input: {\n hostId: string\n layoutId: string\n transform: (nodes: NodesMap) => NodesMap | null\n storeLinks: AiSitePublishStoreLinks | null\n jobId: string\n now: Date\n },\n): Promise<LayoutWrite | null> {\n const { layoutId } = input\n const layoutRef = firestore.collection('hosts').doc(input.hostId).collection('layouts').doc(layoutId)\n const layout = await layoutRef.get()\n const currentVersionId = layout.get('versionId')\n if (!layout.exists || layout.get('deletedAt') != null || typeof currentVersionId !== 'string') return null\n const current = await layoutRef.collection('versions').doc(currentVersionId).get()\n const links = input.storeLinks?.layoutId === layoutId ? input.storeLinks : null\n const draftRef = links ? layoutRef.collection('versions').doc(links.versionId) : null\n const draft = draftRef ? await draftRef.get() : null\n const fromDraft = Boolean(draft?.exists && draft.get(STORE_LINKS_SOURCE_FIELD) === currentVersionId)\n const base = fromDraft && draft ? draft : current\n const nodes = base.exists ? decodeStoredNodes<NodesMap>(base.get('nodes')) : null\n if (!nodes) return null\n let next = input.transform(nodes)\n if (links && !fromDraft) {\n const added = aiLayoutWithStoreLinks((next ?? nodes) as never, links.links, links.screenPaths)\n next = (added.nodes as NodesMap | null) ?? next\n }\n if (!next) return fromDraft && draftRef ? { layoutRef, versionRef: draftRef, version: null } : null\n const packed = encodeStoredNodes(next)\n if (!packed) return null\n const versionRef = layoutRef.collection('versions').doc(createResourceUid())\n return {\n layoutRef,\n versionRef,\n version: {\n layoutId,\n hostId: input.hostId,\n displayName: 'Navigation for the new pages',\n nodes: Buffer.from(packed),\n createdAt: input.now,\n updatedAt: input.now,\n aiJobId: input.jobId,\n },\n }\n}\n"],"names":["isFirstPublishedRoute","createResourceUid","hostPublicOrigin","blockingRouteOwner","normalizeScreenSlug","SCREEN_ROOT_PATH","screenRoutePathToUrl","decodeStoredNodes","encodeStoredNodes","dropPluginSiteCache","sendGa4SitePublished","FieldValue","AI_SITE_BLOG_SLUGS","aiLayoutWithStoreLinks","AI_STORE_LINKS_SOURCE_FIELD","STORE_LINKS_SOURCE_FIELD","aiJobPublishesBuild","job","kind","inputs","aiJobPublishesSite","aiSiteLiveUrl","host","text","value","trim","origin","cname","subdomain","AI_SITE_PUBLISH_DELETED","AI_SITE_PUBLISH_NO_ADDRESS","aiSitePublishTakenCopy","path","AI_SITE_PUBLISH_FAILED","navEntriesOf","outputs","entries","output","resource","navigation","proposal","label","some","entry","screenId","id","push","aiSiteUnwrittenBlogHrefs","blogUnwritten","map","slug","aiLayoutWithNavigation","nodes","input","node","Object","props","changed","dropped","Set","droppedHrefs","size","gone","filter","componentId","has","String","values","child","links","retiredHomeId","homeId","link","linked","Boolean","toolbar","find","missing","length","children","lastLink","reduce","at","index","added","$id","pluginId","parentId","renderAs","color","sx","textDecoration","whiteSpace","slice","aiMergeSitePublish","previous","retried","now","published","drafts","page","liveUrl","aiPublishGuidedSite","firestore","deps","accepted","sendSitePublished","dropCache","hostId","pages","all","findIndex","other","hostRef","collection","doc","get","result","exists","before","starter","placeholder","retry","routing","snapshot","reason","owner","home","releases","retire","limit","layoutWrite","aiNavigationLayoutWrite","pageIds","unwrittenHrefs","jobId","storeLinks","catch","error","console","storeLinksWrite","layoutRef","layoutId","aiLayoutVersionWrite","transform","layoutWrites","write","paths","batch","hostUpdates","updatedAt","delete","set","publishedAt","deletedAt","merge","update","version","create","versionRef","versionId","outboxRef","createdAt","serverTimestamp","attempts","entireHost","commit","hostIds","complete","undefined","firstPublish","layoutIds","screen","add","layout","currentVersionId","current","draftRef","draft","fromDraft","base","next","screenPaths","packed","displayName","Buffer","from","aiJobId"],"mappings":"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"}
@@ -14,7 +14,8 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
14
14
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
15
  * See the License for the specific language governing permissions and
16
16
  * limitations under the License.
17
- */ import { aiPageLinkSection } from "../runtime/ai-page-links.js";
17
+ */ import { aiAsksToGetInTouch } from "../runtime/ai-doctrine-validators.js";
18
+ import { aiPageLinkSection } from "../runtime/ai-page-links.js";
18
19
  /**
19
20
  * A link's words for where it now goes (AGL-3676): a button that named a
20
21
  * page merged into the blog — "Read the articles" — names the blog instead,
@@ -37,7 +38,9 @@ function slugOf(slug) {
37
38
  return `/${slug.replace(/^\/+|\/+$/g, '')}`.toLowerCase();
38
39
  }
39
40
  const HOME_WORDS = /\bhome(?:\s?page)?\b/i;
40
- /** Words that ask a visitor to get in touch, which the site's form answers. */ const CONTACT_WORDS = /\b(?:contact|book|booking|call|quote|estimate|appointment|schedule|reserve|reservation|get in touch|request|visit|enquire|inquire|enquiry|inquiry|message|talk|consult|consultation|order|sign up|join|apply|start)\b/i;
41
+ /** Words that ask a visitor to get in touch, which the site's form answers: rule 10's own list (AGL-3729). */ const CONTACT_WORDS = {
42
+ test: aiAsksToGetInTouch
43
+ };
41
44
  /** The page a word names: an id, a label or an address. */ function pageNamed(word, targets) {
42
45
  var _targets_pages_find, _ref, _targets_pages_find1;
43
46
  const value = word.trim();
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/layout-language/ai-layout-links.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 { aiPageLinkSection } from '../runtime/ai-page-links'\nimport type { AiLayoutListing } from './ai-layout-listings'\n\n/**\n * Where a link the layout language names goes (AGL-3660), resolved by code to\n * a destination rule 10 admits: a page of this site by its id, a section of\n * this page by the platform's Scroll to element interaction, or an `https:`\n * address the brief itself gave. A destination that resolves to nothing, or\n * to the home page under words that do not say home, is no destination, and\n * the compiler leaves the button out rather than shipping a dead control.\n */\n\n/** A page of the site a link may go to: built, or minted on a guided start's plan. */\nexport interface AiLayoutPage {\n id: string\n label: string\n slug: string\n /**\n * A destination on the site that is no page of its own, linked by its path:\n * the blog a guided start writes its posts into (AGL-3660), whose `id` is\n * no screen. Absent for every page.\n */\n href?: string\n /**\n * The title of a planned page merged into this destination (AGL-3676): a\n * page that stood in for the blog, kept only so links to it go to the blog.\n * A link to it is named for the blog, not for the page it replaced.\n */\n standsInFor?: string\n}\n\n/** Everything a page's links resolve against. */\nexport interface AiLayoutTargets {\n /** The page being built, which no link of its own goes to. */\n pageId?: string | null\n /** The site's pages a link may go to. */\n pages: readonly AiLayoutPage[]\n /** The site's home pages, which only words that say home link (rule 10). */\n homeIds: readonly string[]\n /** The saved forms a page may place. */\n forms: readonly { id: string; name: string }[]\n /** The page that places the site's form, where that is another page. */\n formPageId?: string | null\n /** The reusable components a page may place, with the props each fills. */\n components: readonly {\n id: string\n name: string\n props: Record<string, string>\n }[]\n /** Every word the job was given: the brief, the site's answers, its profile. */\n facts: string\n /**\n * The kinds of record the site keeps that its pages show — its catalog,\n * its blog — and the sections that place each (AGL-3676,\n * `ai-layout-listings.ts`). Absent or empty, a page shows only its words.\n */\n listings?: readonly AiLayoutListing[]\n}\n\n/** Where a link resolved to. */\nexport type AiLayoutDestination =\n | { kind: 'page'; screenId: string }\n | { kind: 'section'; index: number }\n | { kind: 'href'; href: string }\n /**\n * A path on this site that is no page of its own, such as the blog\n * (AGL-3660); reached through a planned page merged into it, the title\n * that page had and the name it now goes by (AGL-3676).\n */\n | { kind: 'path'; href: string; standsInFor?: string; label?: string }\n\n/**\n * A link's words for where it now goes (AGL-3676): a button that named a\n * page merged into the blog — \"Read the articles\" — names the blog instead,\n * \"Read the blog\". Words that name neither are kept.\n */\nexport function aiLayoutRenamedLabel(label: string, destination: AiLayoutDestination): string {\n if (destination.kind !== 'path' || !destination.standsInFor || !destination.label) return label\n const name = destination.label.toLowerCase()\n const words = wordsOf(destination.standsInFor)\n if (!words.length) return label\n // The page's own name, or its words alone, singular or plural.\n const stems = words.map((word) => word.replace(/s$/, ''))\n const pattern = new RegExp(`\\\\b(?:${stems.map((stem) => stem.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')).join('\\\\s+')})s?\\\\b`, 'giu')\n const renamed = label.replace(pattern, (found) => (found[0] === found[0].toUpperCase() ? destination.label as string : name))\n return renamed\n}\n\n/** The words of a label or a name, as they are compared. */\nfunction wordsOf(text: string): string[] {\n return text\n .toLowerCase()\n .split(/[^\\p{L}\\p{N}]+/u)\n .filter(Boolean)\n}\n\nfunction slugOf(slug: string): string {\n return `/${slug.replace(/^\\/+|\\/+$/g, '')}`.toLowerCase()\n}\n\nconst HOME_WORDS = /\\bhome(?:\\s?page)?\\b/i\n\n/** Words that ask a visitor to get in touch, which the site's form answers. */\nconst CONTACT_WORDS =\n /\\b(?:contact|book|booking|call|quote|estimate|appointment|schedule|reserve|reservation|get in touch|request|visit|enquire|inquire|enquiry|inquiry|message|talk|consult|consultation|order|sign up|join|apply|start)\\b/i\n\nexport interface AiLayoutLinkScope {\n /** The page's section names in plan order. */\n sections: readonly string[]\n /** The section the link sits in, which it does not scroll to. */\n from: number\n /** The section of this page that places the site's form, if one does. */\n formSection: number | null\n}\n\n/** The page a word names: an id, a label or an address. */\nfunction pageNamed(\n word: string,\n targets: AiLayoutTargets,\n): AiLayoutPage | null {\n const value = word.trim()\n if (!value) return null\n const byId = targets.pages.find((page) => page.id === value)\n if (byId) return byId\n const lower = value.toLowerCase()\n if (lower.startsWith('/'))\n return (\n targets.pages.find((page) => slugOf(page.slug) === slugOf(lower)) ?? null\n )\n const words = wordsOf(value).join(' ')\n return (\n targets.pages.find((page) => wordsOf(page.label).join(' ') === words) ??\n // A page merged into the blog is still named by the title it had (AGL-3676).\n targets.pages.find((page) => page.standsInFor && wordsOf(page.standsInFor).join(' ') === words) ??\n null\n )\n}\n\n/** A destination rule 10 admits, from the page it names, for the words a link shows. */\nfunction pageDestination(\n page: AiLayoutPage | null,\n label: string,\n targets: AiLayoutTargets,\n): AiLayoutDestination | null {\n if (!page || page.id === targets.pageId) return null\n if (\n targets.homeIds.includes(page.id) &&\n !HOME_WORDS.test(label) &&\n !isHomeSlug(page.slug)\n )\n return null\n if (isHomeSlug(page.slug) && !HOME_WORDS.test(label)) return null\n if (page.href) return { kind: 'path', href: page.href, ...(page.standsInFor ? { standsInFor: page.standsInFor, label: page.label } : {}) }\n return { kind: 'page', screenId: page.id }\n}\n\nfunction isHomeSlug(slug: string): boolean {\n return slugOf(slug) === '/'\n}\n\n/** Where the site's form is: a section of this page, else the page that places it. */\nfunction formDestination(\n scope: AiLayoutLinkScope,\n targets: AiLayoutTargets,\n): AiLayoutDestination | null {\n if (scope.formSection !== null && scope.formSection !== scope.from)\n return { kind: 'section', index: scope.formSection }\n if (targets.formPageId && targets.formPageId !== targets.pageId) {\n return { kind: 'page', screenId: targets.formPageId }\n }\n return null\n}\n\n/**\n * Where a link goes: the destination its `to` names, read the language's\n * way; failing that, for words that ask a visitor to get in touch, the site's\n * form; failing that, a page whose name the words say. `null` is a link with\n * nowhere to go, which is left out.\n */\nexport function aiLayoutResolveLink(\n to: string | undefined,\n label: string,\n scope: AiLayoutLinkScope,\n targets: AiLayoutTargets,\n): AiLayoutDestination | null {\n const named = to ? resolveNamed(to.trim(), label, scope, targets) : null\n if (named) return named\n if (CONTACT_WORDS.test(label)) {\n const form = formDestination(scope, targets)\n if (form) return form\n const contact = targets.pages.find(\n (page) =>\n page.id !== targets.pageId &&\n !isHomeSlug(page.slug) &&\n CONTACT_WORDS.test(page.label),\n )\n if (contact) return { kind: 'page', screenId: contact.id }\n }\n const labelWords = new Set(wordsOf(label))\n const said = targets.pages.filter(\n (page) =>\n page.id !== targets.pageId &&\n !isHomeSlug(page.slug) &&\n wordsOf(page.label).length > 0 &&\n wordsOf(page.label).every((word) => labelWords.has(word)),\n )\n return said.length === 1 ? { kind: 'page', screenId: said[0].id } : null\n}\n\nfunction resolveNamed(\n to: string,\n label: string,\n scope: AiLayoutLinkScope,\n targets: AiLayoutTargets,\n): AiLayoutDestination | null {\n const lower = to.toLowerCase()\n if (\n lower === 'form' ||\n lower.startsWith('form:') ||\n targets.forms.some((form) => form.id === to)\n ) {\n return formDestination(scope, targets)\n }\n if (lower.startsWith('https://')) {\n return targets.facts.includes(to) ? { kind: 'href', href: to } : null\n }\n const section = lower.startsWith('#')\n ? to.slice(1)\n : lower.startsWith('section:')\n ? to.slice(8)\n : null\n if (section !== null) {\n const number = Number(section.trim())\n const index =\n Number.isInteger(number) && number >= 1 && number <= scope.sections.length\n ? number - 1\n : aiPageLinkSection(section, scope.sections)\n return index !== null && index !== scope.from\n ? { kind: 'section', index }\n : null\n }\n const page = pageNamed(lower.startsWith('page:') ? to.slice(5) : to, targets)\n return pageDestination(page, label, targets)\n}\n"],"names":["aiPageLinkSection","aiLayoutRenamedLabel","label","destination","kind","standsInFor","name","toLowerCase","words","wordsOf","length","stems","map","word","replace","pattern","RegExp","stem","join","renamed","found","toUpperCase","text","split","filter","Boolean","slugOf","slug","HOME_WORDS","CONTACT_WORDS","pageNamed","targets","value","trim","byId","pages","find","page","id","lower","startsWith","pageDestination","pageId","homeIds","includes","test","isHomeSlug","href","screenId","formDestination","scope","formSection","from","index","formPageId","aiLayoutResolveLink","to","named","resolveNamed","form","contact","labelWords","Set","said","every","has","forms","some","facts","section","slice","number","Number","isInteger","sections"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,iBAAiB,QAAQ,8BAA0B;AAuE5D;;;;CAIC,GACD,OAAO,SAASC,qBAAqBC,KAAa,EAAEC,WAAgC;IAClF,IAAIA,YAAYC,IAAI,KAAK,UAAU,CAACD,YAAYE,WAAW,IAAI,CAACF,YAAYD,KAAK,EAAE,OAAOA;IAC1F,MAAMI,OAAOH,YAAYD,KAAK,CAACK,WAAW;IAC1C,MAAMC,QAAQC,QAAQN,YAAYE,WAAW;IAC7C,IAAI,CAACG,MAAME,MAAM,EAAE,OAAOR;IAC1B,+DAA+D;IAC/D,MAAMS,QAAQH,MAAMI,GAAG,CAAC,CAACC,OAASA,KAAKC,OAAO,CAAC,MAAM;IACrD,MAAMC,UAAU,IAAIC,OAAO,CAAC,MAAM,EAAEL,MAAMC,GAAG,CAAC,CAACK,OAASA,KAAKH,OAAO,CAAC,uBAAuB,SAASI,IAAI,CAAC,QAAQ,MAAM,CAAC,EAAE;IAC3H,MAAMC,UAAUjB,MAAMY,OAAO,CAACC,SAAS,CAACK,QAAWA,KAAK,CAAC,EAAE,KAAKA,KAAK,CAAC,EAAE,CAACC,WAAW,KAAKlB,YAAYD,KAAK,GAAaI;IACvH,OAAOa;AACT;AAEA,0DAA0D,GAC1D,SAASV,QAAQa,IAAY;IAC3B,OAAOA,KACJf,WAAW,GACXgB,KAAK,CAAC,8maACNC,MAAM,CAACC;AACZ;AAEA,SAASC,OAAOC,IAAY;IAC1B,OAAO,CAAC,CAAC,EAAEA,KAAKb,OAAO,CAAC,cAAc,KAAK,CAACP,WAAW;AACzD;AAEA,MAAMqB,aAAa;AAEnB,6EAA6E,GAC7E,MAAMC,gBACJ;AAWF,yDAAyD,GACzD,SAASC,UACPjB,IAAY,EACZkB,OAAwB;QASpBA,qBAIFA,MAAAA;IAXF,MAAMC,QAAQnB,KAAKoB,IAAI;IACvB,IAAI,CAACD,OAAO,OAAO;IACnB,MAAME,OAAOH,QAAQI,KAAK,CAACC,IAAI,CAAC,CAACC,OAASA,KAAKC,EAAE,KAAKN;IACtD,IAAIE,MAAM,OAAOA;IACjB,MAAMK,QAAQP,MAAMzB,WAAW;IAC/B,IAAIgC,MAAMC,UAAU,CAAC,MACnB,QACET,sBAAAA,QAAQI,KAAK,CAACC,IAAI,CAAC,CAACC,OAASX,OAAOW,KAAKV,IAAI,MAAMD,OAAOa,mBAA1DR,sBAAqE;IAEzE,MAAMvB,QAAQC,QAAQuB,OAAOd,IAAI,CAAC;IAClC,QACEa,QAAAA,uBAAAA,QAAQI,KAAK,CAACC,IAAI,CAAC,CAACC,OAAS5B,QAAQ4B,KAAKnC,KAAK,EAAEgB,IAAI,CAAC,SAASV,kBAA/DuB,uBACA,6EAA6E;IAC7EA,QAAQI,KAAK,CAACC,IAAI,CAAC,CAACC,OAASA,KAAKhC,WAAW,IAAII,QAAQ4B,KAAKhC,WAAW,EAAEa,IAAI,CAAC,SAASV,kBAFzFuB,OAGA;AAEJ;AAEA,sFAAsF,GACtF,SAASU,gBACPJ,IAAyB,EACzBnC,KAAa,EACb6B,OAAwB;IAExB,IAAI,CAACM,QAAQA,KAAKC,EAAE,KAAKP,QAAQW,MAAM,EAAE,OAAO;IAChD,IACEX,QAAQY,OAAO,CAACC,QAAQ,CAACP,KAAKC,EAAE,KAChC,CAACV,WAAWiB,IAAI,CAAC3C,UACjB,CAAC4C,WAAWT,KAAKV,IAAI,GAErB,OAAO;IACT,IAAImB,WAAWT,KAAKV,IAAI,KAAK,CAACC,WAAWiB,IAAI,CAAC3C,QAAQ,OAAO;IAC7D,IAAImC,KAAKU,IAAI,EAAE,OAAO;QAAE3C,MAAM;QAAQ2C,MAAMV,KAAKU,IAAI;OAAMV,KAAKhC,WAAW,GAAG;QAAEA,aAAagC,KAAKhC,WAAW;QAAEH,OAAOmC,KAAKnC,KAAK;IAAC,IAAI,CAAC;IACtI,OAAO;QAAEE,MAAM;QAAQ4C,UAAUX,KAAKC,EAAE;IAAC;AAC3C;AAEA,SAASQ,WAAWnB,IAAY;IAC9B,OAAOD,OAAOC,UAAU;AAC1B;AAEA,oFAAoF,GACpF,SAASsB,gBACPC,KAAwB,EACxBnB,OAAwB;IAExB,IAAImB,MAAMC,WAAW,KAAK,QAAQD,MAAMC,WAAW,KAAKD,MAAME,IAAI,EAChE,OAAO;QAAEhD,MAAM;QAAWiD,OAAOH,MAAMC,WAAW;IAAC;IACrD,IAAIpB,QAAQuB,UAAU,IAAIvB,QAAQuB,UAAU,KAAKvB,QAAQW,MAAM,EAAE;QAC/D,OAAO;YAAEtC,MAAM;YAAQ4C,UAAUjB,QAAQuB,UAAU;QAAC;IACtD;IACA,OAAO;AACT;AAEA;;;;;CAKC,GACD,OAAO,SAASC,oBACdC,EAAsB,EACtBtD,KAAa,EACbgD,KAAwB,EACxBnB,OAAwB;IAExB,MAAM0B,QAAQD,KAAKE,aAAaF,GAAGvB,IAAI,IAAI/B,OAAOgD,OAAOnB,WAAW;IACpE,IAAI0B,OAAO,OAAOA;IAClB,IAAI5B,cAAcgB,IAAI,CAAC3C,QAAQ;QAC7B,MAAMyD,OAAOV,gBAAgBC,OAAOnB;QACpC,IAAI4B,MAAM,OAAOA;QACjB,MAAMC,UAAU7B,QAAQI,KAAK,CAACC,IAAI,CAChC,CAACC,OACCA,KAAKC,EAAE,KAAKP,QAAQW,MAAM,IAC1B,CAACI,WAAWT,KAAKV,IAAI,KACrBE,cAAcgB,IAAI,CAACR,KAAKnC,KAAK;QAEjC,IAAI0D,SAAS,OAAO;YAAExD,MAAM;YAAQ4C,UAAUY,QAAQtB,EAAE;QAAC;IAC3D;IACA,MAAMuB,aAAa,IAAIC,IAAIrD,QAAQP;IACnC,MAAM6D,OAAOhC,QAAQI,KAAK,CAACX,MAAM,CAC/B,CAACa,OACCA,KAAKC,EAAE,KAAKP,QAAQW,MAAM,IAC1B,CAACI,WAAWT,KAAKV,IAAI,KACrBlB,QAAQ4B,KAAKnC,KAAK,EAAEQ,MAAM,GAAG,KAC7BD,QAAQ4B,KAAKnC,KAAK,EAAE8D,KAAK,CAAC,CAACnD,OAASgD,WAAWI,GAAG,CAACpD;IAEvD,OAAOkD,KAAKrD,MAAM,KAAK,IAAI;QAAEN,MAAM;QAAQ4C,UAAUe,IAAI,CAAC,EAAE,CAACzB,EAAE;IAAC,IAAI;AACtE;AAEA,SAASoB,aACPF,EAAU,EACVtD,KAAa,EACbgD,KAAwB,EACxBnB,OAAwB;IAExB,MAAMQ,QAAQiB,GAAGjD,WAAW;IAC5B,IACEgC,UAAU,UACVA,MAAMC,UAAU,CAAC,YACjBT,QAAQmC,KAAK,CAACC,IAAI,CAAC,CAACR,OAASA,KAAKrB,EAAE,KAAKkB,KACzC;QACA,OAAOP,gBAAgBC,OAAOnB;IAChC;IACA,IAAIQ,MAAMC,UAAU,CAAC,aAAa;QAChC,OAAOT,QAAQqC,KAAK,CAACxB,QAAQ,CAACY,MAAM;YAAEpD,MAAM;YAAQ2C,MAAMS;QAAG,IAAI;IACnE;IACA,MAAMa,UAAU9B,MAAMC,UAAU,CAAC,OAC7BgB,GAAGc,KAAK,CAAC,KACT/B,MAAMC,UAAU,CAAC,cACfgB,GAAGc,KAAK,CAAC,KACT;IACN,IAAID,YAAY,MAAM;QACpB,MAAME,SAASC,OAAOH,QAAQpC,IAAI;QAClC,MAAMoB,QACJmB,OAAOC,SAAS,CAACF,WAAWA,UAAU,KAAKA,UAAUrB,MAAMwB,QAAQ,CAAChE,MAAM,GACtE6D,SAAS,IACTvE,kBAAkBqE,SAASnB,MAAMwB,QAAQ;QAC/C,OAAOrB,UAAU,QAAQA,UAAUH,MAAME,IAAI,GACzC;YAAEhD,MAAM;YAAWiD;QAAM,IACzB;IACN;IACA,MAAMhB,OAAOP,UAAUS,MAAMC,UAAU,CAAC,WAAWgB,GAAGc,KAAK,CAAC,KAAKd,IAAIzB;IACrE,OAAOU,gBAAgBJ,MAAMnC,OAAO6B;AACtC"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/layout-language/ai-layout-links.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 { aiAsksToGetInTouch } from '../runtime/ai-doctrine-validators'\nimport { aiPageLinkSection } from '../runtime/ai-page-links'\nimport type { AiLayoutListing } from './ai-layout-listings'\n\n/**\n * Where a link the layout language names goes (AGL-3660), resolved by code to\n * a destination rule 10 admits: a page of this site by its id, a section of\n * this page by the platform's Scroll to element interaction, or an `https:`\n * address the brief itself gave. A destination that resolves to nothing, or\n * to the home page under words that do not say home, is no destination, and\n * the compiler leaves the button out rather than shipping a dead control.\n */\n\n/** A page of the site a link may go to: built, or minted on a guided start's plan. */\nexport interface AiLayoutPage {\n id: string\n label: string\n slug: string\n /**\n * A destination on the site that is no page of its own, linked by its path:\n * the blog a guided start writes its posts into (AGL-3660), whose `id` is\n * no screen. Absent for every page.\n */\n href?: string\n /**\n * The title of a planned page merged into this destination (AGL-3676): a\n * page that stood in for the blog, kept only so links to it go to the blog.\n * A link to it is named for the blog, not for the page it replaced.\n */\n standsInFor?: string\n}\n\n/** Everything a page's links resolve against. */\nexport interface AiLayoutTargets {\n /** The page being built, which no link of its own goes to. */\n pageId?: string | null\n /** The site's pages a link may go to. */\n pages: readonly AiLayoutPage[]\n /** The site's home pages, which only words that say home link (rule 10). */\n homeIds: readonly string[]\n /** The saved forms a page may place. */\n forms: readonly { id: string; name: string }[]\n /** The page that places the site's form, where that is another page. */\n formPageId?: string | null\n /** The reusable components a page may place, with the props each fills. */\n components: readonly {\n id: string\n name: string\n props: Record<string, string>\n }[]\n /** Every word the job was given: the brief, the site's answers, its profile. */\n facts: string\n /**\n * The kinds of record the site keeps that its pages show — its catalog,\n * its blog — and the sections that place each (AGL-3676,\n * `ai-layout-listings.ts`). Absent or empty, a page shows only its words.\n */\n listings?: readonly AiLayoutListing[]\n}\n\n/** Where a link resolved to. */\nexport type AiLayoutDestination =\n | { kind: 'page'; screenId: string }\n | { kind: 'section'; index: number }\n | { kind: 'href'; href: string }\n /**\n * A path on this site that is no page of its own, such as the blog\n * (AGL-3660); reached through a planned page merged into it, the title\n * that page had and the name it now goes by (AGL-3676).\n */\n | { kind: 'path'; href: string; standsInFor?: string; label?: string }\n\n/**\n * A link's words for where it now goes (AGL-3676): a button that named a\n * page merged into the blog — \"Read the articles\" — names the blog instead,\n * \"Read the blog\". Words that name neither are kept.\n */\nexport function aiLayoutRenamedLabel(label: string, destination: AiLayoutDestination): string {\n if (destination.kind !== 'path' || !destination.standsInFor || !destination.label) return label\n const name = destination.label.toLowerCase()\n const words = wordsOf(destination.standsInFor)\n if (!words.length) return label\n // The page's own name, or its words alone, singular or plural.\n const stems = words.map((word) => word.replace(/s$/, ''))\n const pattern = new RegExp(`\\\\b(?:${stems.map((stem) => stem.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')).join('\\\\s+')})s?\\\\b`, 'giu')\n const renamed = label.replace(pattern, (found) => (found[0] === found[0].toUpperCase() ? destination.label as string : name))\n return renamed\n}\n\n/** The words of a label or a name, as they are compared. */\nfunction wordsOf(text: string): string[] {\n return text\n .toLowerCase()\n .split(/[^\\p{L}\\p{N}]+/u)\n .filter(Boolean)\n}\n\nfunction slugOf(slug: string): string {\n return `/${slug.replace(/^\\/+|\\/+$/g, '')}`.toLowerCase()\n}\n\nconst HOME_WORDS = /\\bhome(?:\\s?page)?\\b/i\n\n/** Words that ask a visitor to get in touch, which the site's form answers: rule 10's own list (AGL-3729). */\nconst CONTACT_WORDS = { test: aiAsksToGetInTouch }\n\nexport interface AiLayoutLinkScope {\n /** The page's section names in plan order. */\n sections: readonly string[]\n /** The section the link sits in, which it does not scroll to. */\n from: number\n /** The section of this page that places the site's form, if one does. */\n formSection: number | null\n}\n\n/** The page a word names: an id, a label or an address. */\nfunction pageNamed(\n word: string,\n targets: AiLayoutTargets,\n): AiLayoutPage | null {\n const value = word.trim()\n if (!value) return null\n const byId = targets.pages.find((page) => page.id === value)\n if (byId) return byId\n const lower = value.toLowerCase()\n if (lower.startsWith('/'))\n return (\n targets.pages.find((page) => slugOf(page.slug) === slugOf(lower)) ?? null\n )\n const words = wordsOf(value).join(' ')\n return (\n targets.pages.find((page) => wordsOf(page.label).join(' ') === words) ??\n // A page merged into the blog is still named by the title it had (AGL-3676).\n targets.pages.find((page) => page.standsInFor && wordsOf(page.standsInFor).join(' ') === words) ??\n null\n )\n}\n\n/** A destination rule 10 admits, from the page it names, for the words a link shows. */\nfunction pageDestination(\n page: AiLayoutPage | null,\n label: string,\n targets: AiLayoutTargets,\n): AiLayoutDestination | null {\n if (!page || page.id === targets.pageId) return null\n if (\n targets.homeIds.includes(page.id) &&\n !HOME_WORDS.test(label) &&\n !isHomeSlug(page.slug)\n )\n return null\n if (isHomeSlug(page.slug) && !HOME_WORDS.test(label)) return null\n if (page.href) return { kind: 'path', href: page.href, ...(page.standsInFor ? { standsInFor: page.standsInFor, label: page.label } : {}) }\n return { kind: 'page', screenId: page.id }\n}\n\nfunction isHomeSlug(slug: string): boolean {\n return slugOf(slug) === '/'\n}\n\n/** Where the site's form is: a section of this page, else the page that places it. */\nfunction formDestination(\n scope: AiLayoutLinkScope,\n targets: AiLayoutTargets,\n): AiLayoutDestination | null {\n if (scope.formSection !== null && scope.formSection !== scope.from)\n return { kind: 'section', index: scope.formSection }\n if (targets.formPageId && targets.formPageId !== targets.pageId) {\n return { kind: 'page', screenId: targets.formPageId }\n }\n return null\n}\n\n/**\n * Where a link goes: the destination its `to` names, read the language's\n * way; failing that, for words that ask a visitor to get in touch, the site's\n * form; failing that, a page whose name the words say. `null` is a link with\n * nowhere to go, which is left out.\n */\nexport function aiLayoutResolveLink(\n to: string | undefined,\n label: string,\n scope: AiLayoutLinkScope,\n targets: AiLayoutTargets,\n): AiLayoutDestination | null {\n const named = to ? resolveNamed(to.trim(), label, scope, targets) : null\n if (named) return named\n if (CONTACT_WORDS.test(label)) {\n const form = formDestination(scope, targets)\n if (form) return form\n const contact = targets.pages.find(\n (page) =>\n page.id !== targets.pageId &&\n !isHomeSlug(page.slug) &&\n CONTACT_WORDS.test(page.label),\n )\n if (contact) return { kind: 'page', screenId: contact.id }\n }\n const labelWords = new Set(wordsOf(label))\n const said = targets.pages.filter(\n (page) =>\n page.id !== targets.pageId &&\n !isHomeSlug(page.slug) &&\n wordsOf(page.label).length > 0 &&\n wordsOf(page.label).every((word) => labelWords.has(word)),\n )\n return said.length === 1 ? { kind: 'page', screenId: said[0].id } : null\n}\n\nfunction resolveNamed(\n to: string,\n label: string,\n scope: AiLayoutLinkScope,\n targets: AiLayoutTargets,\n): AiLayoutDestination | null {\n const lower = to.toLowerCase()\n if (\n lower === 'form' ||\n lower.startsWith('form:') ||\n targets.forms.some((form) => form.id === to)\n ) {\n return formDestination(scope, targets)\n }\n if (lower.startsWith('https://')) {\n return targets.facts.includes(to) ? { kind: 'href', href: to } : null\n }\n const section = lower.startsWith('#')\n ? to.slice(1)\n : lower.startsWith('section:')\n ? to.slice(8)\n : null\n if (section !== null) {\n const number = Number(section.trim())\n const index =\n Number.isInteger(number) && number >= 1 && number <= scope.sections.length\n ? number - 1\n : aiPageLinkSection(section, scope.sections)\n return index !== null && index !== scope.from\n ? { kind: 'section', index }\n : null\n }\n const page = pageNamed(lower.startsWith('page:') ? to.slice(5) : to, targets)\n return pageDestination(page, label, targets)\n}\n"],"names":["aiAsksToGetInTouch","aiPageLinkSection","aiLayoutRenamedLabel","label","destination","kind","standsInFor","name","toLowerCase","words","wordsOf","length","stems","map","word","replace","pattern","RegExp","stem","join","renamed","found","toUpperCase","text","split","filter","Boolean","slugOf","slug","HOME_WORDS","CONTACT_WORDS","test","pageNamed","targets","value","trim","byId","pages","find","page","id","lower","startsWith","pageDestination","pageId","homeIds","includes","isHomeSlug","href","screenId","formDestination","scope","formSection","from","index","formPageId","aiLayoutResolveLink","to","named","resolveNamed","form","contact","labelWords","Set","said","every","has","forms","some","facts","section","slice","number","Number","isInteger","sections"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,kBAAkB,QAAQ,uCAAmC;AACtE,SAASC,iBAAiB,QAAQ,8BAA0B;AAuE5D;;;;CAIC,GACD,OAAO,SAASC,qBAAqBC,KAAa,EAAEC,WAAgC;IAClF,IAAIA,YAAYC,IAAI,KAAK,UAAU,CAACD,YAAYE,WAAW,IAAI,CAACF,YAAYD,KAAK,EAAE,OAAOA;IAC1F,MAAMI,OAAOH,YAAYD,KAAK,CAACK,WAAW;IAC1C,MAAMC,QAAQC,QAAQN,YAAYE,WAAW;IAC7C,IAAI,CAACG,MAAME,MAAM,EAAE,OAAOR;IAC1B,+DAA+D;IAC/D,MAAMS,QAAQH,MAAMI,GAAG,CAAC,CAACC,OAASA,KAAKC,OAAO,CAAC,MAAM;IACrD,MAAMC,UAAU,IAAIC,OAAO,CAAC,MAAM,EAAEL,MAAMC,GAAG,CAAC,CAACK,OAASA,KAAKH,OAAO,CAAC,uBAAuB,SAASI,IAAI,CAAC,QAAQ,MAAM,CAAC,EAAE;IAC3H,MAAMC,UAAUjB,MAAMY,OAAO,CAACC,SAAS,CAACK,QAAWA,KAAK,CAAC,EAAE,KAAKA,KAAK,CAAC,EAAE,CAACC,WAAW,KAAKlB,YAAYD,KAAK,GAAaI;IACvH,OAAOa;AACT;AAEA,0DAA0D,GAC1D,SAASV,QAAQa,IAAY;IAC3B,OAAOA,KACJf,WAAW,GACXgB,KAAK,CAAC,8maACNC,MAAM,CAACC;AACZ;AAEA,SAASC,OAAOC,IAAY;IAC1B,OAAO,CAAC,CAAC,EAAEA,KAAKb,OAAO,CAAC,cAAc,KAAK,CAACP,WAAW;AACzD;AAEA,MAAMqB,aAAa;AAEnB,4GAA4G,GAC5G,MAAMC,gBAAgB;IAAEC,MAAM/B;AAAmB;AAWjD,yDAAyD,GACzD,SAASgC,UACPlB,IAAY,EACZmB,OAAwB;QASpBA,qBAIFA,MAAAA;IAXF,MAAMC,QAAQpB,KAAKqB,IAAI;IACvB,IAAI,CAACD,OAAO,OAAO;IACnB,MAAME,OAAOH,QAAQI,KAAK,CAACC,IAAI,CAAC,CAACC,OAASA,KAAKC,EAAE,KAAKN;IACtD,IAAIE,MAAM,OAAOA;IACjB,MAAMK,QAAQP,MAAM1B,WAAW;IAC/B,IAAIiC,MAAMC,UAAU,CAAC,MACnB,QACET,sBAAAA,QAAQI,KAAK,CAACC,IAAI,CAAC,CAACC,OAASZ,OAAOY,KAAKX,IAAI,MAAMD,OAAOc,mBAA1DR,sBAAqE;IAEzE,MAAMxB,QAAQC,QAAQwB,OAAOf,IAAI,CAAC;IAClC,QACEc,QAAAA,uBAAAA,QAAQI,KAAK,CAACC,IAAI,CAAC,CAACC,OAAS7B,QAAQ6B,KAAKpC,KAAK,EAAEgB,IAAI,CAAC,SAASV,kBAA/DwB,uBACA,6EAA6E;IAC7EA,QAAQI,KAAK,CAACC,IAAI,CAAC,CAACC,OAASA,KAAKjC,WAAW,IAAII,QAAQ6B,KAAKjC,WAAW,EAAEa,IAAI,CAAC,SAASV,kBAFzFwB,OAGA;AAEJ;AAEA,sFAAsF,GACtF,SAASU,gBACPJ,IAAyB,EACzBpC,KAAa,EACb8B,OAAwB;IAExB,IAAI,CAACM,QAAQA,KAAKC,EAAE,KAAKP,QAAQW,MAAM,EAAE,OAAO;IAChD,IACEX,QAAQY,OAAO,CAACC,QAAQ,CAACP,KAAKC,EAAE,KAChC,CAACX,WAAWE,IAAI,CAAC5B,UACjB,CAAC4C,WAAWR,KAAKX,IAAI,GAErB,OAAO;IACT,IAAImB,WAAWR,KAAKX,IAAI,KAAK,CAACC,WAAWE,IAAI,CAAC5B,QAAQ,OAAO;IAC7D,IAAIoC,KAAKS,IAAI,EAAE,OAAO;QAAE3C,MAAM;QAAQ2C,MAAMT,KAAKS,IAAI;OAAMT,KAAKjC,WAAW,GAAG;QAAEA,aAAaiC,KAAKjC,WAAW;QAAEH,OAAOoC,KAAKpC,KAAK;IAAC,IAAI,CAAC;IACtI,OAAO;QAAEE,MAAM;QAAQ4C,UAAUV,KAAKC,EAAE;IAAC;AAC3C;AAEA,SAASO,WAAWnB,IAAY;IAC9B,OAAOD,OAAOC,UAAU;AAC1B;AAEA,oFAAoF,GACpF,SAASsB,gBACPC,KAAwB,EACxBlB,OAAwB;IAExB,IAAIkB,MAAMC,WAAW,KAAK,QAAQD,MAAMC,WAAW,KAAKD,MAAME,IAAI,EAChE,OAAO;QAAEhD,MAAM;QAAWiD,OAAOH,MAAMC,WAAW;IAAC;IACrD,IAAInB,QAAQsB,UAAU,IAAItB,QAAQsB,UAAU,KAAKtB,QAAQW,MAAM,EAAE;QAC/D,OAAO;YAAEvC,MAAM;YAAQ4C,UAAUhB,QAAQsB,UAAU;QAAC;IACtD;IACA,OAAO;AACT;AAEA;;;;;CAKC,GACD,OAAO,SAASC,oBACdC,EAAsB,EACtBtD,KAAa,EACbgD,KAAwB,EACxBlB,OAAwB;IAExB,MAAMyB,QAAQD,KAAKE,aAAaF,GAAGtB,IAAI,IAAIhC,OAAOgD,OAAOlB,WAAW;IACpE,IAAIyB,OAAO,OAAOA;IAClB,IAAI5B,cAAcC,IAAI,CAAC5B,QAAQ;QAC7B,MAAMyD,OAAOV,gBAAgBC,OAAOlB;QACpC,IAAI2B,MAAM,OAAOA;QACjB,MAAMC,UAAU5B,QAAQI,KAAK,CAACC,IAAI,CAChC,CAACC,OACCA,KAAKC,EAAE,KAAKP,QAAQW,MAAM,IAC1B,CAACG,WAAWR,KAAKX,IAAI,KACrBE,cAAcC,IAAI,CAACQ,KAAKpC,KAAK;QAEjC,IAAI0D,SAAS,OAAO;YAAExD,MAAM;YAAQ4C,UAAUY,QAAQrB,EAAE;QAAC;IAC3D;IACA,MAAMsB,aAAa,IAAIC,IAAIrD,QAAQP;IACnC,MAAM6D,OAAO/B,QAAQI,KAAK,CAACZ,MAAM,CAC/B,CAACc,OACCA,KAAKC,EAAE,KAAKP,QAAQW,MAAM,IAC1B,CAACG,WAAWR,KAAKX,IAAI,KACrBlB,QAAQ6B,KAAKpC,KAAK,EAAEQ,MAAM,GAAG,KAC7BD,QAAQ6B,KAAKpC,KAAK,EAAE8D,KAAK,CAAC,CAACnD,OAASgD,WAAWI,GAAG,CAACpD;IAEvD,OAAOkD,KAAKrD,MAAM,KAAK,IAAI;QAAEN,MAAM;QAAQ4C,UAAUe,IAAI,CAAC,EAAE,CAACxB,EAAE;IAAC,IAAI;AACtE;AAEA,SAASmB,aACPF,EAAU,EACVtD,KAAa,EACbgD,KAAwB,EACxBlB,OAAwB;IAExB,MAAMQ,QAAQgB,GAAGjD,WAAW;IAC5B,IACEiC,UAAU,UACVA,MAAMC,UAAU,CAAC,YACjBT,QAAQkC,KAAK,CAACC,IAAI,CAAC,CAACR,OAASA,KAAKpB,EAAE,KAAKiB,KACzC;QACA,OAAOP,gBAAgBC,OAAOlB;IAChC;IACA,IAAIQ,MAAMC,UAAU,CAAC,aAAa;QAChC,OAAOT,QAAQoC,KAAK,CAACvB,QAAQ,CAACW,MAAM;YAAEpD,MAAM;YAAQ2C,MAAMS;QAAG,IAAI;IACnE;IACA,MAAMa,UAAU7B,MAAMC,UAAU,CAAC,OAC7Be,GAAGc,KAAK,CAAC,KACT9B,MAAMC,UAAU,CAAC,cACfe,GAAGc,KAAK,CAAC,KACT;IACN,IAAID,YAAY,MAAM;QACpB,MAAME,SAASC,OAAOH,QAAQnC,IAAI;QAClC,MAAMmB,QACJmB,OAAOC,SAAS,CAACF,WAAWA,UAAU,KAAKA,UAAUrB,MAAMwB,QAAQ,CAAChE,MAAM,GACtE6D,SAAS,IACTvE,kBAAkBqE,SAASnB,MAAMwB,QAAQ;QAC/C,OAAOrB,UAAU,QAAQA,UAAUH,MAAME,IAAI,GACzC;YAAEhD,MAAM;YAAWiD;QAAM,IACzB;IACN;IACA,MAAMf,OAAOP,UAAUS,MAAMC,UAAU,CAAC,WAAWe,GAAGc,KAAK,CAAC,KAAKd,IAAIxB;IACrE,OAAOU,gBAAgBJ,MAAMpC,OAAO8B;AACtC"}
@@ -130,6 +130,14 @@ export interface AiDoctrineTreeContext extends AiNodeTreeContext {
130
130
  * to when the site has none for what it promises. Absent: none is known.
131
131
  */
132
132
  homeScreenIds?: readonly string[];
133
+ /**
134
+ * The site's pages that place its form, by id (AGL-3729): where a link whose
135
+ * words ask a visitor to get in touch (`aiAsksToGetInTouch`) does what it
136
+ * says, even when that page is home. A guided start's café put its
137
+ * pre-order form on Home, and its Menu page's "Request weekend pickup"
138
+ * links there. Absent: none is known, and a link home is held to its words.
139
+ */
140
+ formScreenIds?: readonly string[];
133
141
  /**
134
142
  * The node as the model wrote it, by the id the checked tree gives it
135
143
  * (AGL-3078), for a tree that was stored before it is checked: a page
@@ -472,6 +480,39 @@ export declare function isAiLinkElement(componentId: unknown): boolean;
472
480
  * a re-ask to inherit the band's contrast text or to set it.
473
481
  */
474
482
  export declare function detectInvisibleLinks(tree: AiDoctrineTree, outputKind: AiOutputKind): AiDoctrineViolation[];
483
+ /**
484
+ * Whether a link's words ask a visitor to get in touch — contact, book, quote,
485
+ * request, order, pre-order, enquire — which the site's form answers
486
+ * (AGL-3729). The layout language sends such a link to the form.
487
+ */
488
+ export declare function aiAsksToGetInTouch(label: string): boolean;
489
+ /**
490
+ * RULE 10'S LINK SENT HOME, REPAIRED IN CODE (AGL-3729).
491
+ *
492
+ * The rule's own advice is "link the page that does, or leave the link out",
493
+ * and both halves are something code can do once it knows where the site's
494
+ * form is. A Free guided start's café (beta.243, 2026-10-12) put its contact
495
+ * and pre-order form on Home; its Menu page's "Request weekend pickup" was
496
+ * sent to Home, and the page was refused on every answer, since the
497
+ * destination was the compiler's, not the model's. So, before the check reads
498
+ * the tree, each link `detectUnrelatedScreenLinks` would refuse is:
499
+ *
500
+ * - repointed to the page that places the site's form, when its words ask a
501
+ * visitor to get in touch and the site has such a page that is not the page
502
+ * being checked (`formScreenIds`, `ownScreenIds`). The platform reaches a
503
+ * section of another page by no anchor (AGL-2867), so the link names the
504
+ * page; a link to a section of its own page is the Scroll to element
505
+ * interaction, which this never writes;
506
+ * - otherwise left out: the node and everything under it are taken off the
507
+ * page, and out of its parent's children.
508
+ *
509
+ * The root is never taken out. A tree with nothing to repair comes back as
510
+ * the same object; a node that is changed is copied, never edited in place.
511
+ */
512
+ export declare function aiSettleUnrelatedScreenLinks<T extends AiDoctrineTree>(tree: T, outputKind: AiOutputKind, context?: Pick<AiDoctrineTreeContext, 'homeScreenIds' | 'formScreenIds'> & {
513
+ /** The page being checked, by every id it goes by: a link never goes to itself. */
514
+ ownScreenIds?: readonly string[];
515
+ }): T;
475
516
  /**
476
517
  * Rule 10, for a link sent where the site has nothing for it (AGL-3056). A
477
518
  * link whose purpose the site has no screen for is left out; it never goes to
@@ -483,7 +524,7 @@ export declare function detectInvisibleLinks(tree: AiDoctrineTree, outputKind: A
483
524
  * not say home is refused, with a re-ask to link the screen that does what it
484
525
  * says, or to leave the link out.
485
526
  */
486
- export declare function detectUnrelatedScreenLinks(tree: AiDoctrineTree, outputKind: AiOutputKind, context?: Pick<AiDoctrineTreeContext, 'homeScreenIds'>): AiDoctrineViolation[];
527
+ export declare function detectUnrelatedScreenLinks(tree: AiDoctrineTree, outputKind: AiOutputKind, context?: Pick<AiDoctrineTreeContext, 'homeScreenIds' | 'formScreenIds'>): AiDoctrineViolation[];
487
528
  /**
488
529
  * Rule 10, for a link that goes nowhere (AGL-3072). A Button or a Screen Link
489
530
  * goes where its `screenId` or its `href` says, and those are the only
@@ -1841,20 +1841,23 @@ const GRID = 'muiGrid';
1841
1841
  ];
1842
1842
  }
1843
1843
  /** A link's words that say it goes home. */ const HOME_WORDS = /\bhome(?:\s?page)?\b/i;
1844
+ /** Words that ask a visitor to get in touch, which the site's form answers. */ const GET_IN_TOUCH_WORDS = /\b(?:contact|book|booking|call|quote|estimate|appointment|schedule|reserve|reservation|get in touch|request|visit|enquire|inquire|enquiry|inquiry|message|talk|consult|consultation|order|pre-?order|sign up|join|apply|start)\b/i;
1844
1845
  /**
1845
- * Rule 10, for a link sent where the site has nothing for it (AGL-3056). A
1846
- * link whose purpose the site has no screen for is left out; it never goes to
1847
- * a screen that does something else. The site inventory cannot say what each
1848
- * screen is for, but it can name the one a model falls back to: a live About
1849
- * page's footer sent "Request a Consultation" to the home screen, because the
1850
- * site had no consultation screen. So a Screen Link or a Button, outside the
1851
- * header and the navigation, that links the home screen with words that do
1852
- * not say home is refused, with a re-ask to link the screen that does what it
1853
- * says, or to leave the link out.
1854
- */ export function detectUnrelatedScreenLinks(tree, outputKind, context = {}) {
1855
- var _context_homeScreenIds;
1846
+ * Whether a link's words ask a visitor to get in touch — contact, book, quote,
1847
+ * request, order, pre-order, enquire — which the site's form answers
1848
+ * (AGL-3729). The layout language sends such a link to the form.
1849
+ */ export function aiAsksToGetInTouch(label) {
1850
+ return GET_IN_TOUCH_WORDS.test(label);
1851
+ }
1852
+ /**
1853
+ * The links rule 10 refuses as sent home for nothing (`detectUnrelatedScreenLinks`),
1854
+ * in document order. A link whose words ask a visitor to get in touch, sent to
1855
+ * a home page that places the site's form, does what it says (AGL-3729).
1856
+ */ function unrelatedScreenLinks(tree, outputKind, context) {
1857
+ var _context_homeScreenIds, _context_formScreenIds;
1856
1858
  if (outputKind === 'email' || outputKind === 'form') return [];
1857
1859
  const home = new Set((_context_homeScreenIds = context.homeScreenIds) != null ? _context_homeScreenIds : []);
1860
+ const forms = new Set((_context_formScreenIds = context.formScreenIds) != null ? _context_formScreenIds : []);
1858
1861
  const sent = [];
1859
1862
  for (const visit of walkTree(tree)){
1860
1863
  var _node_props, _node_props1, _node_props2;
@@ -1864,6 +1867,8 @@ const GRID = 'muiGrid';
1864
1867
  const linksHome = typeof screenId === 'string' && home.has(screenId) || ((_node_props1 = node.props) == null ? void 0 : _node_props1['href']) === '/';
1865
1868
  const label = typeof ((_node_props2 = node.props) == null ? void 0 : _node_props2['children']) === 'string' ? node.props['children'].trim() : '';
1866
1869
  if (!linksHome || !label || HOME_WORDS.test(label)) continue;
1870
+ // Home holds the site's form, and the words ask for what it does (AGL-3729).
1871
+ if (typeof screenId === 'string' && forms.has(screenId) && aiAsksToGetInTouch(label)) continue;
1867
1872
  // The header and the navigation link home by name and by the site's own brand.
1868
1873
  const inNavigation = hasAncestor(tree, visit, (ancestor)=>ancestor.componentId === 'muiAppBar' || [
1869
1874
  'header',
@@ -1874,6 +1879,88 @@ const GRID = 'muiGrid';
1874
1879
  label
1875
1880
  });
1876
1881
  }
1882
+ return sent;
1883
+ }
1884
+ /**
1885
+ * RULE 10'S LINK SENT HOME, REPAIRED IN CODE (AGL-3729).
1886
+ *
1887
+ * The rule's own advice is "link the page that does, or leave the link out",
1888
+ * and both halves are something code can do once it knows where the site's
1889
+ * form is. A Free guided start's café (beta.243, 2026-10-12) put its contact
1890
+ * and pre-order form on Home; its Menu page's "Request weekend pickup" was
1891
+ * sent to Home, and the page was refused on every answer, since the
1892
+ * destination was the compiler's, not the model's. So, before the check reads
1893
+ * the tree, each link `detectUnrelatedScreenLinks` would refuse is:
1894
+ *
1895
+ * - repointed to the page that places the site's form, when its words ask a
1896
+ * visitor to get in touch and the site has such a page that is not the page
1897
+ * being checked (`formScreenIds`, `ownScreenIds`). The platform reaches a
1898
+ * section of another page by no anchor (AGL-2867), so the link names the
1899
+ * page; a link to a section of its own page is the Scroll to element
1900
+ * interaction, which this never writes;
1901
+ * - otherwise left out: the node and everything under it are taken off the
1902
+ * page, and out of its parent's children.
1903
+ *
1904
+ * The root is never taken out. A tree with nothing to repair comes back as
1905
+ * the same object; a node that is changed is copied, never edited in place.
1906
+ */ export function aiSettleUnrelatedScreenLinks(tree, outputKind, context = {}) {
1907
+ var _context_ownScreenIds, _find, _context_formScreenIds;
1908
+ const sent = unrelatedScreenLinks(tree, outputKind, context);
1909
+ if (!sent.length) return tree;
1910
+ const own = new Set((_context_ownScreenIds = context.ownScreenIds) != null ? _context_ownScreenIds : []);
1911
+ const formScreen = (_find = ((_context_formScreenIds = context.formScreenIds) != null ? _context_formScreenIds : []).find((id)=>!own.has(id))) != null ? _find : null;
1912
+ const nodes = _extends({}, tree.nodes);
1913
+ const parents = new Map();
1914
+ for (const visit of walkTree(tree)){
1915
+ const parent = visit.ancestors[visit.ancestors.length - 1];
1916
+ if (parent !== undefined) parents.set(visit.id, parent);
1917
+ }
1918
+ for (const { id, label } of sent){
1919
+ var _nodes_parent_nodes;
1920
+ if (!nodes[id]) continue;
1921
+ if (formScreen && aiAsksToGetInTouch(label)) {
1922
+ var _nodes_id_props;
1923
+ const props = _extends({}, (_nodes_id_props = nodes[id].props) != null ? _nodes_id_props : {}, {
1924
+ screenId: formScreen
1925
+ });
1926
+ delete props['href'];
1927
+ nodes[id] = _extends({}, nodes[id], {
1928
+ props
1929
+ });
1930
+ continue;
1931
+ }
1932
+ const parent = parents.get(id);
1933
+ if (parent === undefined || !nodes[parent]) continue;
1934
+ nodes[parent] = _extends({}, nodes[parent], {
1935
+ nodes: ((_nodes_parent_nodes = nodes[parent].nodes) != null ? _nodes_parent_nodes : []).filter((child)=>child !== id)
1936
+ });
1937
+ const stack = [
1938
+ id
1939
+ ];
1940
+ while(stack.length){
1941
+ var _ref;
1942
+ var _nodes_next;
1943
+ const next = stack.pop();
1944
+ stack.push(...(_ref = (_nodes_next = nodes[next]) == null ? void 0 : _nodes_next.nodes) != null ? _ref : []);
1945
+ delete nodes[next];
1946
+ }
1947
+ }
1948
+ return _extends({}, tree, {
1949
+ nodes
1950
+ });
1951
+ }
1952
+ /**
1953
+ * Rule 10, for a link sent where the site has nothing for it (AGL-3056). A
1954
+ * link whose purpose the site has no screen for is left out; it never goes to
1955
+ * a screen that does something else. The site inventory cannot say what each
1956
+ * screen is for, but it can name the one a model falls back to: a live About
1957
+ * page's footer sent "Request a Consultation" to the home screen, because the
1958
+ * site had no consultation screen. So a Screen Link or a Button, outside the
1959
+ * header and the navigation, that links the home screen with words that do
1960
+ * not say home is refused, with a re-ask to link the screen that does what it
1961
+ * says, or to leave the link out.
1962
+ */ export function detectUnrelatedScreenLinks(tree, outputKind, context = {}) {
1963
+ const sent = unrelatedScreenLinks(tree, outputKind, context);
1877
1964
  if (!sent.length) return [];
1878
1965
  return [
1879
1966
  {