@aglyn/plugins-ai 1.0.0-beta.271 → 1.0.0-beta.273
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +17 -17
- package/src/lib/activity/ai-activity-actions.d.ts +4 -3
- package/src/lib/activity/ai-activity-actions.js +3 -0
- package/src/lib/activity/ai-activity-actions.js.map +1 -1
- package/src/lib/components/ai-site-build-page.component.js +18 -0
- package/src/lib/components/ai-site-build-page.component.js.map +1 -1
- package/src/lib/components/ai-site-import-job.component.d.ts +41 -0
- package/src/lib/components/ai-site-import-job.component.js +707 -0
- package/src/lib/components/ai-site-import-job.component.js.map +1 -0
- package/src/lib/components/ai-site-import-start.component.d.ts +64 -0
- package/src/lib/components/ai-site-import-start.component.js +335 -0
- package/src/lib/components/ai-site-import-start.component.js.map +1 -0
- package/src/lib/components/ai-site-ownership-proof.component.d.ts +42 -0
- package/src/lib/components/ai-site-ownership-proof.component.js +353 -0
- package/src/lib/components/ai-site-ownership-proof.component.js.map +1 -0
- package/src/lib/components/ai-site-start-card.component.js +329 -252
- package/src/lib/components/ai-site-start-card.component.js.map +1 -1
- package/src/lib/jobs/ai-empty-site-sweep.d.ts +12 -1
- package/src/lib/jobs/ai-empty-site-sweep.js +15 -3
- package/src/lib/jobs/ai-empty-site-sweep.js.map +1 -1
- package/src/lib/jobs/ai-job-drafts.d.ts +14 -0
- package/src/lib/jobs/ai-job-drafts.js +24 -0
- package/src/lib/jobs/ai-job-drafts.js.map +1 -1
- package/src/lib/jobs/ai-job-import-step.d.ts +136 -0
- package/src/lib/jobs/ai-job-import-step.js +809 -0
- package/src/lib/jobs/ai-job-import-step.js.map +1 -0
- package/src/lib/jobs/ai-job-page-language.d.ts +39 -1
- package/src/lib/jobs/ai-job-page-language.js +62 -4
- package/src/lib/jobs/ai-job-page-language.js.map +1 -1
- package/src/lib/jobs/ai-job-page-step.d.ts +2 -2
- package/src/lib/jobs/ai-job-page-step.js +26 -4
- package/src/lib/jobs/ai-job-page-step.js.map +1 -1
- package/src/lib/jobs/ai-job-seo-step.js +8 -4
- package/src/lib/jobs/ai-job-seo-step.js.map +1 -1
- package/src/lib/jobs/ai-job-site-datasets.d.ts +39 -1
- package/src/lib/jobs/ai-job-site-datasets.js +111 -15
- package/src/lib/jobs/ai-job-site-datasets.js.map +1 -1
- package/src/lib/jobs/ai-job-site-step.d.ts +12 -3
- package/src/lib/jobs/ai-job-site-step.js +85 -11
- package/src/lib/jobs/ai-job-site-step.js.map +1 -1
- package/src/lib/jobs/ai-job-text-step.d.ts +15 -0
- package/src/lib/jobs/ai-job-text-step.js.map +1 -1
- package/src/lib/jobs/ai-jobs.d.ts +8 -0
- package/src/lib/jobs/ai-jobs.js +19 -3
- package/src/lib/jobs/ai-jobs.js.map +1 -1
- package/src/lib/jobs/ai-layout-owner-photos.d.ts +16 -1
- package/src/lib/jobs/ai-layout-owner-photos.js +49 -2
- package/src/lib/jobs/ai-layout-owner-photos.js.map +1 -1
- package/src/lib/jobs/ai-layout-stock-photos.d.ts +35 -0
- package/src/lib/jobs/ai-layout-stock-photos.js +114 -43
- package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
- package/src/lib/jobs/ai-site-late-photos.d.ts +143 -0
- package/src/lib/jobs/ai-site-late-photos.js +486 -0
- package/src/lib/jobs/ai-site-late-photos.js.map +1 -0
- package/src/lib/jobs/ai-site-publish.d.ts +18 -0
- package/src/lib/jobs/ai-site-publish.js +123 -5
- package/src/lib/jobs/ai-site-publish.js.map +1 -1
- package/src/lib/jobs/ai-theme-brand-inputs.d.ts +61 -3
- package/src/lib/jobs/ai-theme-brand-inputs.js +117 -27
- package/src/lib/jobs/ai-theme-brand-inputs.js.map +1 -1
- package/src/lib/jobs/fixtures/ai-site-late-photos-laurel-fork.d.ts +83 -0
- package/src/lib/jobs/fixtures/ai-site-late-photos-laurel-fork.js +1553 -0
- package/src/lib/jobs/fixtures/ai-site-late-photos-laurel-fork.js.map +1 -0
- package/src/lib/layout-language/ai-layout-pictures.d.ts +50 -13
- package/src/lib/layout-language/ai-layout-pictures.js +81 -19
- package/src/lib/layout-language/ai-layout-pictures.js.map +1 -1
- package/src/lib/model/ai-credit-estimate.d.ts +20 -0
- package/src/lib/model/ai-credit-estimate.js +13 -0
- package/src/lib/model/ai-credit-estimate.js.map +1 -1
- package/src/lib/model/ai-job-activity.d.ts +2 -2
- package/src/lib/model/ai-job-activity.js +6 -4
- package/src/lib/model/ai-job-activity.js.map +1 -1
- package/src/lib/model/ai-job-notice.js +18 -4
- package/src/lib/model/ai-job-notice.js.map +1 -1
- package/src/lib/model/ai-jobs.types.d.ts +39 -2
- package/src/lib/model/ai-jobs.types.js +2 -1
- package/src/lib/model/ai-jobs.types.js.map +1 -1
- package/src/lib/model/ai-site-import-view.d.ts +130 -0
- package/src/lib/model/ai-site-import-view.js +182 -0
- package/src/lib/model/ai-site-import-view.js.map +1 -0
- package/src/lib/model/ai-site-import.d.ts +302 -0
- package/src/lib/model/ai-site-import.js +526 -0
- package/src/lib/model/ai-site-import.js.map +1 -0
- package/src/lib/model/ai-site-photos.d.ts +119 -0
- package/src/lib/model/ai-site-photos.js +128 -0
- package/src/lib/model/ai-site-photos.js.map +1 -0
- package/src/lib/model/ai-site-start.d.ts +22 -9
- package/src/lib/model/ai-site-start.js +64 -23
- package/src/lib/model/ai-site-start.js.map +1 -1
- package/src/lib/runtime/ai-doctrine-validators.d.ts +3 -1
- package/src/lib/runtime/ai-doctrine-validators.js +3 -1
- package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
- package/src/lib/runtime/seo-fields.d.ts +29 -4
- package/src/lib/runtime/seo-fields.js +58 -11
- package/src/lib/runtime/seo-fields.js.map +1 -1
- package/src/lib/server/ai-jobs-route.js +11 -0
- package/src/lib/server/ai-jobs-route.js.map +1 -1
- package/src/lib/server/ai-site-import-route.d.ts +89 -0
- package/src/lib/server/ai-site-import-route.js +356 -0
- package/src/lib/server/ai-site-import-route.js.map +1 -0
- package/src/lib/server.js +14 -0
- package/src/lib/server.js.map +1 -1
- package/src/lib/site-import/fixtures/site-import-crawl-recordings.d.ts +45 -0
- package/src/lib/site-import/fixtures/site-import-crawl-recordings.js +313 -0
- package/src/lib/site-import/fixtures/site-import-crawl-recordings.js.map +1 -0
- package/src/lib/site-import/fixtures/site-import-wave-recordings.d.ts +50 -0
- package/src/lib/site-import/fixtures/site-import-wave-recordings.js +217 -0
- package/src/lib/site-import/fixtures/site-import-wave-recordings.js.map +1 -0
- package/src/lib/site-import/site-import-crawl.d.ts +163 -0
- package/src/lib/site-import/site-import-crawl.js +467 -0
- package/src/lib/site-import/site-import-crawl.js.map +1 -0
- package/src/lib/site-import/site-import-flag.d.ts +23 -0
- package/src/lib/site-import/site-import-flag.js +23 -0
- package/src/lib/site-import/site-import-flag.js.map +1 -0
- package/src/lib/site-import/site-import-html.d.ts +94 -0
- package/src/lib/site-import/site-import-html.js +462 -0
- package/src/lib/site-import/site-import-html.js.map +1 -0
- package/src/lib/site-import/site-import-images.d.ts +54 -0
- package/src/lib/site-import/site-import-images.js +204 -0
- package/src/lib/site-import/site-import-images.js.map +1 -0
- package/src/lib/site-import/site-import-page.d.ts +48 -0
- package/src/lib/site-import/site-import-page.js +150 -0
- package/src/lib/site-import/site-import-page.js.map +1 -0
- package/src/lib/site-import/site-import-review.d.ts +40 -0
- package/src/lib/site-import/site-import-review.js +149 -0
- package/src/lib/site-import/site-import-review.js.map +1 -0
- package/src/lib/site-import/site-import-robots.d.ts +52 -0
- package/src/lib/site-import/site-import-robots.js +126 -0
- package/src/lib/site-import/site-import-robots.js.map +1 -0
- package/src/lib/tools/ai-seo-tool.d.ts +25 -1
- package/src/lib/tools/ai-seo-tool.js +61 -0
- package/src/lib/tools/ai-seo-tool.js.map +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/tools/ai-seo-tool.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 { MEDIA_ALT_MAX_LENGTH } from '@aglyn/aglyn/app-utils/media-alt'\nimport type { SeoFindingCode } from '@aglyn/aglyn/app-utils/seo-audit'\nimport { seoKeywordCount } from '@aglyn/aglyn/app-utils/seo-keywords'\nimport {\n SEO_LISTING_FIELDS,\n type SeoListingFieldKey,\n} from '@aglyn/aglyn/app-utils/seo-listing-fields'\nimport type { SeoPageFacts } from '@aglyn/aglyn/app-utils/seo-page-facts'\nimport { HostEntityType } from '@aglyn/aglyn/foundation/definitions/platform.types'\nimport type { AiSeoFieldValues, AiSeoSiteFormField } from '../model/ai-seo'\nimport type { AiTool } from '../providers/contract'\nimport { AI_TEXT_LIMITS } from '../runtime/ai-palette'\nimport type { AiDoctrineViolation } from '../runtime/ai-doctrine-validators'\n\n/**\n * The strict tools SEO by AI answers through (AGL-2910), and the checks that\n * hold each answer to the editor it proposes for.\n *\n * - `propose_search_listing` — one page's or one product's listing. BUILT\n * from the editor's own field list, the `seo-listing-fields` catalog the\n * SEO card draws its inputs from, so a field and its length are said once:\n * `ai-seo-tool.spec.ts` asserts the parity in both directions.\n * - `propose_seo_fixes` — one batch of a site audit's page fixes.\n * - `propose_site_listing` — the site's structured data and its guidance\n * for AI agents, the two fields `/llms.txt` leads with.\n *\n * A strict schema forbids extra keys, requires every key and says \"no value\"\n * with `null`; it accepts no length keyword, so every length is stated in a\n * description and enforced here. A check returns a value only when nothing\n * in the answer is broken, and otherwise the violations and the parts at\n * fault, for the generation call's one re-ask.\n */\n\n/* ------------------------------------------------------------------------ *\n * Keywords: what stuffing looks like. Coverage — whether a page says a\n * keyword at all — is the platform's (`app-utils/seo-keywords`).\n * ------------------------------------------------------------------------ */\n\n/** The most times one keyword may appear, per field, before it reads as stuffing. */\nexport const AI_SEO_KEYWORD_LIMITS: Readonly<Partial<Record<SeoListingFieldKey, number>>> = {\n title: 1,\n description: 2,\n breadcrumb: 1,\n}\n\n/** A word of four letters or more used three times in one short field reads as stuffing. */\nfunction repeatedWord(text: string): string | null {\n const counts = new Map<string, number>()\n for (const word of text.toLowerCase().match(/[\\p{L}\\p{N}]{4,}/gu) ?? []) {\n const count = (counts.get(word) ?? 0) + 1\n if (count >= 3) return word\n counts.set(word, count)\n }\n return null\n}\n\nconst collapse = (value: unknown): string =>\n typeof value === 'string' ? value.replace(/\\s+/g, ' ').trim() : ''\n\nconst text = (description: string) => ({ anyOf: [{ type: 'string' }, { type: 'null' }], description })\n\n/* ------------------------------------------------------------------------ *\n * One listing, from the editor's field list\n * ------------------------------------------------------------------------ */\n\nexport const AI_SEO_FIELDS_TOOL_NAME = 'propose_search_listing'\n\n/** The fields a listing proposal must fill; the image description may be left out. */\nexport const AI_SEO_REQUIRED_FIELDS: ReadonlySet<SeoListingFieldKey> = new Set([\n 'title',\n 'description',\n 'breadcrumb',\n])\n\n/** Fields in the catalog's order, whatever order a caller named them in. */\nexport function orderedSeoListingFields(fields: readonly SeoListingFieldKey[]): SeoListingFieldKey[] {\n const wanted = new Set(fields)\n return (Object.keys(SEO_LISTING_FIELDS) as SeoListingFieldKey[]).filter((key) => wanted.has(key))\n}\n\n/** The strict tool for a listing with exactly these fields, each required and nullable. */\nexport function aiSeoFieldsTool(fields: readonly SeoListingFieldKey[]): AiTool {\n const keys = orderedSeoListingFields(fields)\n const properties: Record<string, unknown> = {}\n for (const key of keys) {\n const field = SEO_LISTING_FIELDS[key]\n properties[key] = text(\n `${field.label}. ${field.purpose} At most ${field.maxLength} characters.` +\n (AI_SEO_REQUIRED_FIELDS.has(key) ? '' : ' null when nothing you were given says what the picture shows.'),\n )\n }\n return {\n name: AI_SEO_FIELDS_TOOL_NAME,\n description:\n 'Propose the search listing for one page or one product. Every field is required; ' +\n 'null means no value can be written from what you were given.',\n strict: true,\n inputSchema: {\n type: 'object',\n properties,\n required: keys,\n additionalProperties: false,\n },\n }\n}\n\nexport interface AiSeoFieldsCheckContext {\n fields: readonly SeoListingFieldKey[]\n /** Whether the listing has a social image; without one, an image description is dropped. */\n hasImage: boolean\n keywords?: readonly string[]\n /** Titles other pages of the site use, which this listing must not repeat. */\n otherTitles?: readonly string[]\n}\n\nconst normalizeTitle = (value: string): string => value.replace(/\\s+/g, ' ').trim().toLowerCase()\n\n/**\n * A listing answer held to the editor: every requested text field written,\n * each within its length, no keyword past its per-field count, no word\n * repeated to rank, no title another page uses.\n */\nexport function checkAiSeoFields(\n answer: Record<string, unknown>,\n context: AiSeoFieldsCheckContext,\n): { value: AiSeoFieldValues | null; violations: AiDoctrineViolation[]; offending?: Record<string, unknown> } {\n const values: AiSeoFieldValues = {}\n const violations: AiDoctrineViolation[] = []\n const offending: Record<string, unknown> = {}\n const otherTitles = new Set((context.otherTitles ?? []).map(normalizeTitle))\n for (const key of orderedSeoListingFields(context.fields)) {\n const field = SEO_LISTING_FIELDS[key]\n const raw = answer[key]\n if (raw !== null && raw !== undefined && typeof raw !== 'string') {\n violations.push({ rule: null, code: 'type', message: `${field.label} must be text or null.` })\n offending[key] = raw\n continue\n }\n const value = collapse(raw)\n if (!value) {\n if (AI_SEO_REQUIRED_FIELDS.has(key)) {\n violations.push({ rule: null, code: 'missing', message: `${field.label} is required.` })\n }\n continue\n }\n // An image description with no image to describe is a sentence about\n // nothing; it is left out rather than asked for again.\n if (key === 'imageAlt' && !context.hasImage) continue\n if (value.length > field.maxLength) {\n violations.push({\n rule: null,\n code: 'too-long',\n message: `${field.label} is ${value.length} characters; the limit is ${field.maxLength}.`,\n })\n offending[key] = value\n continue\n }\n const limit = AI_SEO_KEYWORD_LIMITS[key]\n const stuffed = limit\n ? (context.keywords ?? []).find((keyword) => seoKeywordCount(value, keyword) > limit)\n : undefined\n if (stuffed) {\n violations.push({\n rule: null,\n code: 'keyword-stuffing',\n message: `${field.label} repeats \"${stuffed}\"; use a keyword at most ${limit === 1 ? 'once' : `${limit} times`} there.`,\n })\n offending[key] = value\n continue\n }\n const repeated = key === 'imageAlt' ? null : repeatedWord(value)\n if (repeated) {\n violations.push({ rule: null, code: 'repetition', message: `${field.label} repeats \"${repeated}\" three times.` })\n offending[key] = value\n continue\n }\n if (key === 'title' && otherTitles.has(normalizeTitle(value))) {\n violations.push({\n rule: null,\n code: 'duplicate-title',\n message: `The title \"${value}\" is already another page's title.`,\n })\n offending[key] = value\n continue\n }\n values[key] = value\n }\n return violations.length ? { value: null, violations, offending } : { value: values, violations: [] }\n}\n\n/* ------------------------------------------------------------------------ *\n * A batch of a site audit's page fixes\n * ------------------------------------------------------------------------ */\n\nexport const AI_SEO_FIXES_TOOL_NAME = 'propose_seo_fixes'\n\n/**\n * How long an image description a batch proposes may be. Shorter than the\n * `alt` ceiling: a description read aloud is better short, and it keeps the\n * largest batch answer inside its measured budget.\n */\nexport const AI_SEO_FIX_ALT_MAX_CHARS = Math.min(150, MEDIA_ALT_MAX_LENGTH)\n\n/** Undescribed images one page of a batch gets descriptions for; the rest wait for the next audit. */\nexport const AI_SEO_FIX_IMAGES_PER_PAGE = 3\n\n/**\n * The undescribed images a batch asks about for a page, in page order: only\n * those the page itself holds. An image a component or a repeat renders is\n * described in the component or the rows, never by a fix to the page.\n */\nexport function aiSeoFixImages(page: Pick<AiSeoBatchPage, 'facts'>): SeoPageFacts['imagesMissingAlt'] {\n return page.facts.imagesMissingAlt.filter((image) => image.editable).slice(0, AI_SEO_FIX_IMAGES_PER_PAGE)\n}\n\n/** The strict tool for one batch: a title, description, heading and image descriptions per page. */\nexport function aiSeoFixesTool(screenIds: readonly string[]): AiTool {\n return {\n name: AI_SEO_FIXES_TOOL_NAME,\n description:\n 'Propose fixes for the pages you were given. One entry per page; null means leave that value as it is.',\n strict: true,\n inputSchema: {\n type: 'object',\n properties: {\n pages: {\n type: 'array',\n items: {\n type: 'object',\n properties: {\n screenId: { type: 'string', enum: [...screenIds] },\n title: text(`${SEO_LISTING_FIELDS.title.purpose} At most ${SEO_LISTING_FIELDS.title.maxLength} characters.`),\n description: text(\n `${SEO_LISTING_FIELDS.description.purpose} At most ${SEO_LISTING_FIELDS.description.maxLength} characters.`,\n ),\n h1: text(`The page’s one main heading. At most ${AI_TEXT_LIMITS.headline} characters.`),\n imageAlts: {\n type: 'array',\n items: {\n type: 'object',\n properties: {\n nodeId: { type: 'string' },\n alt: {\n type: 'string',\n description: `What the picture shows. At most ${AI_SEO_FIX_ALT_MAX_CHARS} characters.`,\n },\n },\n required: ['nodeId', 'alt'],\n additionalProperties: false,\n },\n },\n },\n required: ['screenId', 'title', 'description', 'h1', 'imageAlts'],\n additionalProperties: false,\n },\n },\n },\n required: ['pages'],\n additionalProperties: false,\n },\n }\n}\n\n/** A batch page as the fixes prompt and check read it. */\nexport interface AiSeoBatchPage {\n screenId: string\n path: string\n name: string\n codes: ReadonlySet<SeoFindingCode>\n keywords: readonly string[]\n seo: { title?: string; description?: string; breadcrumb?: string; image?: string; imageAlt?: string } | undefined\n facts: SeoPageFacts\n}\n\n/** The findings that let a batch answer propose each value. */\nexport const AI_SEO_FIX_CODES = {\n title: ['title-missing', 'title-too-long', 'title-duplicate', 'keyword-missing'],\n description: ['description-missing', 'description-too-long', 'description-duplicate', 'keyword-missing'],\n h1: ['h1-missing', 'h1-thin'],\n} as const satisfies Record<string, readonly SeoFindingCode[]>\n\nexport interface AiSeoBatchAnswer {\n title: string | null\n description: string | null\n h1: string | null\n imageAlts: Array<{ nodeId: string; alt: string }>\n}\n\n/**\n * A batch answer held to the audit and the editor: values only where a\n * finding asked for them, each within its length, no keyword past its count,\n * no title another page uses, and image descriptions only for this page's\n * undescribed images.\n */\nexport function checkAiSeoFixes(\n answer: Record<string, unknown>,\n pages: readonly AiSeoBatchPage[],\n titlesElsewhere: readonly string[],\n): { value: Record<string, AiSeoBatchAnswer> | null; violations: AiDoctrineViolation[]; offending?: Record<string, unknown> } {\n const byId = new Map(pages.map((page) => [page.screenId, page]))\n const violations: AiDoctrineViolation[] = []\n const offending: Array<Record<string, unknown>> = []\n const value: Record<string, AiSeoBatchAnswer> = {}\n const taken = new Set(titlesElsewhere.map(normalizeTitle))\n const entries = Array.isArray(answer['pages']) ? (answer['pages'] as unknown[]) : null\n if (!entries) {\n return { value: null, violations: [{ rule: null, code: 'shape', message: 'The answer must list the pages.' }] }\n }\n const asks = (page: AiSeoBatchPage, codes: readonly SeoFindingCode[]) =>\n codes.some((code) => page.codes.has(code))\n for (const raw of entries) {\n const entry = (raw ?? {}) as Record<string, unknown>\n const page = byId.get(String(entry['screenId'] ?? ''))\n if (!page || value[page.screenId]) continue\n const broken: string[] = []\n const fix: AiSeoBatchAnswer = { title: null, description: null, h1: null, imageAlts: [] }\n\n const title = collapse(entry['title'])\n if (title && asks(page, AI_SEO_FIX_CODES.title)) {\n if (title.length > SEO_LISTING_FIELDS.title.maxLength) {\n broken.push(`title is ${title.length} characters; the limit is ${SEO_LISTING_FIELDS.title.maxLength}`)\n } else if (page.keywords.some((keyword) => seoKeywordCount(title, keyword) > (AI_SEO_KEYWORD_LIMITS.title ?? 1))) {\n broken.push('title repeats a keyword')\n } else if (taken.has(normalizeTitle(title))) {\n broken.push(`title \"${title}\" is another page's title`)\n } else {\n fix.title = title\n taken.add(normalizeTitle(title))\n }\n }\n const description = collapse(entry['description'])\n if (description && asks(page, AI_SEO_FIX_CODES.description)) {\n if (description.length > SEO_LISTING_FIELDS.description.maxLength) {\n broken.push(\n `description is ${description.length} characters; the limit is ${SEO_LISTING_FIELDS.description.maxLength}`,\n )\n } else if (\n page.keywords.some((keyword) => seoKeywordCount(description, keyword) > (AI_SEO_KEYWORD_LIMITS.description ?? 2))\n ) {\n broken.push('description repeats a keyword')\n } else {\n fix.description = description\n }\n }\n const h1 = collapse(entry['h1'])\n if (h1 && asks(page, AI_SEO_FIX_CODES.h1)) {\n if (h1.length > AI_TEXT_LIMITS.headline) {\n broken.push(`h1 is ${h1.length} characters; the limit is ${AI_TEXT_LIMITS.headline}`)\n } else {\n fix.h1 = h1\n }\n }\n const undescribed = new Set(aiSeoFixImages(page).map((image) => image.nodeId))\n for (const rawAlt of Array.isArray(entry['imageAlts']) ? (entry['imageAlts'] as unknown[]) : []) {\n const altEntry = (rawAlt ?? {}) as Record<string, unknown>\n const nodeId = String(altEntry['nodeId'] ?? '')\n const alt = collapse(altEntry['alt'])\n if (!undescribed.has(nodeId) || !alt || fix.imageAlts.some((known) => known.nodeId === nodeId)) continue\n if (alt.length > AI_SEO_FIX_ALT_MAX_CHARS) {\n broken.push(`the description of image ${nodeId} is over ${AI_SEO_FIX_ALT_MAX_CHARS} characters`)\n } else {\n fix.imageAlts.push({ nodeId, alt })\n }\n }\n if (broken.length) {\n violations.push(...broken.map((message) => ({ rule: null, code: 'fix', message: `Page ${page.screenId}: ${message}.` })))\n offending.push(entry)\n } else {\n value[page.screenId] = fix\n }\n }\n return violations.length ? { value: null, violations, offending: { pages: offending } } : { value, violations: [] }\n}\n\n/* ------------------------------------------------------------------------ *\n * The site: structured data and guidance for AI agents\n * ------------------------------------------------------------------------ */\n\nexport const AI_SEO_SITE_TOOL_NAME = 'propose_site_listing'\n\n/** The longest structured-data description the site SEO form accepts. */\nexport const AI_SEO_ENTITY_DESCRIPTION_MAX_CHARS = 300\n\n/** The longest agent guidance the site SEO form accepts. */\nexport const AI_SEO_AGENT_GUIDANCE_MAX_CHARS = 1_000\n\n/** The entity types the tool offers, and the value the site SEO form stores for each. */\nexport const AI_SEO_ENTITY_TYPE_VALUES: Readonly<Record<string, string>> = {\n Organization: `${HostEntityType.ORGANIZATION}`,\n Person: `${HostEntityType.PERSON}`,\n}\n\n/** The site-wide proposal's strict tool: structured data and agent guidance, each nullable. */\nexport function aiSeoSiteTool(): AiTool {\n const properties = {\n entityType: {\n anyOf: [{ type: 'string', enum: Object.keys(AI_SEO_ENTITY_TYPE_VALUES) }, { type: 'null' }],\n description: 'Whether a company or a person publishes the site.',\n },\n entityName: text('The publisher’s name as the site gives it.'),\n entityDescription: text(\n `What the publisher IS, in one sentence — not what a page is about. At most ${AI_SEO_ENTITY_DESCRIPTION_MAX_CHARS} characters.`,\n ),\n contactEmail: text('A contact email address, only when a page of the site shows it.'),\n contactTelephone: text('A contact telephone number, only when a page of the site shows it.'),\n contactType: text('What that contact answers, such as customer support or sales.'),\n whenToUse: text(\n `For AI agents: the questions this site is the best source for, specifically. At most ${AI_SEO_AGENT_GUIDANCE_MAX_CHARS} characters.`,\n ),\n howToUse: text(\n `For AI agents: which pages answer what, and what not to rely on. At most ${AI_SEO_AGENT_GUIDANCE_MAX_CHARS} characters.`,\n ),\n }\n return {\n name: AI_SEO_SITE_TOOL_NAME,\n description:\n 'Propose the site’s structured data and its guidance for AI agents. Every field is required; null means leave it as it is.',\n strict: true,\n inputSchema: {\n type: 'object',\n properties,\n required: Object.keys(properties),\n additionalProperties: false,\n },\n }\n}\n\nexport interface AiSeoSiteCheckContext {\n /** The form fields that are blank on the site now; only these may be proposed. */\n blank: ReadonlySet<AiSeoSiteFormField>\n /** Every word the site's pages show, for the contact details to be found in. */\n siteText: string\n}\n\nconst EMAIL = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\n\n/**\n * The site proposal held to the form and to the site: lengths, and a contact\n * detail only when a page shows it. A detail no page shows is left out with\n * a note rather than asked for again — asking again invites composing one.\n */\nexport function checkAiSeoSite(\n answer: Record<string, unknown>,\n context: AiSeoSiteCheckContext,\n): {\n value: { values: Partial<Record<AiSeoSiteFormField, string>>; notes: string[] } | null\n violations: AiDoctrineViolation[]\n offending?: Record<string, unknown>\n} {\n const values: Partial<Record<AiSeoSiteFormField, string>> = {}\n const notes: string[] = []\n const violations: AiDoctrineViolation[] = []\n const offending: Record<string, unknown> = {}\n const put = (field: AiSeoSiteFormField, value: string) => {\n if (value && context.blank.has(field)) values[field] = value\n }\n\n const type = collapse(answer['entityType'])\n if (type && AI_SEO_ENTITY_TYPE_VALUES[type]) put('seo.entity.type', AI_SEO_ENTITY_TYPE_VALUES[type])\n put('seo.entity.name', collapse(answer['entityName']).slice(0, 200))\n\n const description = collapse(answer['entityDescription'])\n if (description.length > AI_SEO_ENTITY_DESCRIPTION_MAX_CHARS) {\n violations.push({\n rule: null,\n code: 'too-long',\n message: `The entity description is ${description.length} characters; the limit is ${AI_SEO_ENTITY_DESCRIPTION_MAX_CHARS}.`,\n })\n offending['entityDescription'] = description\n } else {\n put('seo.entity.description', description)\n }\n\n const siteText = context.siteText.toLowerCase()\n const siteDigits = context.siteText.replace(/\\D+/g, '')\n const email = collapse(answer['contactEmail'])\n if (email) {\n if (EMAIL.test(email) && siteText.includes(email.toLowerCase())) put('seo.entity.email', email)\n else notes.push('A contact email was left out: no page of the site shows it.')\n }\n const telephone = collapse(answer['contactTelephone'])\n if (telephone) {\n const digits = telephone.replace(/\\D+/g, '')\n if (digits.length >= 7 && siteDigits.includes(digits)) put('seo.entity.telephone', telephone)\n else notes.push('A contact telephone number was left out: no page of the site shows it.')\n }\n if (values['seo.entity.email'] || values['seo.entity.telephone']) {\n put('seo.entity.contactType', collapse(answer['contactType']).slice(0, 100))\n }\n\n for (const [key, field] of [\n ['whenToUse', 'seo.agent.whenToUse'],\n ['howToUse', 'seo.agent.howToUse'],\n ] as const) {\n const guidance = collapse(answer[key])\n if (guidance.length > AI_SEO_AGENT_GUIDANCE_MAX_CHARS) {\n violations.push({\n rule: null,\n code: 'too-long',\n message: `${key} is ${guidance.length} characters; the limit is ${AI_SEO_AGENT_GUIDANCE_MAX_CHARS}.`,\n })\n offending[key] = guidance\n } else {\n put(field, guidance)\n }\n }\n return violations.length ? { value: null, violations, offending } : { value: { values, notes }, violations: [] }\n}\n"],"names":["MEDIA_ALT_MAX_LENGTH","seoKeywordCount","SEO_LISTING_FIELDS","HostEntityType","AI_TEXT_LIMITS","AI_SEO_KEYWORD_LIMITS","title","description","breadcrumb","repeatedWord","text","counts","Map","word","toLowerCase","match","count","get","set","collapse","value","replace","trim","anyOf","type","AI_SEO_FIELDS_TOOL_NAME","AI_SEO_REQUIRED_FIELDS","Set","orderedSeoListingFields","fields","wanted","Object","keys","filter","key","has","aiSeoFieldsTool","properties","field","label","purpose","maxLength","name","strict","inputSchema","required","additionalProperties","normalizeTitle","checkAiSeoFields","answer","context","values","violations","offending","otherTitles","map","raw","undefined","push","rule","code","message","hasImage","length","limit","stuffed","keywords","find","keyword","repeated","AI_SEO_FIXES_TOOL_NAME","AI_SEO_FIX_ALT_MAX_CHARS","Math","min","AI_SEO_FIX_IMAGES_PER_PAGE","aiSeoFixImages","page","facts","imagesMissingAlt","image","editable","slice","aiSeoFixesTool","screenIds","pages","items","screenId","enum","h1","headline","imageAlts","nodeId","alt","AI_SEO_FIX_CODES","checkAiSeoFixes","titlesElsewhere","byId","taken","entries","Array","isArray","asks","codes","some","entry","String","broken","fix","add","undescribed","rawAlt","altEntry","known","AI_SEO_SITE_TOOL_NAME","AI_SEO_ENTITY_DESCRIPTION_MAX_CHARS","AI_SEO_AGENT_GUIDANCE_MAX_CHARS","AI_SEO_ENTITY_TYPE_VALUES","Organization","ORGANIZATION","Person","PERSON","aiSeoSiteTool","entityType","entityName","entityDescription","contactEmail","contactTelephone","contactType","whenToUse","howToUse","EMAIL","checkAiSeoSite","notes","put","blank","siteText","siteDigits","email","test","includes","telephone","digits","guidance"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/tools/ai-seo-tool.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 { MEDIA_ALT_MAX_LENGTH } from '@aglyn/aglyn/app-utils/media-alt'\nimport type { SeoFindingCode } from '@aglyn/aglyn/app-utils/seo-audit'\nimport { seoKeywordCount } from '@aglyn/aglyn/app-utils/seo-keywords'\nimport {\n SEO_LISTING_FIELDS,\n type SeoListingFieldKey,\n} from '@aglyn/aglyn/app-utils/seo-listing-fields'\nimport type { SeoPageFacts } from '@aglyn/aglyn/app-utils/seo-page-facts'\nimport { HostEntityType } from '@aglyn/aglyn/foundation/definitions/platform.types'\nimport type { AiSeoFieldValues, AiSeoSiteFormField } from '../model/ai-seo'\nimport type { AiTool } from '../providers/contract'\nimport { AI_TEXT_LIMITS } from '../runtime/ai-palette'\nimport { aiClipAtWord, type AiDoctrineViolation } from '../runtime/ai-doctrine-validators'\n\n/**\n * The strict tools SEO by AI answers through (AGL-2910), and the checks that\n * hold each answer to the editor it proposes for.\n *\n * - `propose_search_listing` — one page's or one product's listing. BUILT\n * from the editor's own field list, the `seo-listing-fields` catalog the\n * SEO card draws its inputs from, so a field and its length are said once:\n * `ai-seo-tool.spec.ts` asserts the parity in both directions.\n * - `propose_seo_fixes` — one batch of a site audit's page fixes.\n * - `propose_site_listing` — the site's structured data and its guidance\n * for AI agents, the two fields `/llms.txt` leads with.\n *\n * A strict schema forbids extra keys, requires every key and says \"no value\"\n * with `null`; it accepts no length keyword, so every length is stated in a\n * description and enforced here. A check returns a value only when nothing\n * in the answer is broken, and otherwise the violations and the parts at\n * fault, for the generation call's one re-ask.\n */\n\n/* ------------------------------------------------------------------------ *\n * Keywords: what stuffing looks like. Coverage — whether a page says a\n * keyword at all — is the platform's (`app-utils/seo-keywords`).\n * ------------------------------------------------------------------------ */\n\n/** The most times one keyword may appear, per field, before it reads as stuffing. */\nexport const AI_SEO_KEYWORD_LIMITS: Readonly<Partial<Record<SeoListingFieldKey, number>>> = {\n title: 1,\n description: 2,\n breadcrumb: 1,\n}\n\n/** A word of four letters or more used three times in one short field reads as stuffing. */\nfunction repeatedWord(text: string): string | null {\n const counts = new Map<string, number>()\n for (const word of text.toLowerCase().match(/[\\p{L}\\p{N}]{4,}/gu) ?? []) {\n const count = (counts.get(word) ?? 0) + 1\n if (count >= 3) return word\n counts.set(word, count)\n }\n return null\n}\n\nconst collapse = (value: unknown): string =>\n typeof value === 'string' ? value.replace(/\\s+/g, ' ').trim() : ''\n\nconst text = (description: string) => ({ anyOf: [{ type: 'string' }, { type: 'null' }], description })\n\n/* ------------------------------------------------------------------------ *\n * Fitting an answer that ran past a length (AGL-4016)\n * ------------------------------------------------------------------------ */\n\n/**\n * One text held to `max` characters, cut the way the page step cuts the\n * listing it stores (`aiClipAtWord`, AGL-3813): at a word, with no joining\n * mark or dangling word left at the end.\n *\n * The model is told every length and re-asked once with the count it ran\n * past, and a fast model still overruns by a few characters: counting\n * characters is the one thing it reliably does badly. MindFullMe's listing\n * failed on 2026-10-20 over a 65-character title, twice. So an answer whose\n * only fault is a length is fitted rather than thrown away.\n */\nexport function aiFitSeoText(value: string, max: number): string {\n return aiClipAtWord(collapse(value), max)\n}\n\n/**\n * A listing answer with every text field past its length fitted to it, or\n * `null` when no field ran over. Nothing else in the answer changes; the\n * caller runs `checkAiSeoFields` on the result again.\n */\nexport function fitAiSeoFieldLengths(\n answer: Record<string, unknown>,\n fields: readonly SeoListingFieldKey[],\n): Record<string, unknown> | null {\n let fitted: Record<string, unknown> | null = null\n for (const key of orderedSeoListingFields(fields)) {\n const raw = answer[key]\n const { maxLength } = SEO_LISTING_FIELDS[key]\n if (typeof raw !== 'string' || collapse(raw).length <= maxLength) continue\n fitted ??= { ...answer }\n fitted[key] = aiFitSeoText(raw, maxLength)\n }\n return fitted\n}\n\n/* ------------------------------------------------------------------------ *\n * One listing, from the editor's field list\n * ------------------------------------------------------------------------ */\n\nexport const AI_SEO_FIELDS_TOOL_NAME = 'propose_search_listing'\n\n/** The fields a listing proposal must fill; the image description may be left out. */\nexport const AI_SEO_REQUIRED_FIELDS: ReadonlySet<SeoListingFieldKey> = new Set([\n 'title',\n 'description',\n 'breadcrumb',\n])\n\n/** Fields in the catalog's order, whatever order a caller named them in. */\nexport function orderedSeoListingFields(fields: readonly SeoListingFieldKey[]): SeoListingFieldKey[] {\n const wanted = new Set(fields)\n return (Object.keys(SEO_LISTING_FIELDS) as SeoListingFieldKey[]).filter((key) => wanted.has(key))\n}\n\n/** The strict tool for a listing with exactly these fields, each required and nullable. */\nexport function aiSeoFieldsTool(fields: readonly SeoListingFieldKey[]): AiTool {\n const keys = orderedSeoListingFields(fields)\n const properties: Record<string, unknown> = {}\n for (const key of keys) {\n const field = SEO_LISTING_FIELDS[key]\n properties[key] = text(\n `${field.label}. ${field.purpose} At most ${field.maxLength} characters.` +\n (AI_SEO_REQUIRED_FIELDS.has(key) ? '' : ' null when nothing you were given says what the picture shows.'),\n )\n }\n return {\n name: AI_SEO_FIELDS_TOOL_NAME,\n description:\n 'Propose the search listing for one page or one product. Every field is required; ' +\n 'null means no value can be written from what you were given.',\n strict: true,\n inputSchema: {\n type: 'object',\n properties,\n required: keys,\n additionalProperties: false,\n },\n }\n}\n\nexport interface AiSeoFieldsCheckContext {\n fields: readonly SeoListingFieldKey[]\n /** Whether the listing has a social image; without one, an image description is dropped. */\n hasImage: boolean\n keywords?: readonly string[]\n /** Titles other pages of the site use, which this listing must not repeat. */\n otherTitles?: readonly string[]\n}\n\nconst normalizeTitle = (value: string): string => value.replace(/\\s+/g, ' ').trim().toLowerCase()\n\n/**\n * A listing answer held to the editor: every requested text field written,\n * each within its length, no keyword past its per-field count, no word\n * repeated to rank, no title another page uses.\n */\nexport function checkAiSeoFields(\n answer: Record<string, unknown>,\n context: AiSeoFieldsCheckContext,\n): { value: AiSeoFieldValues | null; violations: AiDoctrineViolation[]; offending?: Record<string, unknown> } {\n const values: AiSeoFieldValues = {}\n const violations: AiDoctrineViolation[] = []\n const offending: Record<string, unknown> = {}\n const otherTitles = new Set((context.otherTitles ?? []).map(normalizeTitle))\n for (const key of orderedSeoListingFields(context.fields)) {\n const field = SEO_LISTING_FIELDS[key]\n const raw = answer[key]\n if (raw !== null && raw !== undefined && typeof raw !== 'string') {\n violations.push({ rule: null, code: 'type', message: `${field.label} must be text or null.` })\n offending[key] = raw\n continue\n }\n const value = collapse(raw)\n if (!value) {\n if (AI_SEO_REQUIRED_FIELDS.has(key)) {\n violations.push({ rule: null, code: 'missing', message: `${field.label} is required.` })\n }\n continue\n }\n // An image description with no image to describe is a sentence about\n // nothing; it is left out rather than asked for again.\n if (key === 'imageAlt' && !context.hasImage) continue\n if (value.length > field.maxLength) {\n violations.push({\n rule: null,\n code: 'too-long',\n message: `${field.label} is ${value.length} characters; the limit is ${field.maxLength}.`,\n })\n offending[key] = value\n continue\n }\n const limit = AI_SEO_KEYWORD_LIMITS[key]\n const stuffed = limit\n ? (context.keywords ?? []).find((keyword) => seoKeywordCount(value, keyword) > limit)\n : undefined\n if (stuffed) {\n violations.push({\n rule: null,\n code: 'keyword-stuffing',\n message: `${field.label} repeats \"${stuffed}\"; use a keyword at most ${limit === 1 ? 'once' : `${limit} times`} there.`,\n })\n offending[key] = value\n continue\n }\n const repeated = key === 'imageAlt' ? null : repeatedWord(value)\n if (repeated) {\n violations.push({ rule: null, code: 'repetition', message: `${field.label} repeats \"${repeated}\" three times.` })\n offending[key] = value\n continue\n }\n if (key === 'title' && otherTitles.has(normalizeTitle(value))) {\n violations.push({\n rule: null,\n code: 'duplicate-title',\n message: `The title \"${value}\" is already another page's title.`,\n })\n offending[key] = value\n continue\n }\n values[key] = value\n }\n return violations.length ? { value: null, violations, offending } : { value: values, violations: [] }\n}\n\n/* ------------------------------------------------------------------------ *\n * A batch of a site audit's page fixes\n * ------------------------------------------------------------------------ */\n\nexport const AI_SEO_FIXES_TOOL_NAME = 'propose_seo_fixes'\n\n/**\n * How long an image description a batch proposes may be. Shorter than the\n * `alt` ceiling: a description read aloud is better short, and it keeps the\n * largest batch answer inside its measured budget.\n */\nexport const AI_SEO_FIX_ALT_MAX_CHARS = Math.min(150, MEDIA_ALT_MAX_LENGTH)\n\n/** Undescribed images one page of a batch gets descriptions for; the rest wait for the next audit. */\nexport const AI_SEO_FIX_IMAGES_PER_PAGE = 3\n\n/**\n * The undescribed images a batch asks about for a page, in page order: only\n * those the page itself holds. An image a component or a repeat renders is\n * described in the component or the rows, never by a fix to the page.\n */\nexport function aiSeoFixImages(page: Pick<AiSeoBatchPage, 'facts'>): SeoPageFacts['imagesMissingAlt'] {\n return page.facts.imagesMissingAlt.filter((image) => image.editable).slice(0, AI_SEO_FIX_IMAGES_PER_PAGE)\n}\n\n/** The strict tool for one batch: a title, description, heading and image descriptions per page. */\nexport function aiSeoFixesTool(screenIds: readonly string[]): AiTool {\n return {\n name: AI_SEO_FIXES_TOOL_NAME,\n description:\n 'Propose fixes for the pages you were given. One entry per page; null means leave that value as it is.',\n strict: true,\n inputSchema: {\n type: 'object',\n properties: {\n pages: {\n type: 'array',\n items: {\n type: 'object',\n properties: {\n screenId: { type: 'string', enum: [...screenIds] },\n title: text(`${SEO_LISTING_FIELDS.title.purpose} At most ${SEO_LISTING_FIELDS.title.maxLength} characters.`),\n description: text(\n `${SEO_LISTING_FIELDS.description.purpose} At most ${SEO_LISTING_FIELDS.description.maxLength} characters.`,\n ),\n h1: text(`The page’s one main heading. At most ${AI_TEXT_LIMITS.headline} characters.`),\n imageAlts: {\n type: 'array',\n items: {\n type: 'object',\n properties: {\n nodeId: { type: 'string' },\n alt: {\n type: 'string',\n description: `What the picture shows. At most ${AI_SEO_FIX_ALT_MAX_CHARS} characters.`,\n },\n },\n required: ['nodeId', 'alt'],\n additionalProperties: false,\n },\n },\n },\n required: ['screenId', 'title', 'description', 'h1', 'imageAlts'],\n additionalProperties: false,\n },\n },\n },\n required: ['pages'],\n additionalProperties: false,\n },\n }\n}\n\n/** A batch page as the fixes prompt and check read it. */\nexport interface AiSeoBatchPage {\n screenId: string\n path: string\n name: string\n codes: ReadonlySet<SeoFindingCode>\n keywords: readonly string[]\n seo: { title?: string; description?: string; breadcrumb?: string; image?: string; imageAlt?: string } | undefined\n facts: SeoPageFacts\n}\n\n/** The findings that let a batch answer propose each value. */\nexport const AI_SEO_FIX_CODES = {\n title: ['title-missing', 'title-too-long', 'title-duplicate', 'keyword-missing'],\n description: ['description-missing', 'description-too-long', 'description-duplicate', 'keyword-missing'],\n h1: ['h1-missing', 'h1-thin'],\n} as const satisfies Record<string, readonly SeoFindingCode[]>\n\nexport interface AiSeoBatchAnswer {\n title: string | null\n description: string | null\n h1: string | null\n imageAlts: Array<{ nodeId: string; alt: string }>\n}\n\n/**\n * A batch answer held to the audit and the editor: values only where a\n * finding asked for them, each within its length, no keyword past its count,\n * no title another page uses, and image descriptions only for this page's\n * undescribed images.\n */\nexport function checkAiSeoFixes(\n answer: Record<string, unknown>,\n pages: readonly AiSeoBatchPage[],\n titlesElsewhere: readonly string[],\n): { value: Record<string, AiSeoBatchAnswer> | null; violations: AiDoctrineViolation[]; offending?: Record<string, unknown> } {\n const byId = new Map(pages.map((page) => [page.screenId, page]))\n const violations: AiDoctrineViolation[] = []\n const offending: Array<Record<string, unknown>> = []\n const value: Record<string, AiSeoBatchAnswer> = {}\n const taken = new Set(titlesElsewhere.map(normalizeTitle))\n const entries = Array.isArray(answer['pages']) ? (answer['pages'] as unknown[]) : null\n if (!entries) {\n return { value: null, violations: [{ rule: null, code: 'shape', message: 'The answer must list the pages.' }] }\n }\n const asks = (page: AiSeoBatchPage, codes: readonly SeoFindingCode[]) =>\n codes.some((code) => page.codes.has(code))\n for (const raw of entries) {\n const entry = (raw ?? {}) as Record<string, unknown>\n const page = byId.get(String(entry['screenId'] ?? ''))\n if (!page || value[page.screenId]) continue\n const broken: string[] = []\n const fix: AiSeoBatchAnswer = { title: null, description: null, h1: null, imageAlts: [] }\n\n const title = collapse(entry['title'])\n if (title && asks(page, AI_SEO_FIX_CODES.title)) {\n if (title.length > SEO_LISTING_FIELDS.title.maxLength) {\n broken.push(`title is ${title.length} characters; the limit is ${SEO_LISTING_FIELDS.title.maxLength}`)\n } else if (page.keywords.some((keyword) => seoKeywordCount(title, keyword) > (AI_SEO_KEYWORD_LIMITS.title ?? 1))) {\n broken.push('title repeats a keyword')\n } else if (taken.has(normalizeTitle(title))) {\n broken.push(`title \"${title}\" is another page's title`)\n } else {\n fix.title = title\n taken.add(normalizeTitle(title))\n }\n }\n const description = collapse(entry['description'])\n if (description && asks(page, AI_SEO_FIX_CODES.description)) {\n if (description.length > SEO_LISTING_FIELDS.description.maxLength) {\n broken.push(\n `description is ${description.length} characters; the limit is ${SEO_LISTING_FIELDS.description.maxLength}`,\n )\n } else if (\n page.keywords.some((keyword) => seoKeywordCount(description, keyword) > (AI_SEO_KEYWORD_LIMITS.description ?? 2))\n ) {\n broken.push('description repeats a keyword')\n } else {\n fix.description = description\n }\n }\n const h1 = collapse(entry['h1'])\n if (h1 && asks(page, AI_SEO_FIX_CODES.h1)) {\n if (h1.length > AI_TEXT_LIMITS.headline) {\n broken.push(`h1 is ${h1.length} characters; the limit is ${AI_TEXT_LIMITS.headline}`)\n } else {\n fix.h1 = h1\n }\n }\n const undescribed = new Set(aiSeoFixImages(page).map((image) => image.nodeId))\n for (const rawAlt of Array.isArray(entry['imageAlts']) ? (entry['imageAlts'] as unknown[]) : []) {\n const altEntry = (rawAlt ?? {}) as Record<string, unknown>\n const nodeId = String(altEntry['nodeId'] ?? '')\n const alt = collapse(altEntry['alt'])\n if (!undescribed.has(nodeId) || !alt || fix.imageAlts.some((known) => known.nodeId === nodeId)) continue\n if (alt.length > AI_SEO_FIX_ALT_MAX_CHARS) {\n broken.push(`the description of image ${nodeId} is over ${AI_SEO_FIX_ALT_MAX_CHARS} characters`)\n } else {\n fix.imageAlts.push({ nodeId, alt })\n }\n }\n if (broken.length) {\n violations.push(...broken.map((message) => ({ rule: null, code: 'fix', message: `Page ${page.screenId}: ${message}.` })))\n offending.push(entry)\n } else {\n value[page.screenId] = fix\n }\n }\n return violations.length ? { value: null, violations, offending: { pages: offending } } : { value, violations: [] }\n}\n\n/**\n * A batch answer with each page's title, description, heading and image\n * descriptions fitted to their lengths (AGL-4016), or `null` when nothing\n * ran over. The caller runs `checkAiSeoFixes` on the result again.\n */\nexport function fitAiSeoFixLengths(answer: Record<string, unknown>): Record<string, unknown> | null {\n if (!Array.isArray(answer['pages'])) return null\n let changed = false\n const fit = (raw: unknown, max: number) => {\n if (typeof raw !== 'string' || collapse(raw).length <= max) return raw\n changed = true\n return aiFitSeoText(raw, max)\n }\n const pages = (answer['pages'] as unknown[]).map((raw) => {\n if (!raw || typeof raw !== 'object') return raw\n const entry = raw as Record<string, unknown>\n return {\n ...entry,\n title: fit(entry['title'], SEO_LISTING_FIELDS.title.maxLength),\n description: fit(entry['description'], SEO_LISTING_FIELDS.description.maxLength),\n h1: fit(entry['h1'], AI_TEXT_LIMITS.headline),\n ...(Array.isArray(entry['imageAlts'])\n ? {\n imageAlts: (entry['imageAlts'] as unknown[]).map((alt) =>\n alt && typeof alt === 'object'\n ? {\n ...(alt as Record<string, unknown>),\n alt: fit((alt as Record<string, unknown>)['alt'], AI_SEO_FIX_ALT_MAX_CHARS),\n }\n : alt,\n ),\n }\n : {}),\n }\n })\n return changed ? { ...answer, pages } : null\n}\n\n/* ------------------------------------------------------------------------ *\n * The site: structured data and guidance for AI agents\n * ------------------------------------------------------------------------ */\n\nexport const AI_SEO_SITE_TOOL_NAME = 'propose_site_listing'\n\n/** The longest structured-data description the site SEO form accepts. */\nexport const AI_SEO_ENTITY_DESCRIPTION_MAX_CHARS = 300\n\n/** The longest agent guidance the site SEO form accepts. */\nexport const AI_SEO_AGENT_GUIDANCE_MAX_CHARS = 1_000\n\n/** The entity types the tool offers, and the value the site SEO form stores for each. */\nexport const AI_SEO_ENTITY_TYPE_VALUES: Readonly<Record<string, string>> = {\n Organization: `${HostEntityType.ORGANIZATION}`,\n Person: `${HostEntityType.PERSON}`,\n}\n\n/** The site-wide proposal's strict tool: structured data and agent guidance, each nullable. */\nexport function aiSeoSiteTool(): AiTool {\n const properties = {\n entityType: {\n anyOf: [{ type: 'string', enum: Object.keys(AI_SEO_ENTITY_TYPE_VALUES) }, { type: 'null' }],\n description: 'Whether a company or a person publishes the site.',\n },\n entityName: text('The publisher’s name as the site gives it.'),\n entityDescription: text(\n `What the publisher IS, in one sentence — not what a page is about. At most ${AI_SEO_ENTITY_DESCRIPTION_MAX_CHARS} characters.`,\n ),\n contactEmail: text('A contact email address, only when a page of the site shows it.'),\n contactTelephone: text('A contact telephone number, only when a page of the site shows it.'),\n contactType: text('What that contact answers, such as customer support or sales.'),\n whenToUse: text(\n `For AI agents: the questions this site is the best source for, specifically. At most ${AI_SEO_AGENT_GUIDANCE_MAX_CHARS} characters.`,\n ),\n howToUse: text(\n `For AI agents: which pages answer what, and what not to rely on. At most ${AI_SEO_AGENT_GUIDANCE_MAX_CHARS} characters.`,\n ),\n }\n return {\n name: AI_SEO_SITE_TOOL_NAME,\n description:\n 'Propose the site’s structured data and its guidance for AI agents. Every field is required; null means leave it as it is.',\n strict: true,\n inputSchema: {\n type: 'object',\n properties,\n required: Object.keys(properties),\n additionalProperties: false,\n },\n }\n}\n\nexport interface AiSeoSiteCheckContext {\n /** The form fields that are blank on the site now; only these may be proposed. */\n blank: ReadonlySet<AiSeoSiteFormField>\n /** Every word the site's pages show, for the contact details to be found in. */\n siteText: string\n}\n\nconst EMAIL = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\n\n/**\n * The site proposal held to the form and to the site: lengths, and a contact\n * detail only when a page shows it. A detail no page shows is left out with\n * a note rather than asked for again — asking again invites composing one.\n */\nexport function checkAiSeoSite(\n answer: Record<string, unknown>,\n context: AiSeoSiteCheckContext,\n): {\n value: { values: Partial<Record<AiSeoSiteFormField, string>>; notes: string[] } | null\n violations: AiDoctrineViolation[]\n offending?: Record<string, unknown>\n} {\n const values: Partial<Record<AiSeoSiteFormField, string>> = {}\n const notes: string[] = []\n const violations: AiDoctrineViolation[] = []\n const offending: Record<string, unknown> = {}\n const put = (field: AiSeoSiteFormField, value: string) => {\n if (value && context.blank.has(field)) values[field] = value\n }\n\n const type = collapse(answer['entityType'])\n if (type && AI_SEO_ENTITY_TYPE_VALUES[type]) put('seo.entity.type', AI_SEO_ENTITY_TYPE_VALUES[type])\n put('seo.entity.name', collapse(answer['entityName']).slice(0, 200))\n\n const description = collapse(answer['entityDescription'])\n if (description.length > AI_SEO_ENTITY_DESCRIPTION_MAX_CHARS) {\n violations.push({\n rule: null,\n code: 'too-long',\n message: `The entity description is ${description.length} characters; the limit is ${AI_SEO_ENTITY_DESCRIPTION_MAX_CHARS}.`,\n })\n offending['entityDescription'] = description\n } else {\n put('seo.entity.description', description)\n }\n\n const siteText = context.siteText.toLowerCase()\n const siteDigits = context.siteText.replace(/\\D+/g, '')\n const email = collapse(answer['contactEmail'])\n if (email) {\n if (EMAIL.test(email) && siteText.includes(email.toLowerCase())) put('seo.entity.email', email)\n else notes.push('A contact email was left out: no page of the site shows it.')\n }\n const telephone = collapse(answer['contactTelephone'])\n if (telephone) {\n const digits = telephone.replace(/\\D+/g, '')\n if (digits.length >= 7 && siteDigits.includes(digits)) put('seo.entity.telephone', telephone)\n else notes.push('A contact telephone number was left out: no page of the site shows it.')\n }\n if (values['seo.entity.email'] || values['seo.entity.telephone']) {\n put('seo.entity.contactType', collapse(answer['contactType']).slice(0, 100))\n }\n\n for (const [key, field] of [\n ['whenToUse', 'seo.agent.whenToUse'],\n ['howToUse', 'seo.agent.howToUse'],\n ] as const) {\n const guidance = collapse(answer[key])\n if (guidance.length > AI_SEO_AGENT_GUIDANCE_MAX_CHARS) {\n violations.push({\n rule: null,\n code: 'too-long',\n message: `${key} is ${guidance.length} characters; the limit is ${AI_SEO_AGENT_GUIDANCE_MAX_CHARS}.`,\n })\n offending[key] = guidance\n } else {\n put(field, guidance)\n }\n }\n return violations.length ? { value: null, violations, offending } : { value: { values, notes }, violations: [] }\n}\n"],"names":["MEDIA_ALT_MAX_LENGTH","seoKeywordCount","SEO_LISTING_FIELDS","HostEntityType","AI_TEXT_LIMITS","aiClipAtWord","AI_SEO_KEYWORD_LIMITS","title","description","breadcrumb","repeatedWord","text","counts","Map","word","toLowerCase","match","count","get","set","collapse","value","replace","trim","anyOf","type","aiFitSeoText","max","fitAiSeoFieldLengths","answer","fields","fitted","key","orderedSeoListingFields","raw","maxLength","length","AI_SEO_FIELDS_TOOL_NAME","AI_SEO_REQUIRED_FIELDS","Set","wanted","Object","keys","filter","has","aiSeoFieldsTool","properties","field","label","purpose","name","strict","inputSchema","required","additionalProperties","normalizeTitle","checkAiSeoFields","context","values","violations","offending","otherTitles","map","undefined","push","rule","code","message","hasImage","limit","stuffed","keywords","find","keyword","repeated","AI_SEO_FIXES_TOOL_NAME","AI_SEO_FIX_ALT_MAX_CHARS","Math","min","AI_SEO_FIX_IMAGES_PER_PAGE","aiSeoFixImages","page","facts","imagesMissingAlt","image","editable","slice","aiSeoFixesTool","screenIds","pages","items","screenId","enum","h1","headline","imageAlts","nodeId","alt","AI_SEO_FIX_CODES","checkAiSeoFixes","titlesElsewhere","byId","taken","entries","Array","isArray","asks","codes","some","entry","String","broken","fix","add","undescribed","rawAlt","altEntry","known","fitAiSeoFixLengths","changed","fit","AI_SEO_SITE_TOOL_NAME","AI_SEO_ENTITY_DESCRIPTION_MAX_CHARS","AI_SEO_AGENT_GUIDANCE_MAX_CHARS","AI_SEO_ENTITY_TYPE_VALUES","Organization","ORGANIZATION","Person","PERSON","aiSeoSiteTool","entityType","entityName","entityDescription","contactEmail","contactTelephone","contactType","whenToUse","howToUse","EMAIL","checkAiSeoSite","notes","put","blank","siteText","siteDigits","email","test","includes","telephone","digits","guidance"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,oBAAoB,QAAQ,mCAAkC;AAEvE,SAASC,eAAe,QAAQ,sCAAqC;AACrE,SACEC,kBAAkB,QAEb,4CAA2C;AAElD,SAASC,cAAc,QAAQ,qDAAoD;AAGnF,SAASC,cAAc,QAAQ,2BAAuB;AACtD,SAASC,YAAY,QAAkC,uCAAmC;AAE1F;;;;;;;;;;;;;;;;;CAiBC,GAED;;;4EAG4E,GAE5E,mFAAmF,GACnF,OAAO,MAAMC,wBAA+E;IAC1FC,OAAO;IACPC,aAAa;IACbC,YAAY;AACd,EAAC;AAED,0FAA0F,GAC1F,SAASC,aAAaC,IAAY;QAEbA;IADnB,MAAMC,SAAS,IAAIC;IACnB,KAAK,MAAMC,SAAQH,0BAAAA,KAAKI,WAAW,GAAGC,KAAK,CAAC,4naAAzBL,0BAAkD,EAAE,CAAE;YACxDC;QAAf,MAAMK,QAAQ,EAACL,cAAAA,OAAOM,GAAG,CAACJ,iBAAXF,cAAoB,KAAK;QACxC,IAAIK,SAAS,GAAG,OAAOH;QACvBF,OAAOO,GAAG,CAACL,MAAMG;IACnB;IACA,OAAO;AACT;AAEA,MAAMG,WAAW,CAACC,QAChB,OAAOA,UAAU,WAAWA,MAAMC,OAAO,CAAC,QAAQ,KAAKC,IAAI,KAAK;AAElE,MAAMZ,OAAO,CAACH,cAAyB,CAAA;QAAEgB,OAAO;YAAC;gBAAEC,MAAM;YAAS;YAAG;gBAAEA,MAAM;YAAO;SAAE;QAAEjB;IAAY,CAAA;AAEpG;;4EAE4E,GAE5E;;;;;;;;;;CAUC,GACD,OAAO,SAASkB,aAAaL,KAAa,EAAEM,GAAW;IACrD,OAAOtB,aAAae,SAASC,QAAQM;AACvC;AAEA;;;;CAIC,GACD,OAAO,SAASC,qBACdC,MAA+B,EAC/BC,MAAqC;IAErC,IAAIC,SAAyC;IAC7C,KAAK,MAAMC,OAAOC,wBAAwBH,QAAS;QACjD,MAAMI,MAAML,MAAM,CAACG,IAAI;QACvB,MAAM,EAAEG,SAAS,EAAE,GAAGjC,kBAAkB,CAAC8B,IAAI;QAC7C,IAAI,OAAOE,QAAQ,YAAYd,SAASc,KAAKE,MAAM,IAAID,WAAW;QAClEJ,iBAAAA,SAAAA,SAAW,aAAKF;QAChBE,MAAM,CAACC,IAAI,GAAGN,aAAaQ,KAAKC;IAClC;IACA,OAAOJ;AACT;AAEA;;4EAE4E,GAE5E,OAAO,MAAMM,0BAA0B,yBAAwB;AAE/D,oFAAoF,GACpF,OAAO,MAAMC,yBAA0D,IAAIC,IAAI;IAC7E;IACA;IACA;CACD,EAAC;AAEF,0EAA0E,GAC1E,OAAO,SAASN,wBAAwBH,MAAqC;IAC3E,MAAMU,SAAS,IAAID,IAAIT;IACvB,OAAO,AAACW,OAAOC,IAAI,CAACxC,oBAA6CyC,MAAM,CAAC,CAACX,MAAQQ,OAAOI,GAAG,CAACZ;AAC9F;AAEA,yFAAyF,GACzF,OAAO,SAASa,gBAAgBf,MAAqC;IACnE,MAAMY,OAAOT,wBAAwBH;IACrC,MAAMgB,aAAsC,CAAC;IAC7C,KAAK,MAAMd,OAAOU,KAAM;QACtB,MAAMK,QAAQ7C,kBAAkB,CAAC8B,IAAI;QACrCc,UAAU,CAACd,IAAI,GAAGrB,KAChB,GAAGoC,MAAMC,KAAK,CAAC,EAAE,EAAED,MAAME,OAAO,CAAC,SAAS,EAAEF,MAAMZ,SAAS,CAAC,YAAY,CAAC,GACtEG,CAAAA,uBAAuBM,GAAG,CAACZ,OAAO,KAAK,gEAA+D;IAE7G;IACA,OAAO;QACLkB,MAAMb;QACN7B,aACE,sFACA;QACF2C,QAAQ;QACRC,aAAa;YACX3B,MAAM;YACNqB;YACAO,UAAUX;YACVY,sBAAsB;QACxB;IACF;AACF;AAWA,MAAMC,iBAAiB,CAAClC,QAA0BA,MAAMC,OAAO,CAAC,QAAQ,KAAKC,IAAI,GAAGR,WAAW;AAE/F;;;;CAIC,GACD,OAAO,SAASyC,iBACd3B,MAA+B,EAC/B4B,OAAgC;QAKHA;IAH7B,MAAMC,SAA2B,CAAC;IAClC,MAAMC,aAAoC,EAAE;IAC5C,MAAMC,YAAqC,CAAC;IAC5C,MAAMC,cAAc,IAAItB,IAAI,EAACkB,uBAAAA,QAAQI,WAAW,YAAnBJ,uBAAuB,EAAE,EAAEK,GAAG,CAACP;IAC5D,KAAK,MAAMvB,OAAOC,wBAAwBwB,QAAQ3B,MAAM,EAAG;YA6BpD2B;QA5BL,MAAMV,QAAQ7C,kBAAkB,CAAC8B,IAAI;QACrC,MAAME,MAAML,MAAM,CAACG,IAAI;QACvB,IAAIE,QAAQ,QAAQA,QAAQ6B,aAAa,OAAO7B,QAAQ,UAAU;YAChEyB,WAAWK,IAAI,CAAC;gBAAEC,MAAM;gBAAMC,MAAM;gBAAQC,SAAS,GAAGpB,MAAMC,KAAK,CAAC,sBAAsB,CAAC;YAAC;YAC5FY,SAAS,CAAC5B,IAAI,GAAGE;YACjB;QACF;QACA,MAAMb,QAAQD,SAASc;QACvB,IAAI,CAACb,OAAO;YACV,IAAIiB,uBAAuBM,GAAG,CAACZ,MAAM;gBACnC2B,WAAWK,IAAI,CAAC;oBAAEC,MAAM;oBAAMC,MAAM;oBAAWC,SAAS,GAAGpB,MAAMC,KAAK,CAAC,aAAa,CAAC;gBAAC;YACxF;YACA;QACF;QACA,qEAAqE;QACrE,uDAAuD;QACvD,IAAIhB,QAAQ,cAAc,CAACyB,QAAQW,QAAQ,EAAE;QAC7C,IAAI/C,MAAMe,MAAM,GAAGW,MAAMZ,SAAS,EAAE;YAClCwB,WAAWK,IAAI,CAAC;gBACdC,MAAM;gBACNC,MAAM;gBACNC,SAAS,GAAGpB,MAAMC,KAAK,CAAC,IAAI,EAAE3B,MAAMe,MAAM,CAAC,0BAA0B,EAAEW,MAAMZ,SAAS,CAAC,CAAC,CAAC;YAC3F;YACAyB,SAAS,CAAC5B,IAAI,GAAGX;YACjB;QACF;QACA,MAAMgD,QAAQ/D,qBAAqB,CAAC0B,IAAI;QACxC,MAAMsC,UAAUD,QACZ,EAACZ,oBAAAA,QAAQc,QAAQ,YAAhBd,oBAAoB,EAAE,EAAEe,IAAI,CAAC,CAACC,UAAYxE,gBAAgBoB,OAAOoD,WAAWJ,SAC7EN;QACJ,IAAIO,SAAS;YACXX,WAAWK,IAAI,CAAC;gBACdC,MAAM;gBACNC,MAAM;gBACNC,SAAS,GAAGpB,MAAMC,KAAK,CAAC,UAAU,EAAEsB,QAAQ,yBAAyB,EAAED,UAAU,IAAI,SAAS,GAAGA,MAAM,MAAM,CAAC,CAAC,OAAO,CAAC;YACzH;YACAT,SAAS,CAAC5B,IAAI,GAAGX;YACjB;QACF;QACA,MAAMqD,WAAW1C,QAAQ,aAAa,OAAOtB,aAAaW;QAC1D,IAAIqD,UAAU;YACZf,WAAWK,IAAI,CAAC;gBAAEC,MAAM;gBAAMC,MAAM;gBAAcC,SAAS,GAAGpB,MAAMC,KAAK,CAAC,UAAU,EAAE0B,SAAS,cAAc,CAAC;YAAC;YAC/Gd,SAAS,CAAC5B,IAAI,GAAGX;YACjB;QACF;QACA,IAAIW,QAAQ,WAAW6B,YAAYjB,GAAG,CAACW,eAAelC,SAAS;YAC7DsC,WAAWK,IAAI,CAAC;gBACdC,MAAM;gBACNC,MAAM;gBACNC,SAAS,CAAC,WAAW,EAAE9C,MAAM,kCAAkC,CAAC;YAClE;YACAuC,SAAS,CAAC5B,IAAI,GAAGX;YACjB;QACF;QACAqC,MAAM,CAAC1B,IAAI,GAAGX;IAChB;IACA,OAAOsC,WAAWvB,MAAM,GAAG;QAAEf,OAAO;QAAMsC;QAAYC;IAAU,IAAI;QAAEvC,OAAOqC;QAAQC,YAAY,EAAE;IAAC;AACtG;AAEA;;4EAE4E,GAE5E,OAAO,MAAMgB,yBAAyB,oBAAmB;AAEzD;;;;CAIC,GACD,OAAO,MAAMC,2BAA2BC,KAAKC,GAAG,CAAC,KAAK9E,sBAAqB;AAE3E,oGAAoG,GACpG,OAAO,MAAM+E,6BAA6B,EAAC;AAE3C;;;;CAIC,GACD,OAAO,SAASC,eAAeC,IAAmC;IAChE,OAAOA,KAAKC,KAAK,CAACC,gBAAgB,CAACxC,MAAM,CAAC,CAACyC,QAAUA,MAAMC,QAAQ,EAAEC,KAAK,CAAC,GAAGP;AAChF;AAEA,kGAAkG,GAClG,OAAO,SAASQ,eAAeC,SAA4B;IACzD,OAAO;QACLtC,MAAMyB;QACNnE,aACE;QACF2C,QAAQ;QACRC,aAAa;YACX3B,MAAM;YACNqB,YAAY;gBACV2C,OAAO;oBACLhE,MAAM;oBACNiE,OAAO;wBACLjE,MAAM;wBACNqB,YAAY;4BACV6C,UAAU;gCAAElE,MAAM;gCAAUmE,MAAM;uCAAIJ;iCAAU;4BAAC;4BACjDjF,OAAOI,KAAK,GAAGT,mBAAmBK,KAAK,CAAC0C,OAAO,CAAC,SAAS,EAAE/C,mBAAmBK,KAAK,CAAC4B,SAAS,CAAC,YAAY,CAAC;4BAC3G3B,aAAaG,KACX,GAAGT,mBAAmBM,WAAW,CAACyC,OAAO,CAAC,SAAS,EAAE/C,mBAAmBM,WAAW,CAAC2B,SAAS,CAAC,YAAY,CAAC;4BAE7G0D,IAAIlF,KAAK,CAAC,qCAAqC,EAAEP,eAAe0F,QAAQ,CAAC,YAAY,CAAC;4BACtFC,WAAW;gCACTtE,MAAM;gCACNiE,OAAO;oCACLjE,MAAM;oCACNqB,YAAY;wCACVkD,QAAQ;4CAAEvE,MAAM;wCAAS;wCACzBwE,KAAK;4CACHxE,MAAM;4CACNjB,aAAa,CAAC,gCAAgC,EAAEoE,yBAAyB,YAAY,CAAC;wCACxF;oCACF;oCACAvB,UAAU;wCAAC;wCAAU;qCAAM;oCAC3BC,sBAAsB;gCACxB;4BACF;wBACF;wBACAD,UAAU;4BAAC;4BAAY;4BAAS;4BAAe;4BAAM;yBAAY;wBACjEC,sBAAsB;oBACxB;gBACF;YACF;YACAD,UAAU;gBAAC;aAAQ;YACnBC,sBAAsB;QACxB;IACF;AACF;AAaA,6DAA6D,GAC7D,OAAO,MAAM4C,mBAAmB;IAC9B3F,OAAO;QAAC;QAAiB;QAAkB;QAAmB;KAAkB;IAChFC,aAAa;QAAC;QAAuB;QAAwB;QAAyB;KAAkB;IACxGqF,IAAI;QAAC;QAAc;KAAU;AAC/B,EAA8D;AAS9D;;;;;CAKC,GACD,OAAO,SAASM,gBACdtE,MAA+B,EAC/B4D,KAAgC,EAChCW,eAAkC;IAElC,MAAMC,OAAO,IAAIxF,IAAI4E,MAAM3B,GAAG,CAAC,CAACmB,OAAS;YAACA,KAAKU,QAAQ;YAAEV;SAAK;IAC9D,MAAMtB,aAAoC,EAAE;IAC5C,MAAMC,YAA4C,EAAE;IACpD,MAAMvC,QAA0C,CAAC;IACjD,MAAMiF,QAAQ,IAAI/D,IAAI6D,gBAAgBtC,GAAG,CAACP;IAC1C,MAAMgD,UAAUC,MAAMC,OAAO,CAAC5E,MAAM,CAAC,QAAQ,IAAKA,MAAM,CAAC,QAAQ,GAAiB;IAClF,IAAI,CAAC0E,SAAS;QACZ,OAAO;YAAElF,OAAO;YAAMsC,YAAY;gBAAC;oBAAEM,MAAM;oBAAMC,MAAM;oBAASC,SAAS;gBAAkC;aAAE;QAAC;IAChH;IACA,MAAMuC,OAAO,CAACzB,MAAsB0B,QAClCA,MAAMC,IAAI,CAAC,CAAC1C,OAASe,KAAK0B,KAAK,CAAC/D,GAAG,CAACsB;IACtC,KAAK,MAAMhC,OAAOqE,QAAS;YAEIM;QAD7B,MAAMA,QAAS3E,cAAAA,MAAO,CAAC;QACvB,MAAM+C,OAAOoB,KAAKnF,GAAG,CAAC4F,QAAOD,kBAAAA,KAAK,CAAC,WAAW,YAAjBA,kBAAqB;QAClD,IAAI,CAAC5B,QAAQ5D,KAAK,CAAC4D,KAAKU,QAAQ,CAAC,EAAE;QACnC,MAAMoB,SAAmB,EAAE;QAC3B,MAAMC,MAAwB;YAAEzG,OAAO;YAAMC,aAAa;YAAMqF,IAAI;YAAME,WAAW,EAAE;QAAC;QAExF,MAAMxF,QAAQa,SAASyF,KAAK,CAAC,QAAQ;QACrC,IAAItG,SAASmG,KAAKzB,MAAMiB,iBAAiB3F,KAAK,GAAG;YAC/C,IAAIA,MAAM6B,MAAM,GAAGlC,mBAAmBK,KAAK,CAAC4B,SAAS,EAAE;gBACrD4E,OAAO/C,IAAI,CAAC,CAAC,SAAS,EAAEzD,MAAM6B,MAAM,CAAC,0BAA0B,EAAElC,mBAAmBK,KAAK,CAAC4B,SAAS,EAAE;YACvG,OAAO,IAAI8C,KAAKV,QAAQ,CAACqC,IAAI,CAAC,CAACnC;oBAA+CnE;uBAAnCL,gBAAgBM,OAAOkE,aAAYnE,+BAAAA,sBAAsBC,KAAK,YAA3BD,+BAA+B;gBAAK;gBAChHyG,OAAO/C,IAAI,CAAC;YACd,OAAO,IAAIsC,MAAM1D,GAAG,CAACW,eAAehD,SAAS;gBAC3CwG,OAAO/C,IAAI,CAAC,CAAC,OAAO,EAAEzD,MAAM,yBAAyB,CAAC;YACxD,OAAO;gBACLyG,IAAIzG,KAAK,GAAGA;gBACZ+F,MAAMW,GAAG,CAAC1D,eAAehD;YAC3B;QACF;QACA,MAAMC,cAAcY,SAASyF,KAAK,CAAC,cAAc;QACjD,IAAIrG,eAAekG,KAAKzB,MAAMiB,iBAAiB1F,WAAW,GAAG;YAC3D,IAAIA,YAAY4B,MAAM,GAAGlC,mBAAmBM,WAAW,CAAC2B,SAAS,EAAE;gBACjE4E,OAAO/C,IAAI,CACT,CAAC,eAAe,EAAExD,YAAY4B,MAAM,CAAC,0BAA0B,EAAElC,mBAAmBM,WAAW,CAAC2B,SAAS,EAAE;YAE/G,OAAO,IACL8C,KAAKV,QAAQ,CAACqC,IAAI,CAAC,CAACnC;oBAAqDnE;uBAAzCL,gBAAgBO,aAAaiE,aAAYnE,qCAAAA,sBAAsBE,WAAW,YAAjCF,qCAAqC;gBAC9G;gBACAyG,OAAO/C,IAAI,CAAC;YACd,OAAO;gBACLgD,IAAIxG,WAAW,GAAGA;YACpB;QACF;QACA,MAAMqF,KAAKzE,SAASyF,KAAK,CAAC,KAAK;QAC/B,IAAIhB,MAAMa,KAAKzB,MAAMiB,iBAAiBL,EAAE,GAAG;YACzC,IAAIA,GAAGzD,MAAM,GAAGhC,eAAe0F,QAAQ,EAAE;gBACvCiB,OAAO/C,IAAI,CAAC,CAAC,MAAM,EAAE6B,GAAGzD,MAAM,CAAC,0BAA0B,EAAEhC,eAAe0F,QAAQ,EAAE;YACtF,OAAO;gBACLkB,IAAInB,EAAE,GAAGA;YACX;QACF;QACA,MAAMqB,cAAc,IAAI3E,IAAIyC,eAAeC,MAAMnB,GAAG,CAAC,CAACsB,QAAUA,MAAMY,MAAM;QAC5E,KAAK,MAAMmB,UAAUX,MAAMC,OAAO,CAACI,KAAK,CAAC,YAAY,IAAKA,KAAK,CAAC,YAAY,GAAiB,EAAE,CAAE;gBAEzEO;YADtB,MAAMA,WAAYD,iBAAAA,SAAU,CAAC;YAC7B,MAAMnB,SAASc,QAAOM,mBAAAA,QAAQ,CAAC,SAAS,YAAlBA,mBAAsB;YAC5C,MAAMnB,MAAM7E,SAASgG,QAAQ,CAAC,MAAM;YACpC,IAAI,CAACF,YAAYtE,GAAG,CAACoD,WAAW,CAACC,OAAOe,IAAIjB,SAAS,CAACa,IAAI,CAAC,CAACS,QAAUA,MAAMrB,MAAM,KAAKA,SAAS;YAChG,IAAIC,IAAI7D,MAAM,GAAGwC,0BAA0B;gBACzCmC,OAAO/C,IAAI,CAAC,CAAC,yBAAyB,EAAEgC,OAAO,SAAS,EAAEpB,yBAAyB,WAAW,CAAC;YACjG,OAAO;gBACLoC,IAAIjB,SAAS,CAAC/B,IAAI,CAAC;oBAAEgC;oBAAQC;gBAAI;YACnC;QACF;QACA,IAAIc,OAAO3E,MAAM,EAAE;YACjBuB,WAAWK,IAAI,IAAI+C,OAAOjD,GAAG,CAAC,CAACK,UAAa,CAAA;oBAAEF,MAAM;oBAAMC,MAAM;oBAAOC,SAAS,CAAC,KAAK,EAAEc,KAAKU,QAAQ,CAAC,EAAE,EAAExB,QAAQ,CAAC,CAAC;gBAAC,CAAA;YACrHP,UAAUI,IAAI,CAAC6C;QACjB,OAAO;YACLxF,KAAK,CAAC4D,KAAKU,QAAQ,CAAC,GAAGqB;QACzB;IACF;IACA,OAAOrD,WAAWvB,MAAM,GAAG;QAAEf,OAAO;QAAMsC;QAAYC,WAAW;YAAE6B,OAAO7B;QAAU;IAAE,IAAI;QAAEvC;QAAOsC,YAAY,EAAE;IAAC;AACpH;AAEA;;;;CAIC,GACD,OAAO,SAAS2D,mBAAmBzF,MAA+B;IAChE,IAAI,CAAC2E,MAAMC,OAAO,CAAC5E,MAAM,CAAC,QAAQ,GAAG,OAAO;IAC5C,IAAI0F,UAAU;IACd,MAAMC,MAAM,CAACtF,KAAcP;QACzB,IAAI,OAAOO,QAAQ,YAAYd,SAASc,KAAKE,MAAM,IAAIT,KAAK,OAAOO;QACnEqF,UAAU;QACV,OAAO7F,aAAaQ,KAAKP;IAC3B;IACA,MAAM8D,QAAQ,AAAC5D,MAAM,CAAC,QAAQ,CAAeiC,GAAG,CAAC,CAAC5B;QAChD,IAAI,CAACA,OAAO,OAAOA,QAAQ,UAAU,OAAOA;QAC5C,MAAM2E,QAAQ3E;QACd,OAAO,aACF2E;YACHtG,OAAOiH,IAAIX,KAAK,CAAC,QAAQ,EAAE3G,mBAAmBK,KAAK,CAAC4B,SAAS;YAC7D3B,aAAagH,IAAIX,KAAK,CAAC,cAAc,EAAE3G,mBAAmBM,WAAW,CAAC2B,SAAS;YAC/E0D,IAAI2B,IAAIX,KAAK,CAAC,KAAK,EAAEzG,eAAe0F,QAAQ;WACxCU,MAAMC,OAAO,CAACI,KAAK,CAAC,YAAY,IAChC;YACEd,WAAW,AAACc,KAAK,CAAC,YAAY,CAAe/C,GAAG,CAAC,CAACmC,MAChDA,OAAO,OAAOA,QAAQ,WAClB,aACMA;oBACJA,KAAKuB,IAAI,AAACvB,GAA+B,CAAC,MAAM,EAAErB;qBAEpDqB;QAER,IACA,CAAC;IAET;IACA,OAAOsB,UAAU,aAAK1F;QAAQ4D;SAAU;AAC1C;AAEA;;4EAE4E,GAE5E,OAAO,MAAMgC,wBAAwB,uBAAsB;AAE3D,uEAAuE,GACvE,OAAO,MAAMC,sCAAsC,IAAG;AAEtD,0DAA0D,GAC1D,OAAO,MAAMC,kCAAkC,KAAK;AAEpD,uFAAuF,GACvF,OAAO,MAAMC,4BAA8D;IACzEC,cAAc,GAAG1H,eAAe2H,YAAY,EAAE;IAC9CC,QAAQ,GAAG5H,eAAe6H,MAAM,EAAE;AACpC,EAAC;AAED,6FAA6F,GAC7F,OAAO,SAASC;IACd,MAAMnF,aAAa;QACjBoF,YAAY;YACV1G,OAAO;gBAAC;oBAAEC,MAAM;oBAAUmE,MAAMnD,OAAOC,IAAI,CAACkF;gBAA2B;gBAAG;oBAAEnG,MAAM;gBAAO;aAAE;YAC3FjB,aAAa;QACf;QACA2H,YAAYxH,KAAK;QACjByH,mBAAmBzH,KACjB,CAAC,2EAA2E,EAAE+G,oCAAoC,YAAY,CAAC;QAEjIW,cAAc1H,KAAK;QACnB2H,kBAAkB3H,KAAK;QACvB4H,aAAa5H,KAAK;QAClB6H,WAAW7H,KACT,CAAC,qFAAqF,EAAEgH,gCAAgC,YAAY,CAAC;QAEvIc,UAAU9H,KACR,CAAC,yEAAyE,EAAEgH,gCAAgC,YAAY,CAAC;IAE7H;IACA,OAAO;QACLzE,MAAMuE;QACNjH,aACE;QACF2C,QAAQ;QACRC,aAAa;YACX3B,MAAM;YACNqB;YACAO,UAAUZ,OAAOC,IAAI,CAACI;YACtBQ,sBAAsB;QACxB;IACF;AACF;AASA,MAAMoF,QAAQ;AAEd;;;;CAIC,GACD,OAAO,SAASC,eACd9G,MAA+B,EAC/B4B,OAA8B;IAM9B,MAAMC,SAAsD,CAAC;IAC7D,MAAMkF,QAAkB,EAAE;IAC1B,MAAMjF,aAAoC,EAAE;IAC5C,MAAMC,YAAqC,CAAC;IAC5C,MAAMiF,MAAM,CAAC9F,OAA2B1B;QACtC,IAAIA,SAASoC,QAAQqF,KAAK,CAAClG,GAAG,CAACG,QAAQW,MAAM,CAACX,MAAM,GAAG1B;IACzD;IAEA,MAAMI,OAAOL,SAASS,MAAM,CAAC,aAAa;IAC1C,IAAIJ,QAAQmG,yBAAyB,CAACnG,KAAK,EAAEoH,IAAI,mBAAmBjB,yBAAyB,CAACnG,KAAK;IACnGoH,IAAI,mBAAmBzH,SAASS,MAAM,CAAC,aAAa,EAAEyD,KAAK,CAAC,GAAG;IAE/D,MAAM9E,cAAcY,SAASS,MAAM,CAAC,oBAAoB;IACxD,IAAIrB,YAAY4B,MAAM,GAAGsF,qCAAqC;QAC5D/D,WAAWK,IAAI,CAAC;YACdC,MAAM;YACNC,MAAM;YACNC,SAAS,CAAC,0BAA0B,EAAE3D,YAAY4B,MAAM,CAAC,0BAA0B,EAAEsF,oCAAoC,CAAC,CAAC;QAC7H;QACA9D,SAAS,CAAC,oBAAoB,GAAGpD;IACnC,OAAO;QACLqI,IAAI,0BAA0BrI;IAChC;IAEA,MAAMuI,WAAWtF,QAAQsF,QAAQ,CAAChI,WAAW;IAC7C,MAAMiI,aAAavF,QAAQsF,QAAQ,CAACzH,OAAO,CAAC,QAAQ;IACpD,MAAM2H,QAAQ7H,SAASS,MAAM,CAAC,eAAe;IAC7C,IAAIoH,OAAO;QACT,IAAIP,MAAMQ,IAAI,CAACD,UAAUF,SAASI,QAAQ,CAACF,MAAMlI,WAAW,KAAK8H,IAAI,oBAAoBI;aACpFL,MAAM5E,IAAI,CAAC;IAClB;IACA,MAAMoF,YAAYhI,SAASS,MAAM,CAAC,mBAAmB;IACrD,IAAIuH,WAAW;QACb,MAAMC,SAASD,UAAU9H,OAAO,CAAC,QAAQ;QACzC,IAAI+H,OAAOjH,MAAM,IAAI,KAAK4G,WAAWG,QAAQ,CAACE,SAASR,IAAI,wBAAwBO;aAC9ER,MAAM5E,IAAI,CAAC;IAClB;IACA,IAAIN,MAAM,CAAC,mBAAmB,IAAIA,MAAM,CAAC,uBAAuB,EAAE;QAChEmF,IAAI,0BAA0BzH,SAASS,MAAM,CAAC,cAAc,EAAEyD,KAAK,CAAC,GAAG;IACzE;IAEA,KAAK,MAAM,CAACtD,KAAKe,MAAM,IAAI;QACzB;YAAC;YAAa;SAAsB;QACpC;YAAC;YAAY;SAAqB;KACnC,CAAW;QACV,MAAMuG,WAAWlI,SAASS,MAAM,CAACG,IAAI;QACrC,IAAIsH,SAASlH,MAAM,GAAGuF,iCAAiC;YACrDhE,WAAWK,IAAI,CAAC;gBACdC,MAAM;gBACNC,MAAM;gBACNC,SAAS,GAAGnC,IAAI,IAAI,EAAEsH,SAASlH,MAAM,CAAC,0BAA0B,EAAEuF,gCAAgC,CAAC,CAAC;YACtG;YACA/D,SAAS,CAAC5B,IAAI,GAAGsH;QACnB,OAAO;YACLT,IAAI9F,OAAOuG;QACb;IACF;IACA,OAAO3F,WAAWvB,MAAM,GAAG;QAAEf,OAAO;QAAMsC;QAAYC;IAAU,IAAI;QAAEvC,OAAO;YAAEqC;YAAQkF;QAAM;QAAGjF,YAAY,EAAE;IAAC;AACjH"}
|