@aglyn/plugins-ai 1.0.0-beta.233 → 1.0.0-beta.234
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/components/ai-job-links.d.ts +6 -0
- package/src/lib/components/ai-job-links.js +9 -0
- package/src/lib/components/ai-job-links.js.map +1 -1
- package/src/lib/components/ai-site-build-page.component.js +41 -3
- package/src/lib/components/ai-site-build-page.component.js.map +1 -1
- package/src/lib/components/ai-site-start-card.component.js +91 -8
- package/src/lib/components/ai-site-start-card.component.js.map +1 -1
- package/src/lib/jobs/ai-job-admission.d.ts +2 -1
- package/src/lib/jobs/ai-job-admission.js.map +1 -1
- package/src/lib/jobs/ai-job-email-step.d.ts +39 -0
- package/src/lib/jobs/ai-job-email-step.js +116 -5
- package/src/lib/jobs/ai-job-email-step.js.map +1 -1
- package/src/lib/jobs/ai-job-layout-language.d.ts +3 -0
- package/src/lib/jobs/ai-job-layout-language.js +3 -1
- package/src/lib/jobs/ai-job-layout-language.js.map +1 -1
- package/src/lib/jobs/ai-job-layout-site-pages.d.ts +2 -0
- package/src/lib/jobs/ai-job-layout-site-pages.js +23 -11
- package/src/lib/jobs/ai-job-layout-site-pages.js.map +1 -1
- package/src/lib/jobs/ai-job-layout-step.js +3 -2
- package/src/lib/jobs/ai-job-layout-step.js.map +1 -1
- package/src/lib/jobs/ai-job-page-language.d.ts +16 -0
- package/src/lib/jobs/ai-job-page-language.js +35 -6
- package/src/lib/jobs/ai-job-page-language.js.map +1 -1
- package/src/lib/jobs/ai-job-page-step.d.ts +3 -0
- package/src/lib/jobs/ai-job-page-step.js +18 -4
- package/src/lib/jobs/ai-job-page-step.js.map +1 -1
- package/src/lib/jobs/ai-job-plan-step.d.ts +34 -0
- package/src/lib/jobs/ai-job-plan-step.js +106 -14
- package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
- package/src/lib/jobs/ai-job-site-content.d.ts +6 -6
- package/src/lib/jobs/ai-job-site-content.js +6 -12
- package/src/lib/jobs/ai-job-site-content.js.map +1 -1
- package/src/lib/jobs/ai-job-site-step.d.ts +11 -0
- package/src/lib/jobs/ai-job-site-step.js +96 -44
- package/src/lib/jobs/ai-job-site-step.js.map +1 -1
- package/src/lib/jobs/ai-jobs.d.ts +8 -4
- package/src/lib/jobs/ai-jobs.js +19 -2
- package/src/lib/jobs/ai-jobs.js.map +1 -1
- package/src/lib/jobs/ai-layout-stock-photos.d.ts +98 -0
- package/src/lib/jobs/ai-layout-stock-photos.js +248 -0
- package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -0
- package/src/lib/jobs/ai-site-publish.d.ts +11 -0
- package/src/lib/jobs/ai-site-publish.js +31 -1
- package/src/lib/jobs/ai-site-publish.js.map +1 -1
- package/src/lib/jobs/fixtures/ai-juniper-portfolio-page-recording.d.ts +151 -0
- package/src/lib/jobs/fixtures/ai-juniper-portfolio-page-recording.js +183 -0
- package/src/lib/jobs/fixtures/ai-juniper-portfolio-page-recording.js.map +1 -0
- package/src/lib/jobs/fixtures/ai-slow-roads-site-plan-recording.d.ts +27 -0
- package/src/lib/jobs/fixtures/ai-slow-roads-site-plan-recording.js +192 -0
- package/src/lib/jobs/fixtures/ai-slow-roads-site-plan-recording.js.map +1 -0
- package/src/lib/jobs/fixtures/ai-yoga-site-plan-recording.d.ts +27 -0
- package/src/lib/jobs/fixtures/ai-yoga-site-plan-recording.js +222 -0
- package/src/lib/jobs/fixtures/ai-yoga-site-plan-recording.js.map +1 -0
- package/src/lib/layout-language/ai-layout-compiler.d.ts +29 -0
- package/src/lib/layout-language/ai-layout-compiler.js +698 -12
- package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
- package/src/lib/layout-language/ai-layout-design.d.ts +107 -0
- package/src/lib/layout-language/ai-layout-design.js +197 -0
- package/src/lib/layout-language/ai-layout-design.js.map +1 -0
- package/src/lib/layout-language/ai-layout-frame.d.ts +6 -0
- package/src/lib/layout-language/ai-layout-frame.js +22 -6
- package/src/lib/layout-language/ai-layout-frame.js.map +1 -1
- package/src/lib/layout-language/ai-layout-language.d.ts +2 -0
- package/src/lib/layout-language/ai-layout-language.js +15 -1
- package/src/lib/layout-language/ai-layout-language.js.map +1 -1
- package/src/lib/layout-language/ai-layout-links.d.ts +11 -0
- package/src/lib/layout-language/ai-layout-links.js +4 -0
- package/src/lib/layout-language/ai-layout-links.js.map +1 -1
- package/src/lib/layout-language/ai-layout-pictures.d.ts +11 -5
- package/src/lib/layout-language/ai-layout-pictures.js +80 -17
- package/src/lib/layout-language/ai-layout-pictures.js.map +1 -1
- package/src/lib/model/ai-job-notice.d.ts +9 -1
- package/src/lib/model/ai-job-notice.js +23 -0
- package/src/lib/model/ai-job-notice.js.map +1 -1
- package/src/lib/model/ai-plan-capabilities.d.ts +7 -0
- package/src/lib/model/ai-plan-capabilities.js.map +1 -1
- package/src/lib/model/ai-site-build-progress.d.ts +12 -2
- package/src/lib/model/ai-site-build-progress.js +78 -4
- package/src/lib/model/ai-site-build-progress.js.map +1 -1
- package/src/lib/model/ai-site-job.d.ts +209 -22
- package/src/lib/model/ai-site-job.js +247 -20
- package/src/lib/model/ai-site-job.js.map +1 -1
- package/src/lib/model/ai-site-kinds.js +6 -1
- package/src/lib/model/ai-site-kinds.js.map +1 -1
- package/src/lib/model/ai-site-look.d.ts +21 -0
- package/src/lib/model/ai-site-look.js +63 -14
- package/src/lib/model/ai-site-look.js.map +1 -1
- package/src/lib/runtime/ai-doctrine-validators.d.ts +32 -1
- package/src/lib/runtime/ai-doctrine-validators.js +227 -4
- package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
- package/src/lib/runtime/ai-doctrine.js +5 -3
- package/src/lib/runtime/ai-doctrine.js.map +1 -1
- package/src/lib/runtime/ai-palette.generated.d.ts +2 -0
- package/src/lib/runtime/ai-palette.generated.js +18 -2
- package/src/lib/runtime/ai-palette.generated.js.map +1 -1
- package/src/lib/server/ai-jobs-resume.js.map +1 -1
- package/src/lib/server/ai-jobs-route.js +15 -3
- package/src/lib/server/ai-jobs-route.js.map +1 -1
- package/src/lib/server/assist-edit.js +5 -2
- package/src/lib/server/assist-edit.js.map +1 -1
- package/src/lib/usage/free-ai-credits-left.d.ts +54 -0
- package/src/lib/usage/free-ai-credits-left.js +62 -0
- package/src/lib/usage/free-ai-credits-left.js.map +1 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-email-step.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 type {\n PluginDraftRecord,\n PluginResourceDraftWriter,\n} from '@aglyn/aglyn/plugin-manager/plugin-resource-drafts'\nimport type { NodesMap } from '@aglyn/aglyn/types/nodes'\nimport type { AiJob, AiJobOutput, AiJobPlan } from '../model/ai-jobs.types'\nimport type { AiSiteInventory } from '../model/ai-site-inventory'\nimport { AI_STEP_TIERS, type AiStepKind } from '../providers/catalog'\nimport { AI_ROUTING_TABLE, aiModelForStep } from '../providers/routing'\nimport {\n aiDoctrineTreeTool,\n runValidatedGeneration,\n type AiValidatedGeneration,\n type AiValidatedTree,\n} from '../runtime/ai-doctrine'\nimport {\n walkTree,\n type AiDoctrineNode,\n type AiDoctrineViolation,\n} from '../runtime/ai-doctrine-validators'\nimport { AI_TEXT_LIMITS } from '../runtime/ai-palette'\nimport type { AiSystemBlock, AiTool } from '../runtime/ai-runtime'\nimport { readSiteInventory } from '../runtime/site-inventory'\nimport {\n aiEmailProductNote,\n resolveAiEmailProducts,\n type AiEmailProductBinding,\n} from './ai-email-bindings'\nimport { registerAiJobAdmission, type AiJobAdmission } from './ai-job-admission'\nimport { aiJobPlanCreationsRefusal } from './ai-job-plan-creations'\nimport { aiJobStepBudget, type AiJobStepBudget } from './ai-job-budget'\nimport { aiJobDraftId } from './ai-job-draft-ids'\nimport { aiSiteSubdomain } from './ai-job-drafts'\nimport {\n aiConfirmedPlan,\n aiDoctrineReview,\n aiGenerationSpent,\n aiJobBriefLine,\n aiLimitReview,\n aiModelNodeIds,\n aiPlanCreation,\n aiPlanReferenceLines,\n aiUnspentOutcome,\n} from './ai-job-generation'\nimport {\n aiPluginDraftAdmissionRefusal,\n aiPluginDraftWriter,\n type AiPluginDraftWriterLookup,\n} from './ai-job-plugin-drafts'\nimport type { AiJobStepOutcome, AiJobStepRunner } from './ai-job-text-step'\nimport { registerAiJobStep } from './ai-jobs'\n\n/**\n * The email step (AGL-2912): the generation step of an `email` job, run once a\n * member confirmed the job's plan, and the generation the campaign step\n * drafts its email with.\n *\n * It builds ONE email design from the email palette — sections holding text,\n * buttons, images, dividers, spacers and product blocks, never hand-written\n * HTML, which is not on the email surface — through\n * `runValidatedGeneration('email', …)`, so every building rule holds on the\n * tree, rule 6's brand and rule 17's clip size included. Beside the design the\n * answer carries three subject lines and three preheaders. The door's checks\n * ride `extend`:\n *\n * - every block sits in a section, every button links somewhere, and the\n * copy's only merge tokens are the recipient's own, in text;\n * - exactly as many product blocks as the job binds products;\n * - three different subject lines and preheaders, within their lengths and\n * free of merge tokens, which a subject line does not fill;\n * - and the design RENDERS: the email plugin's writer checks it through the\n * send path's own renderer, reached through the core's resource-drafts\n * seam, and a problem it names is a violation the model is re-asked with.\n *\n * What the model is sent is the brief, the email's name and kind, the\n * confirmed plan and the site inventory, and how many product blocks to place.\n * The products themselves are bound by id after it answers\n * (`ai-email-bindings.ts`); a link to one of the site's pages is completed\n * with the site's address token, which the send fills.\n *\n * The design lands through that same writer: a draft email screen, listed on\n * the Emails page's templates, that no campaign sends until a member picks it.\n * The job never sends, schedules or publishes anything.\n *\n * Like the other generation steps, it runs on the job beat, never at an inline\n * door: it registers the least time its lookup rounds, its answer and its\n * re-ask need (AGL-3035), and runs without extended thinking under a tighter\n * answer ceiling than the doctrine's default for an email, lowered on a tier\n * too slow to answer it inside that time.\n */\n\n/** The resource the email plugin writes an email design under. */\nexport const AI_EMAIL_DESIGN_RESOURCE = 'emailDesign'\n\n/** The longest answer an email may run to; the tree is held to the email budget either way. */\nexport const AI_JOB_EMAIL_MAX_TOKENS = AI_ROUTING_TABLE['job.email'].maxTokens\n\n/**\n * The time an email generation takes (AGL-3035, AGL-3036), for each step kind\n * that runs one: its two inventory-lookup rounds, its answer and its re-ask at\n * the kind's routing ceiling on the tier the kind is served from, fitted to\n * what a beat can give a step. The email step registers the first, and the\n * campaign step — whose email is this same generation — the second.\n */\nexport const AI_JOB_EMAIL_STEP_BUDGETS: Readonly<\n Record<AiEmailGenerationInput['step'], AiJobStepBudget>\n> = {\n 'job.email': aiJobStepBudget({\n tier: AI_STEP_TIERS['job.email'],\n maxTokens: AI_ROUTING_TABLE['job.email'].maxTokens,\n }),\n 'job.campaign': aiJobStepBudget({\n tier: AI_STEP_TIERS['job.campaign'],\n maxTokens: AI_ROUTING_TABLE['job.campaign'].maxTokens,\n }),\n}\n\n/** The least time one email step needs before it starts. */\nexport const AI_JOB_EMAIL_STEP_MINIMUM_MS = AI_JOB_EMAIL_STEP_BUDGETS['job.email'].minimumMs\n\n/** How many subject lines, and how many preheaders, an email is written with. */\nexport const AI_EMAIL_VARIANTS = 3\n\n/** A subject line is a headline. */\nexport const AI_EMAIL_SUBJECT_MAX_CHARS = AI_TEXT_LIMITS.headline\n\n/** The longest preheader the campaign send route stores. */\nexport const AI_EMAIL_PREHEADER_MAX_CHARS = 200\n\n/** An email's name when neither the plan nor the job names one. */\nexport const AI_JOB_EMAIL_DEFAULT_NAME = 'New email'\n\n/** A job's failure when the site has no email design writer. */\nexport const AI_EMAIL_UNAVAILABLE_COPY = 'Email is not available on this site.'\n\n/** A job's failure when a design that passed every check could not be stored. */\nexport const AI_EMAIL_SAVE_FAILURE_COPY =\n 'The email was written but could not be saved. Try the job again.'\n\n/**\n * The kinds of email a job may carry, as `inputs.emailType`, and how the\n * prompt says each.\n *\n * A member picks one for an email of their own; a job the platform composes\n * sets one for itself, which is what `reply` is for (AGL-2918, named by the\n * site model's `AI_SITE_EMAIL_TYPE`). A scaffold's email answers one person\n * who wrote in, and none of the five campaign kinds says that: the nearest,\n * `welcome`, names a signup the site has not had, and a kind that\n * misdescribes the email is worse than no kind at all, because this line is\n * what the model reads first.\n *\n * Every label is the step's own words, so none of them may name a list, a\n * recipient or the people an email is sent to — the same rule the step's\n * instructions keep, held over the whole catalog by `ai-job-email-step.spec`.\n */\nexport const AI_EMAIL_TYPES = {\n welcome: 'a welcome email for someone who just signed up',\n newsletter: 'a newsletter',\n launch: 'a launch announcement',\n abandonedCart: 'a reminder about items left in a cart',\n eventReminder: 'a reminder about an upcoming event',\n reply: 'a reply to somebody who got in touch through the site',\n} as const\n\nexport type AiEmailType = keyof typeof AI_EMAIL_TYPES\n\n/** The merge tokens a generated email's copy may carry: the recipient's own, which each send fills. */\nexport const AI_EMAIL_TEXT_MERGE_TOKENS: readonly string[] = [\n 'contact.firstName',\n 'contact.name',\n 'contact.email',\n]\n\n/** The token the send fills with the site's address, set in front of a link to one of its pages. */\nexport const AI_EMAIL_SITE_URL_TOKEN = '{{site.url}}'\n\n/** The email step's own instructions, cached after the doctrine. */\nexport const AI_JOB_EMAIL_INSTRUCTIONS: readonly AiSystemBlock[] = [\n {\n text: [\n 'You write one marketing email for the owner of a website: its design as one flat node map, three subject lines and three preheaders. Answer with submit_email.',\n 'The root is the document wrapper (div, id \"_@_\") and holds only Email sections (emailSection). Each section is a band holding the email’s blocks from top to bottom: Email text (emailText), Email buttons (emailButton), Email images (emailImage), Email dividers (emailDivider), Email spacers (emailSpacer) and, only when the job binds products, Email product blocks (emailProduct).',\n 'Open with a heading (emailText with variant heading), keep each paragraph short, and give the email one main call to action as a button. Greet the reader by first name with {{contact.firstName}} where a greeting fits. The only merge tokens are {{contact.firstName}}, {{contact.name}} and {{contact.email}}, and only in text: never in a link, a subject line or a preheader.',\n 'Every button links somewhere: a page of the site by its address from the site inventory, such as /menu, which is completed with the site’s domain when the email is sent, or a full https address the brief gives.',\n 'An image has an empty src for the owner to fill from the media library, alt text describing the picture it should show, and a width of 600 or less.',\n 'Place exactly as many Email product blocks as the job binds products, one for each, in the order the brief names them. Each shows its product’s current name, price and picture when the email is sent, so never type a product’s price.',\n 'Leave out an unsubscribe link and a postal address: the platform adds the unsubscribe link to every campaign email. A fact the brief does not give, such as a date or a place, goes in square brackets.',\n `Subject lines: three that take different approaches, such as direct, curious and benefit-led, each at most ${AI_EMAIL_SUBJECT_MAX_CHARS} characters and best under 60, with no merge token. Preheaders: three, each extending the subject line in the same position, at most ${AI_EMAIL_PREHEADER_MAX_CHARS} characters and best between 40 and 110.`,\n ].join('\\n'),\n },\n]\n\nconst DOCTRINE_EMAIL_TOOL = aiDoctrineTreeTool('email')\n\n/** The doctrine's email tree tool, with the subject lines and preheaders beside the tree. */\nexport const AI_JOB_EMAIL_TOOL: AiTool = {\n name: DOCTRINE_EMAIL_TOOL.name,\n description:\n 'Submit the email: its design as one flat node map, three subject lines and three preheaders.',\n strict: true,\n inputSchema: {\n type: 'object',\n additionalProperties: false,\n required: ['tree', 'subjects', 'preheaders'],\n properties: {\n tree: (DOCTRINE_EMAIL_TOOL.inputSchema['properties'] as Record<string, unknown>)['tree'],\n subjects: {\n type: 'array',\n items: { type: 'string' },\n description: 'Three subject lines that take different approaches, the strongest first.',\n },\n preheaders: {\n type: 'array',\n items: { type: 'string' },\n description: 'Three preheaders, each extending the subject line in the same position.',\n },\n },\n },\n}\n\n/** How the prompt names the kind of email a member picked; `null` for none or an unknown one. */\nexport function aiEmailTypeLabel(value: unknown): string | null {\n return typeof value === 'string' && Object.prototype.hasOwnProperty.call(AI_EMAIL_TYPES, value)\n ? AI_EMAIL_TYPES[value as AiEmailType]\n : null\n}\n\n/** A name the member gave the job, on one line and bounded; empty when none. */\nexport function aiJobInputName(job: Pick<AiJob, 'inputs'>): string {\n const raw = job.inputs?.['name']\n return typeof raw === 'string' ? raw.replace(/\\s+/g, ' ').trim().slice(0, 120) : ''\n}\n\n/** The email job as the generation's user turn. */\nexport function aiJobEmailPrompt(\n job: Pick<AiJob, 'brief' | 'inputs'>,\n plan: AiJobPlan | null,\n heading: string | null,\n products: number,\n): string {\n const lines = heading ? [heading] : []\n const type = aiEmailTypeLabel(job.inputs?.['emailType'])\n if (type) lines.push(`Kind of email: ${type}`)\n lines.push(aiJobBriefLine(job), ...aiPlanReferenceLines(plan))\n lines.push(\n products > 0\n ? `Products bound to this email: ${products}. Place exactly ${products} Email product ${\n products === 1 ? 'block' : 'blocks'\n }.`\n : 'No products are bound to this email: place no Email product block.',\n )\n return lines.join('\\n')\n}\n\n/** The subject lines and preheaders an answer carries. */\nexport interface AiEmailCopy {\n subjects: string[]\n preheaders: string[]\n}\n\nfunction oneLine(value: string): string {\n return value.replace(/\\s+/g, ' ').trim()\n}\n\nexport function parseAiEmailCopy(answer: Readonly<Record<string, unknown>>): AiEmailCopy {\n const lines = (value: unknown) =>\n Array.isArray(value)\n ? value.filter((entry): entry is string => typeof entry === 'string').map(oneLine)\n : []\n return { subjects: lines(answer['subjects']), preheaders: lines(answer['preheaders']) }\n}\n\n/** The copy's own rules: three different lines of each, within their lengths, with no merge token. */\nexport function aiEmailCopyViolations(copy: AiEmailCopy): AiDoctrineViolation[] {\n const violations: AiDoctrineViolation[] = []\n const distinct = (lines: string[]) =>\n new Set(lines.filter(Boolean).map((line) => line.toLowerCase())).size\n if (copy.subjects.length !== AI_EMAIL_VARIANTS || distinct(copy.subjects) !== AI_EMAIL_VARIANTS) {\n violations.push({\n rule: null,\n code: 'email-subjects',\n message: `Write exactly ${AI_EMAIL_VARIANTS} different subject lines.`,\n })\n }\n if (copy.preheaders.length !== AI_EMAIL_VARIANTS || distinct(copy.preheaders) !== AI_EMAIL_VARIANTS) {\n violations.push({\n rule: null,\n code: 'email-preheaders',\n message: `Write exactly ${AI_EMAIL_VARIANTS} different preheaders, one for each subject line.`,\n })\n }\n if (copy.subjects.some((line) => line.length > AI_EMAIL_SUBJECT_MAX_CHARS)) {\n violations.push({\n rule: null,\n code: 'email-subject-length',\n message: `A subject line is over ${AI_EMAIL_SUBJECT_MAX_CHARS} characters. Keep each within ${AI_EMAIL_SUBJECT_MAX_CHARS}, and best under 60.`,\n })\n }\n if (copy.preheaders.some((line) => line.length > AI_EMAIL_PREHEADER_MAX_CHARS)) {\n violations.push({\n rule: null,\n code: 'email-preheader-length',\n message: `A preheader is over ${AI_EMAIL_PREHEADER_MAX_CHARS} characters. Keep each within ${AI_EMAIL_PREHEADER_MAX_CHARS}.`,\n })\n }\n if ([...copy.subjects, ...copy.preheaders].some((line) => line.includes('{{'))) {\n violations.push({\n rule: null,\n code: 'email-header-token',\n message:\n 'A subject line or preheader carries a merge token, which is not filled there. Write them without {{…}}.',\n })\n }\n return violations\n}\n\nconst MERGE_TOKEN = /\\{\\{\\s*([^}]*?)\\s*\\}\\}/g\n\nfunction visitsOf(tree: Pick<AiValidatedTree, 'rootId' | 'nodes'>) {\n return walkTree({\n rootId: tree.rootId,\n nodes: tree.nodes as unknown as Record<string, AiDoctrineNode>,\n })\n}\n\n/** The design's own rules, on a tree the doctrine admitted. */\nexport function aiEmailTreeViolations(tree: AiValidatedTree, products: number): AiDoctrineViolation[] {\n const violations: AiDoctrineViolation[] = []\n const loose = (tree.nodes[tree.rootId]?.nodes ?? []).filter(\n (id) => tree.nodes[id]?.componentId !== 'emailSection',\n )\n if (loose.length) {\n violations.push({\n rule: null,\n code: 'email-loose-block',\n message: 'A block sits outside every Email section. Put each block inside a section.',\n nodeIds: aiModelNodeIds(loose, tree.sourceIds),\n })\n }\n const tokens = new Set<string>()\n const tokenNodes: string[] = []\n const unlinked: string[] = []\n let placed = 0\n for (const { id, node } of visitsOf(tree)) {\n for (const value of Object.values(node.props ?? {})) {\n if (typeof value !== 'string') continue\n for (const match of value.matchAll(MERGE_TOKEN)) {\n if (!AI_EMAIL_TEXT_MERGE_TOKENS.includes(match[1])) {\n tokens.add(`{{${match[1]}}}`)\n tokenNodes.push(id)\n }\n }\n }\n if (node.componentId === 'emailButton' && typeof node.props?.['href'] !== 'string') {\n unlinked.push(id)\n }\n if (node.componentId === 'emailProduct') placed += 1\n }\n if (tokens.size) {\n violations.push({\n rule: null,\n code: 'email-merge-token',\n message: `This email writes ${[...tokens].slice(0, 3).join(', ')}, which a generated email may not use. Use only {{contact.firstName}}, {{contact.name}} or {{contact.email}}, and only in text.`,\n nodeIds: aiModelNodeIds([...new Set(tokenNodes)], tree.sourceIds),\n })\n }\n if (unlinked.length) {\n violations.push({\n rule: null,\n code: 'email-button-link',\n message:\n 'An Email button links nowhere. Link each to a page of the site by its address, such as /menu, or to a full https address the brief gives.',\n nodeIds: aiModelNodeIds(unlinked, tree.sourceIds),\n })\n }\n if (placed !== products) {\n violations.push({\n rule: null,\n code: 'email-product-count',\n message: products\n ? `This email binds ${products} ${products === 1 ? 'product' : 'products'}: place exactly ${products} Email product ${\n products === 1 ? 'block' : 'blocks'\n }.`\n : 'No products are bound to this email: remove the Email product blocks.',\n })\n }\n return violations\n}\n\n/**\n * The design as it is stored: a link to one of the site's pages completed with\n * the site's address token, and each product block bound, in document order,\n * to the next product the job binds.\n */\nexport function aiEmailDesignNodes(\n tree: Pick<AiValidatedTree, 'rootId' | 'nodes'>,\n productIds: readonly string[],\n): NodesMap {\n const nodes: NodesMap = { ...tree.nodes }\n let next = 0\n for (const { id, node } of visitsOf(tree)) {\n const props: Record<string, unknown> = { ...(node.props ?? {}) }\n let changed = false\n const href = props['href']\n if (typeof href === 'string' && href.startsWith('/') && !href.startsWith('//')) {\n props['href'] = `${AI_EMAIL_SITE_URL_TOKEN}${href}`\n changed = true\n }\n if (node.componentId === 'emailProduct') {\n const productId = productIds[next]\n next += 1\n if (productId) {\n props['productId'] = productId\n changed = true\n }\n }\n if (changed) nodes[id] = { ...nodes[id], props } as NodesMap[string]\n }\n return nodes\n}\n\n/** The content the email plugin's writer stores a design from. */\nexport function aiEmailDesignContent(nodes: NodesMap, copy: AiEmailCopy): Record<string, unknown> {\n return {\n nodes,\n subject: copy.subjects[0] ?? '',\n preheader: copy.preheaders[0] ?? '',\n subjectVariants: copy.subjects,\n preheaderVariants: copy.preheaders,\n }\n}\n\n/** What the email plugin's renderer says about the design, as violations the model is re-asked with. */\nexport function aiEmailRenderViolations(\n design: PluginResourceDraftWriter,\n hostId: string,\n nodes: NodesMap,\n copy: AiEmailCopy,\n): AiDoctrineViolation[] {\n const checked = design.check(aiEmailDesignContent(nodes, copy), { hostId })\n if (checked.ok === true) return []\n return [...new Set(checked.problems)].map((problem) => ({\n rule: null,\n code: 'email-render',\n message: `The email does not render as written: ${problem}.`,\n }))\n}\n\nexport interface AiEmailGenerationInput {\n job: AiJob\n step: Extract<AiStepKind, 'job.email' | 'job.campaign'>\n model: string\n /** The line naming what is built, such as `Email name: …`; `null` for none. */\n heading: string | null\n plan: AiJobPlan | null\n inventory: AiSiteInventory | null\n hostId: string\n products: AiEmailProductBinding\n /** The email plugin's design writer, whose check the design must pass. */\n design: PluginResourceDraftWriter\n signal?: AbortSignal\n}\n\nexport interface AiEmailGeneration {\n result: AiValidatedGeneration<AiValidatedTree>\n /** The copy of the answer the doctrine kept; `null` unless it kept one. */\n copy: AiEmailCopy | null\n /** The kept design as it is stored; `null` unless the doctrine kept one. */\n nodes: NodesMap | null\n}\n\n/** Generates one email under the doctrine, with the email's own checks riding `extend`. */\nexport async function generateAiEmail(input: AiEmailGenerationInput): Promise<AiEmailGeneration> {\n // The answer `extend` saw last is the one the doctrine kept when it keeps one.\n let last = null as { copy: AiEmailCopy; nodes: NodesMap } | null\n const result = await runValidatedGeneration('email', {\n step: input.step,\n model: input.model,\n instructions: AI_JOB_EMAIL_INSTRUCTIONS,\n inventory: input.inventory,\n messages: [\n {\n role: 'user',\n content: aiJobEmailPrompt(input.job, input.plan, input.heading, input.products.ids.length),\n },\n ],\n tool: AI_JOB_EMAIL_TOOL,\n maxTokens: AI_JOB_EMAIL_STEP_BUDGETS[input.step].maxTokens(input.model),\n thinking: 'off',\n extend: (tree, answer) => {\n const copy = parseAiEmailCopy(answer)\n const nodes = aiEmailDesignNodes(tree, input.products.ids)\n last = { copy, nodes }\n const violations = [\n ...aiEmailTreeViolations(tree, input.products.ids.length),\n ...aiEmailCopyViolations(copy),\n ]\n // Rendered once the design's own rules hold, so a problem is named once.\n return violations.length\n ? violations\n : aiEmailRenderViolations(input.design, input.hostId, nodes, copy)\n },\n ...(input.signal ? { signal: input.signal } : {}),\n })\n const kept = result.status === 'ok' ? last : null\n return { result, copy: kept?.copy ?? null, nodes: kept?.nodes ?? null }\n}\n\n/** An email design as the job's output. */\nexport function aiEmailDesignOutput(\n record: PluginDraftRecord,\n place: { hostId: string; hostSubdomain: string | null },\n note: string | null,\n): AiJobOutput {\n return {\n resource: 'emailScreen',\n id: record.id,\n versionId: record.versionId,\n hostId: place.hostId,\n hostSubdomain: place.hostSubdomain,\n label: record.name,\n ...(note ? { note } : {}),\n }\n}\n\n/**\n * An email job is admitted for a site of its org with email on and room for a\n * design, and confirmed only for a plan that creates nothing but the design:\n * an email places nothing another job builds (AGL-3143 §15).\n */\nexport const aiEmailJobAdmission: AiJobAdmission = async (context) => {\n const refusal = await aiPluginDraftAdmissionRefusal(context, {\n kind: 'email',\n drafts: [{ resource: AI_EMAIL_DESIGN_RESOURCE, label: 'Email' }],\n })\n if (refusal || !context.hostId) return refusal\n return aiJobPlanCreationsRefusal('email', context, context.hostId)\n}\n\nexport interface AiJobEmailStepDeps {\n /** The inventory reader; specs hand in a fake. */\n readInventory?: typeof readSiteInventory\n /** How a resource's draft writer is found; the core's registry otherwise. */\n writerFor?: AiPluginDraftWriterLookup\n}\n\nexport function createAiJobEmailStep(deps: AiJobEmailStepDeps = {}): AiJobStepRunner {\n const readInventory = deps.readInventory ?? readSiteInventory\n const writerFor = deps.writerFor ?? aiPluginDraftWriter\n return async ({ job, now, signal, firestore, modelFor }): Promise<AiJobStepOutcome> => {\n const hostId = job.hostId\n if (!hostId) throw new Error('an email job names no site, and its admission refuses one')\n // The switch's answer for this job, else the routing table's (AGL-2942).\n const model = modelFor?.('job.email') ?? aiModelForStep('job.email')\n const design = writerFor(AI_EMAIL_DESIGN_RESOURCE)\n if (!design) return { ...aiUnspentOutcome(model), failure: AI_EMAIL_UNAVAILABLE_COPY }\n\n // A run cut off after its design was written reports that design, found by the id the job recorded.\n const draftId = aiJobDraftId(job, 'email')\n const written = await design.read({ hostId, id: draftId })\n if (written) {\n const hostSubdomain = await aiSiteSubdomain(firestore, hostId)\n return aiUnspentOutcome(model, {\n outputs: [aiEmailDesignOutput(written, { hostId, hostSubdomain }, null)],\n })\n }\n\n const [inventory, orgSnapshot, products, hostSubdomain] = await Promise.all([\n readInventory(job.orgId, hostId, { firestore }),\n firestore.collection('orgs').doc(job.orgId).get(),\n resolveAiEmailProducts({ hostId, brief: job.brief, inputs: job.inputs }),\n aiSiteSubdomain(firestore, hostId),\n ])\n const org = (orgSnapshot.data() ?? null) as Record<string, unknown> | null\n const context = { orgId: job.orgId, hostId, uid: job.createdBy, org, now }\n const refusal = await design.refusal(context)\n if (refusal) {\n return refusal.status === 403\n ? aiUnspentOutcome(model, { review: aiLimitReview(refusal.error) })\n : { ...aiUnspentOutcome(model), failure: refusal.error }\n }\n\n const plan = aiConfirmedPlan(job)\n const name =\n aiPlanCreation(plan, 'email')?.name || aiJobInputName(job) || AI_JOB_EMAIL_DEFAULT_NAME\n const generated = await generateAiEmail({\n job,\n step: 'job.email',\n model,\n heading: `Email name: ${name}`,\n plan,\n inventory,\n hostId,\n products,\n design,\n ...(signal ? { signal } : {}),\n })\n const { result } = generated\n const spent = aiGenerationSpent(result)\n if (result.status === 'refused') return { ...spent, refused: true }\n if (result.status === 'needs_input') return { ...spent, review: aiDoctrineReview(result) }\n if (!generated.copy || !generated.nodes) return { ...spent, failure: AI_EMAIL_SAVE_FAILURE_COPY }\n\n const write = await design.write({\n ...context,\n id: draftId,\n name,\n content: aiEmailDesignContent(generated.nodes, generated.copy),\n })\n if (write.ok === false) {\n return write.status === 403\n ? { ...spent, review: aiLimitReview(write.error) }\n : { ...spent, failure: AI_EMAIL_SAVE_FAILURE_COPY }\n }\n return {\n ...spent,\n outputs: [aiEmailDesignOutput(write, { hostId, hostSubdomain }, aiEmailProductNote(products))],\n }\n }\n}\n\nexport const runAiJobEmailStep = createAiJobEmailStep()\n\n/** Registers the email step and the check a email job passes before it is created or resumed. */\nexport function registerAiEmailJob(): void {\n registerAiJobStep('email', runAiJobEmailStep, { minimumMs: AI_JOB_EMAIL_STEP_MINIMUM_MS })\n registerAiJobAdmission('email', aiEmailJobAdmission)\n}\n"],"names":["AI_STEP_TIERS","AI_ROUTING_TABLE","aiModelForStep","aiDoctrineTreeTool","runValidatedGeneration","walkTree","AI_TEXT_LIMITS","readSiteInventory","aiEmailProductNote","resolveAiEmailProducts","registerAiJobAdmission","aiJobPlanCreationsRefusal","aiJobStepBudget","aiJobDraftId","aiSiteSubdomain","aiConfirmedPlan","aiDoctrineReview","aiGenerationSpent","aiJobBriefLine","aiLimitReview","aiModelNodeIds","aiPlanCreation","aiPlanReferenceLines","aiUnspentOutcome","aiPluginDraftAdmissionRefusal","aiPluginDraftWriter","registerAiJobStep","AI_EMAIL_DESIGN_RESOURCE","AI_JOB_EMAIL_MAX_TOKENS","maxTokens","AI_JOB_EMAIL_STEP_BUDGETS","tier","AI_JOB_EMAIL_STEP_MINIMUM_MS","minimumMs","AI_EMAIL_VARIANTS","AI_EMAIL_SUBJECT_MAX_CHARS","headline","AI_EMAIL_PREHEADER_MAX_CHARS","AI_JOB_EMAIL_DEFAULT_NAME","AI_EMAIL_UNAVAILABLE_COPY","AI_EMAIL_SAVE_FAILURE_COPY","AI_EMAIL_TYPES","welcome","newsletter","launch","abandonedCart","eventReminder","reply","AI_EMAIL_TEXT_MERGE_TOKENS","AI_EMAIL_SITE_URL_TOKEN","AI_JOB_EMAIL_INSTRUCTIONS","text","join","DOCTRINE_EMAIL_TOOL","AI_JOB_EMAIL_TOOL","name","description","strict","inputSchema","type","additionalProperties","required","properties","tree","subjects","items","preheaders","aiEmailTypeLabel","value","Object","prototype","hasOwnProperty","call","aiJobInputName","job","raw","inputs","replace","trim","slice","aiJobEmailPrompt","plan","heading","products","lines","push","oneLine","parseAiEmailCopy","answer","Array","isArray","filter","entry","map","aiEmailCopyViolations","copy","violations","distinct","Set","Boolean","line","toLowerCase","size","length","rule","code","message","some","includes","MERGE_TOKEN","visitsOf","rootId","nodes","aiEmailTreeViolations","loose","id","componentId","nodeIds","sourceIds","tokens","tokenNodes","unlinked","placed","node","values","props","match","matchAll","add","aiEmailDesignNodes","productIds","next","changed","href","startsWith","productId","aiEmailDesignContent","subject","preheader","subjectVariants","preheaderVariants","aiEmailRenderViolations","design","hostId","checked","check","ok","problems","problem","generateAiEmail","input","last","result","step","model","instructions","inventory","messages","role","content","ids","tool","thinking","extend","signal","kept","status","aiEmailDesignOutput","record","place","note","resource","versionId","hostSubdomain","label","aiEmailJobAdmission","context","refusal","kind","drafts","createAiJobEmailStep","deps","readInventory","writerFor","now","firestore","modelFor","orgSnapshot","Error","failure","draftId","written","read","outputs","Promise","all","orgId","collection","doc","get","brief","org","data","uid","createdBy","review","error","generated","spent","refused","write","runAiJobEmailStep","registerAiEmailJob"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-email-step.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 type {\n PluginDraftRecord,\n PluginResourceDraftWriter,\n} from '@aglyn/aglyn/plugin-manager/plugin-resource-drafts'\nimport type { NodesMap } from '@aglyn/aglyn/types/nodes'\nimport type { AiJob, AiJobOutput, AiJobPlan } from '../model/ai-jobs.types'\nimport type { AiSiteInventory } from '../model/ai-site-inventory'\nimport { AI_STEP_TIERS, type AiStepKind } from '../providers/catalog'\nimport { AI_ROUTING_TABLE, aiModelForStep } from '../providers/routing'\nimport {\n aiDoctrineTreeTool,\n runValidatedGeneration,\n type AiValidatedGeneration,\n type AiValidatedTree,\n} from '../runtime/ai-doctrine'\nimport {\n walkTree,\n type AiDoctrineNode,\n type AiDoctrineViolation,\n} from '../runtime/ai-doctrine-validators'\nimport { AI_TEXT_LIMITS } from '../runtime/ai-palette'\nimport type { AiSystemBlock, AiTool } from '../runtime/ai-runtime'\nimport { readSiteInventory } from '../runtime/site-inventory'\nimport {\n aiEmailProductNote,\n resolveAiEmailProducts,\n type AiEmailProductBinding,\n} from './ai-email-bindings'\nimport { registerAiJobAdmission, type AiJobAdmission } from './ai-job-admission'\nimport { aiJobPlanCreationsRefusal } from './ai-job-plan-creations'\nimport { aiJobStepBudget, type AiJobStepBudget } from './ai-job-budget'\nimport { aiJobDraftId } from './ai-job-draft-ids'\nimport { aiSiteSubdomain } from './ai-job-drafts'\nimport {\n aiConfirmedPlan,\n aiDoctrineReview,\n aiGenerationSpent,\n aiJobBriefLine,\n aiLimitReview,\n aiModelNodeIds,\n aiPlanCreation,\n aiPlanReferenceLines,\n aiUnspentOutcome,\n} from './ai-job-generation'\nimport {\n aiPluginDraftAdmissionRefusal,\n aiPluginDraftWriter,\n type AiPluginDraftWriterLookup,\n} from './ai-job-plugin-drafts'\nimport type { AiJobStepOutcome, AiJobStepRunner } from './ai-job-text-step'\nimport { registerAiJobStep } from './ai-jobs'\n\n/**\n * The email step (AGL-2912): the generation step of an `email` job, run once a\n * member confirmed the job's plan, and the generation the campaign step\n * drafts its email with.\n *\n * It builds ONE email design from the email palette — sections holding text,\n * buttons, images, dividers, spacers and product blocks, never hand-written\n * HTML, which is not on the email surface — through\n * `runValidatedGeneration('email', …)`, so every building rule holds on the\n * tree, rule 6's brand and rule 17's clip size included. Beside the design the\n * answer carries three subject lines and three preheaders. The door's checks\n * ride `extend`:\n *\n * - every block sits in a section, every button links somewhere, and the\n * copy's only merge tokens are the recipient's own, in text;\n * - exactly as many product blocks as the job binds products;\n * - three different subject lines and preheaders, within their lengths and\n * free of merge tokens, which a subject line does not fill;\n * - and the design RENDERS: the email plugin's writer checks it through the\n * send path's own renderer, reached through the core's resource-drafts\n * seam, and a problem it names is a violation the model is re-asked with.\n *\n * What the model is sent is the brief, the email's name and kind, the\n * confirmed plan and the site inventory, and how many product blocks to place.\n * The products themselves are bound by id after it answers\n * (`ai-email-bindings.ts`); a link to one of the site's pages is completed\n * with the site's address token, which the send fills.\n *\n * The design lands through that same writer: a draft email screen, listed on\n * the Emails page's templates, that no campaign sends until a member picks it.\n * The job never sends, schedules or publishes anything.\n *\n * Like the other generation steps, it runs on the job beat, never at an inline\n * door: it registers the least time its lookup rounds, its answer and its\n * re-ask need (AGL-3035), and runs without extended thinking under a tighter\n * answer ceiling than the doctrine's default for an email, lowered on a tier\n * too slow to answer it inside that time.\n */\n\n/** The resource the email plugin writes an email design under. */\nexport const AI_EMAIL_DESIGN_RESOURCE = 'emailDesign'\n\n/** The longest answer an email may run to; the tree is held to the email budget either way. */\nexport const AI_JOB_EMAIL_MAX_TOKENS = AI_ROUTING_TABLE['job.email'].maxTokens\n\n/**\n * The time an email generation takes (AGL-3035, AGL-3036), for each step kind\n * that runs one: its two inventory-lookup rounds, its answer and its re-ask at\n * the kind's routing ceiling on the tier the kind is served from, fitted to\n * what a beat can give a step. The email step registers the first, and the\n * campaign step — whose email is this same generation — the second.\n */\nexport const AI_JOB_EMAIL_STEP_BUDGETS: Readonly<\n Record<AiEmailGenerationInput['step'], AiJobStepBudget>\n> = {\n 'job.email': aiJobStepBudget({\n tier: AI_STEP_TIERS['job.email'],\n maxTokens: AI_ROUTING_TABLE['job.email'].maxTokens,\n }),\n 'job.campaign': aiJobStepBudget({\n tier: AI_STEP_TIERS['job.campaign'],\n maxTokens: AI_ROUTING_TABLE['job.campaign'].maxTokens,\n }),\n}\n\n/** The least time one email step needs before it starts. */\nexport const AI_JOB_EMAIL_STEP_MINIMUM_MS = AI_JOB_EMAIL_STEP_BUDGETS['job.email'].minimumMs\n\n/** How many subject lines, and how many preheaders, an email is written with. */\nexport const AI_EMAIL_VARIANTS = 3\n\n/** A subject line is a headline. */\nexport const AI_EMAIL_SUBJECT_MAX_CHARS = AI_TEXT_LIMITS.headline\n\n/** The longest preheader the campaign send route stores. */\nexport const AI_EMAIL_PREHEADER_MAX_CHARS = 200\n\n/** An email's name when neither the plan nor the job names one. */\nexport const AI_JOB_EMAIL_DEFAULT_NAME = 'New email'\n\n/** A job's failure when the site has no email design writer. */\nexport const AI_EMAIL_UNAVAILABLE_COPY = 'Email is not available on this site.'\n\n/** A job's failure when a design that passed every check could not be stored. */\nexport const AI_EMAIL_SAVE_FAILURE_COPY =\n 'The email was written but could not be saved. Try the job again.'\n\n/**\n * The kinds of email a job may carry, as `inputs.emailType`, and how the\n * prompt says each.\n *\n * A member picks one for an email of their own; a job the platform composes\n * sets one for itself, which is what `reply` is for (AGL-2918, named by the\n * site model's `AI_SITE_EMAIL_TYPE`). A scaffold's email answers one person\n * who wrote in, and none of the five campaign kinds says that: the nearest,\n * `welcome`, names a signup the site has not had, and a kind that\n * misdescribes the email is worse than no kind at all, because this line is\n * what the model reads first.\n *\n * Every label is the step's own words, so none of them may name a list, a\n * recipient or the people an email is sent to — the same rule the step's\n * instructions keep, held over the whole catalog by `ai-job-email-step.spec`.\n */\nexport const AI_EMAIL_TYPES = {\n welcome: 'a welcome email for someone who just signed up',\n newsletter: 'a newsletter',\n launch: 'a launch announcement',\n abandonedCart: 'a reminder about items left in a cart',\n eventReminder: 'a reminder about an upcoming event',\n reply: 'a reply to somebody who got in touch through the site',\n} as const\n\nexport type AiEmailType = keyof typeof AI_EMAIL_TYPES\n\n/** The merge tokens a generated email's copy may carry: the recipient's own, which each send fills. */\nexport const AI_EMAIL_TEXT_MERGE_TOKENS: readonly string[] = [\n 'contact.firstName',\n 'contact.name',\n 'contact.email',\n]\n\n/** The token the send fills with the site's address, set in front of a link to one of its pages. */\nexport const AI_EMAIL_SITE_URL_TOKEN = '{{site.url}}'\n\n/** The email step's own instructions, cached after the doctrine. */\nexport const AI_JOB_EMAIL_INSTRUCTIONS: readonly AiSystemBlock[] = [\n {\n text: [\n 'You write one marketing email for the owner of a website: its design as one flat node map, three subject lines and three preheaders. Answer with submit_email.',\n 'The root is the document wrapper (div, id \"_@_\") and holds only Email sections (emailSection). Each section is a band holding the email’s blocks from top to bottom: Email text (emailText), Email buttons (emailButton), Email images (emailImage), Email dividers (emailDivider), Email spacers (emailSpacer) and, only when the job binds products, Email product blocks (emailProduct).',\n 'Open with a heading (emailText with variant heading), keep each paragraph short, and give the email one main call to action as a button. Greet the reader by first name with {{contact.firstName}} where a greeting fits. The only merge tokens are {{contact.firstName}}, {{contact.name}} and {{contact.email}}, and only in the email’s text: never in a link, a subject line or a preheader.',\n 'Every button links somewhere: a page of the site by its address from the site inventory, such as /menu, which is completed with the site’s domain when the email is sent, or a full https address the brief gives.',\n 'An image has an empty src for the owner to fill from the media library, alt text describing the picture it should show, and a width of 600 or less.',\n 'Place exactly as many Email product blocks as the job binds products, one for each, in the order the brief names them. Each shows its product’s current name, price and picture when the email is sent, so never type a product’s price.',\n 'Leave out an unsubscribe link and a postal address: the platform adds the unsubscribe link to every campaign email. A fact the brief does not give, such as a date or a place, goes in square brackets.',\n `Subject lines: three that take different approaches, such as direct, curious and benefit-led, each at most ${AI_EMAIL_SUBJECT_MAX_CHARS} characters and best under 60. Preheaders: three, each extending the subject line in the same position, at most ${AI_EMAIL_PREHEADER_MAX_CHARS} characters and best between 40 and 110.`,\n 'Subject lines and preheaders are plain text that nothing fills: they cannot carry a merge token. Write no {{…}} in them and do not address the reader by name there — write \"Welcome to the bakery\", never \"Welcome, {{contact.firstName}}\".',\n ].join('\\n'),\n },\n]\n\nconst DOCTRINE_EMAIL_TOOL = aiDoctrineTreeTool('email')\n\n/** The doctrine's email tree tool, with the subject lines and preheaders beside the tree. */\nexport const AI_JOB_EMAIL_TOOL: AiTool = {\n name: DOCTRINE_EMAIL_TOOL.name,\n description:\n 'Submit the email: its design as one flat node map, three subject lines and three preheaders.',\n strict: true,\n inputSchema: {\n type: 'object',\n additionalProperties: false,\n required: ['tree', 'subjects', 'preheaders'],\n properties: {\n tree: (DOCTRINE_EMAIL_TOOL.inputSchema['properties'] as Record<string, unknown>)['tree'],\n subjects: {\n type: 'array',\n items: { type: 'string' },\n description:\n 'Three subject lines that take different approaches, the strongest first. Plain text: no {{…}} merge token.',\n },\n preheaders: {\n type: 'array',\n items: { type: 'string' },\n description:\n 'Three preheaders, each extending the subject line in the same position. Plain text: no {{…}} merge token.',\n },\n },\n },\n}\n\n/** How the prompt names the kind of email a member picked; `null` for none or an unknown one. */\nexport function aiEmailTypeLabel(value: unknown): string | null {\n return typeof value === 'string' && Object.prototype.hasOwnProperty.call(AI_EMAIL_TYPES, value)\n ? AI_EMAIL_TYPES[value as AiEmailType]\n : null\n}\n\n/** A name the member gave the job, on one line and bounded; empty when none. */\nexport function aiJobInputName(job: Pick<AiJob, 'inputs'>): string {\n const raw = job.inputs?.['name']\n return typeof raw === 'string' ? raw.replace(/\\s+/g, ' ').trim().slice(0, 120) : ''\n}\n\n/** The email job as the generation's user turn. */\nexport function aiJobEmailPrompt(\n job: Pick<AiJob, 'brief' | 'inputs'>,\n plan: AiJobPlan | null,\n heading: string | null,\n products: number,\n): string {\n const lines = heading ? [heading] : []\n const type = aiEmailTypeLabel(job.inputs?.['emailType'])\n if (type) lines.push(`Kind of email: ${type}`)\n lines.push(aiJobBriefLine(job), ...aiPlanReferenceLines(plan))\n lines.push(\n products > 0\n ? `Products bound to this email: ${products}. Place exactly ${products} Email product ${\n products === 1 ? 'block' : 'blocks'\n }.`\n : 'No products are bound to this email: place no Email product block.',\n )\n return lines.join('\\n')\n}\n\n/** The subject lines and preheaders an answer carries. */\nexport interface AiEmailCopy {\n subjects: string[]\n preheaders: string[]\n}\n\nfunction oneLine(value: string): string {\n return value.replace(/\\s+/g, ' ').trim()\n}\n\nexport function parseAiEmailCopy(answer: Readonly<Record<string, unknown>>): AiEmailCopy {\n const lines = (value: unknown) =>\n Array.isArray(value)\n ? value.filter((entry): entry is string => typeof entry === 'string').map(oneLine)\n : []\n return { subjects: lines(answer['subjects']), preheaders: lines(answer['preheaders']) }\n}\n\n/**\n * A merge token that ends a clause, with the words it leaves dangling in\n * front of it — \"to\", \"for\", \"your\" — and an apostrophe-s it took.\n */\nconst CLAUSE_END_TOKEN =\n /(?:\\s+(?:to|at|from|with|for|of|by|in|on|into|the|a|an|and|&|or|our|your|my))*\\s*\\{\\{[^{}]*\\}\\}(?:['’]s\\b)?(?=\\s*(?:[!?.,;:—–]|\\s-\\s|$))/gi\n\n/** Any other merge token, with an apostrophe-s it took. */\nconst INNER_TOKEN = /\\{\\{[^{}]*\\}\\}(?:['’]s\\b)?/g\n\n/**\n * A subject line or preheader with its merge tokens taken out (AGL-3676). Such\n * a line is never filled — the send fills tokens in the email's text only —\n * so `{{contact.firstName}}` there would reach the inbox as written. The\n * guided start's welcome email kept greeting by name in its subject and\n * stopped on `email-header-token` after its re-ask, so the token is removed\n * and the line around it tidied, never filled with a name:\n *\n * - a token that ends a clause takes the words left dangling in front of it,\n * `Welcome to {{business}}!` → `Welcome!`;\n * - a separator left with nothing after it goes, `Welcome, {{firstName}}!` →\n * `Welcome!`, and a doubled one collapses;\n * - a line that opened with the token opens with what followed it,\n * capitalized: `{{firstName}}, your table is ready` → `Your table is ready`.\n *\n * A line with no token is returned as it was; one that is empty once settled\n * is left empty, for the copy's own rules to refuse.\n */\nexport function aiSettleEmailHeaderLine(line: string): string {\n if (!line.includes('{{')) return line\n const opened = /^\\s*\\{\\{/.test(line)\n let out = line\n .replace(CLAUSE_END_TOKEN, '')\n .replace(INNER_TOKEN, '')\n .replace(/\\s+/g, ' ')\n .replace(/\\s+([!?.,;:])/g, '$1')\n .replace(/([,;:—–-])(?:\\s*[,;:—–-])+/g, '$1')\n .replace(/[,;:—–-]+\\s*(?=[!?.]|$)/g, '')\n .replace(/^[\\s,;:—–!?.-]+/, '')\n .trim()\n if (opened && out) out = out[0].toUpperCase() + out.slice(1)\n return out\n}\n\n/**\n * The copy as it is kept (AGL-3676): each line's merge tokens taken out, and,\n * where the model wrote more than three distinct lines of one kind, the first\n * three of them — the count rule is a cap, not a judgment.\n */\nexport function aiSettleEmailCopy(copy: AiEmailCopy): AiEmailCopy {\n const settle = (lines: string[]) => {\n const settled = lines.map(aiSettleEmailHeaderLine)\n if (settled.length <= AI_EMAIL_VARIANTS) return settled\n const seen = new Set<string>()\n const distinct = settled.filter((line) => {\n const key = line.toLowerCase()\n if (!line || seen.has(key)) return false\n seen.add(key)\n return true\n })\n return distinct.length >= AI_EMAIL_VARIANTS ? distinct.slice(0, AI_EMAIL_VARIANTS) : settled\n }\n return { subjects: settle(copy.subjects), preheaders: settle(copy.preheaders) }\n}\n\n/** The copy's own rules: three different lines of each, within their lengths, with no merge token. */\nexport function aiEmailCopyViolations(copy: AiEmailCopy): AiDoctrineViolation[] {\n const violations: AiDoctrineViolation[] = []\n const distinct = (lines: string[]) =>\n new Set(lines.filter(Boolean).map((line) => line.toLowerCase())).size\n if (copy.subjects.length !== AI_EMAIL_VARIANTS || distinct(copy.subjects) !== AI_EMAIL_VARIANTS) {\n violations.push({\n rule: null,\n code: 'email-subjects',\n message: `Write exactly ${AI_EMAIL_VARIANTS} different subject lines.`,\n })\n }\n if (copy.preheaders.length !== AI_EMAIL_VARIANTS || distinct(copy.preheaders) !== AI_EMAIL_VARIANTS) {\n violations.push({\n rule: null,\n code: 'email-preheaders',\n message: `Write exactly ${AI_EMAIL_VARIANTS} different preheaders, one for each subject line.`,\n })\n }\n if (copy.subjects.some((line) => line.length > AI_EMAIL_SUBJECT_MAX_CHARS)) {\n violations.push({\n rule: null,\n code: 'email-subject-length',\n message: `A subject line is over ${AI_EMAIL_SUBJECT_MAX_CHARS} characters. Keep each within ${AI_EMAIL_SUBJECT_MAX_CHARS}, and best under 60.`,\n })\n }\n if (copy.preheaders.some((line) => line.length > AI_EMAIL_PREHEADER_MAX_CHARS)) {\n violations.push({\n rule: null,\n code: 'email-preheader-length',\n message: `A preheader is over ${AI_EMAIL_PREHEADER_MAX_CHARS} characters. Keep each within ${AI_EMAIL_PREHEADER_MAX_CHARS}.`,\n })\n }\n if ([...copy.subjects, ...copy.preheaders].some((line) => line.includes('{{'))) {\n violations.push({\n rule: null,\n code: 'email-header-token',\n message:\n 'A subject line or preheader carries a merge token, which is not filled there. Write them without {{…}}.',\n })\n }\n return violations\n}\n\nconst MERGE_TOKEN = /\\{\\{\\s*([^}]*?)\\s*\\}\\}/g\n\nfunction visitsOf(tree: Pick<AiValidatedTree, 'rootId' | 'nodes'>) {\n return walkTree({\n rootId: tree.rootId,\n nodes: tree.nodes as unknown as Record<string, AiDoctrineNode>,\n })\n}\n\n/** The recipient tokens a model writes under another spelling, by that spelling folded. */\nconst RECIPIENT_TOKEN_ALIASES: Readonly<Record<string, string>> = {\n firstname: 'contact.firstName',\n name: 'contact.name',\n fullname: 'contact.name',\n email: 'contact.email',\n emailaddress: 'contact.email',\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\n/**\n * An email's node map settled where it has one reading (AGL-3676), before the\n * doctrine reads it:\n *\n * - a recipient token under another spelling — `{{firstName}}`,\n * `{{contact.first_name}}`, `{{ email }}` — is written as the token the\n * send fills, in text; any other token is left for the check to refuse;\n * - a link that opens with the site's address token, `{{site.url}}/menu`, is\n * the page address the design completes with that token itself (see\n * `aiEmailDesignNodes`), so it is written as `/menu`.\n *\n * Reads and returns the tree as the model wrote it.\n */\nexport function aiSettleEmailTree(input: unknown): unknown {\n if (!isRecord(input) || !isRecord(input['nodes'])) return input\n const siteUrl = /^\\s*\\{\\{\\s*site\\.url\\s*\\}\\}/\n let nodes: Record<string, unknown> | null = null\n for (const [id, node] of Object.entries(input['nodes'])) {\n if (!isRecord(node) || !isRecord(node['props'])) continue\n let props: Record<string, unknown> | null = null\n for (const [name, value] of Object.entries(node['props'])) {\n if (typeof value !== 'string' || !value.includes('{{')) continue\n let next = value\n if (name === 'href') {\n if (siteUrl.test(value)) next = value.replace(siteUrl, '').trim() || '/'\n if (!next.startsWith('/')) next = value\n } else {\n next = value.replace(MERGE_TOKEN, (token, key: string) => {\n if (AI_EMAIL_TEXT_MERGE_TOKENS.includes(key)) return token\n const alias = RECIPIENT_TOKEN_ALIASES[key.replace(/^contact\\./i, '').replace(/[\\s_-]/g, '').toLowerCase()]\n return alias ? `{{${alias}}}` : token\n })\n }\n if (next === value) continue\n props ??= { ...node['props'] }\n props[name] = next\n }\n if (!props) continue\n nodes ??= { ...input['nodes'] }\n nodes[id] = { ...node, props }\n }\n return nodes ? { ...input, nodes } : input\n}\n\n/** The design's own rules, on a tree the doctrine admitted. */\nexport function aiEmailTreeViolations(tree: AiValidatedTree, products: number): AiDoctrineViolation[] {\n const violations: AiDoctrineViolation[] = []\n const loose = (tree.nodes[tree.rootId]?.nodes ?? []).filter(\n (id) => tree.nodes[id]?.componentId !== 'emailSection',\n )\n if (loose.length) {\n violations.push({\n rule: null,\n code: 'email-loose-block',\n message: 'A block sits outside every Email section. Put each block inside a section.',\n nodeIds: aiModelNodeIds(loose, tree.sourceIds),\n })\n }\n const tokens = new Set<string>()\n const tokenNodes: string[] = []\n const unlinked: string[] = []\n let placed = 0\n for (const { id, node } of visitsOf(tree)) {\n for (const value of Object.values(node.props ?? {})) {\n if (typeof value !== 'string') continue\n for (const match of value.matchAll(MERGE_TOKEN)) {\n if (!AI_EMAIL_TEXT_MERGE_TOKENS.includes(match[1])) {\n tokens.add(`{{${match[1]}}}`)\n tokenNodes.push(id)\n }\n }\n }\n if (node.componentId === 'emailButton' && typeof node.props?.['href'] !== 'string') {\n unlinked.push(id)\n }\n if (node.componentId === 'emailProduct') placed += 1\n }\n if (tokens.size) {\n violations.push({\n rule: null,\n code: 'email-merge-token',\n message: `This email writes ${[...tokens].slice(0, 3).join(', ')}, which a generated email may not use. Use only {{contact.firstName}}, {{contact.name}} or {{contact.email}}, and only in text.`,\n nodeIds: aiModelNodeIds([...new Set(tokenNodes)], tree.sourceIds),\n })\n }\n if (unlinked.length) {\n violations.push({\n rule: null,\n code: 'email-button-link',\n message:\n 'An Email button links nowhere. Link each to a page of the site by its address, such as /menu, or to a full https address the brief gives.',\n nodeIds: aiModelNodeIds(unlinked, tree.sourceIds),\n })\n }\n if (placed !== products) {\n violations.push({\n rule: null,\n code: 'email-product-count',\n message: products\n ? `This email binds ${products} ${products === 1 ? 'product' : 'products'}: place exactly ${products} Email product ${\n products === 1 ? 'block' : 'blocks'\n }.`\n : 'No products are bound to this email: remove the Email product blocks.',\n })\n }\n return violations\n}\n\n/**\n * The design as it is stored: a link to one of the site's pages completed with\n * the site's address token, and each product block bound, in document order,\n * to the next product the job binds.\n */\nexport function aiEmailDesignNodes(\n tree: Pick<AiValidatedTree, 'rootId' | 'nodes'>,\n productIds: readonly string[],\n): NodesMap {\n const nodes: NodesMap = { ...tree.nodes }\n let next = 0\n for (const { id, node } of visitsOf(tree)) {\n const props: Record<string, unknown> = { ...(node.props ?? {}) }\n let changed = false\n const href = props['href']\n if (typeof href === 'string' && href.startsWith('/') && !href.startsWith('//')) {\n props['href'] = `${AI_EMAIL_SITE_URL_TOKEN}${href}`\n changed = true\n }\n if (node.componentId === 'emailProduct') {\n const productId = productIds[next]\n next += 1\n if (productId) {\n props['productId'] = productId\n changed = true\n }\n }\n if (changed) nodes[id] = { ...nodes[id], props } as NodesMap[string]\n }\n return nodes\n}\n\n/** The content the email plugin's writer stores a design from. */\nexport function aiEmailDesignContent(nodes: NodesMap, copy: AiEmailCopy): Record<string, unknown> {\n return {\n nodes,\n subject: copy.subjects[0] ?? '',\n preheader: copy.preheaders[0] ?? '',\n subjectVariants: copy.subjects,\n preheaderVariants: copy.preheaders,\n }\n}\n\n/** What the email plugin's renderer says about the design, as violations the model is re-asked with. */\nexport function aiEmailRenderViolations(\n design: PluginResourceDraftWriter,\n hostId: string,\n nodes: NodesMap,\n copy: AiEmailCopy,\n): AiDoctrineViolation[] {\n const checked = design.check(aiEmailDesignContent(nodes, copy), { hostId })\n if (checked.ok === true) return []\n return [...new Set(checked.problems)].map((problem) => ({\n rule: null,\n code: 'email-render',\n message: `The email does not render as written: ${problem}.`,\n }))\n}\n\nexport interface AiEmailGenerationInput {\n job: AiJob\n step: Extract<AiStepKind, 'job.email' | 'job.campaign'>\n model: string\n /** The line naming what is built, such as `Email name: …`; `null` for none. */\n heading: string | null\n plan: AiJobPlan | null\n inventory: AiSiteInventory | null\n hostId: string\n products: AiEmailProductBinding\n /** The email plugin's design writer, whose check the design must pass. */\n design: PluginResourceDraftWriter\n signal?: AbortSignal\n}\n\nexport interface AiEmailGeneration {\n result: AiValidatedGeneration<AiValidatedTree>\n /** The copy of the answer the doctrine kept; `null` unless it kept one. */\n copy: AiEmailCopy | null\n /** The kept design as it is stored; `null` unless the doctrine kept one. */\n nodes: NodesMap | null\n}\n\n/** Generates one email under the doctrine, with the email's own checks riding `extend`. */\nexport async function generateAiEmail(input: AiEmailGenerationInput): Promise<AiEmailGeneration> {\n // The answer `extend` saw last is the one the doctrine kept when it keeps one.\n let last = null as { copy: AiEmailCopy; nodes: NodesMap } | null\n const result = await runValidatedGeneration('email', {\n step: input.step,\n model: input.model,\n instructions: AI_JOB_EMAIL_INSTRUCTIONS,\n inventory: input.inventory,\n messages: [\n {\n role: 'user',\n content: aiJobEmailPrompt(input.job, input.plan, input.heading, input.products.ids.length),\n },\n ],\n tool: AI_JOB_EMAIL_TOOL,\n maxTokens: AI_JOB_EMAIL_STEP_BUDGETS[input.step].maxTokens(input.model),\n thinking: 'off',\n // What has one reading is settled, not re-asked (AGL-3676): the tree's\n // token spellings and site-address links, the copy's header tokens.\n complete: aiSettleEmailTree,\n extend: (tree, answer) => {\n const copy = aiSettleEmailCopy(parseAiEmailCopy(answer))\n const nodes = aiEmailDesignNodes(tree, input.products.ids)\n last = { copy, nodes }\n const violations = [\n ...aiEmailTreeViolations(tree, input.products.ids.length),\n ...aiEmailCopyViolations(copy),\n ]\n // Rendered once the design's own rules hold, so a problem is named once.\n return violations.length\n ? violations\n : aiEmailRenderViolations(input.design, input.hostId, nodes, copy)\n },\n ...(input.signal ? { signal: input.signal } : {}),\n })\n const kept = result.status === 'ok' ? last : null\n return { result, copy: kept?.copy ?? null, nodes: kept?.nodes ?? null }\n}\n\n/** An email design as the job's output. */\nexport function aiEmailDesignOutput(\n record: PluginDraftRecord,\n place: { hostId: string; hostSubdomain: string | null },\n note: string | null,\n): AiJobOutput {\n return {\n resource: 'emailScreen',\n id: record.id,\n versionId: record.versionId,\n hostId: place.hostId,\n hostSubdomain: place.hostSubdomain,\n label: record.name,\n ...(note ? { note } : {}),\n }\n}\n\n/**\n * An email job is admitted for a site of its org with email on and room for a\n * design, and confirmed only for a plan that creates nothing but the design:\n * an email places nothing another job builds (AGL-3143 §15).\n */\nexport const aiEmailJobAdmission: AiJobAdmission = async (context) => {\n const refusal = await aiPluginDraftAdmissionRefusal(context, {\n kind: 'email',\n drafts: [{ resource: AI_EMAIL_DESIGN_RESOURCE, label: 'Email' }],\n })\n if (refusal || !context.hostId) return refusal\n return aiJobPlanCreationsRefusal('email', context, context.hostId)\n}\n\nexport interface AiJobEmailStepDeps {\n /** The inventory reader; specs hand in a fake. */\n readInventory?: typeof readSiteInventory\n /** How a resource's draft writer is found; the core's registry otherwise. */\n writerFor?: AiPluginDraftWriterLookup\n}\n\nexport function createAiJobEmailStep(deps: AiJobEmailStepDeps = {}): AiJobStepRunner {\n const readInventory = deps.readInventory ?? readSiteInventory\n const writerFor = deps.writerFor ?? aiPluginDraftWriter\n return async ({ job, now, signal, firestore, modelFor }): Promise<AiJobStepOutcome> => {\n const hostId = job.hostId\n if (!hostId) throw new Error('an email job names no site, and its admission refuses one')\n // The switch's answer for this job, else the routing table's (AGL-2942).\n const model = modelFor?.('job.email') ?? aiModelForStep('job.email')\n const design = writerFor(AI_EMAIL_DESIGN_RESOURCE)\n if (!design) return { ...aiUnspentOutcome(model), failure: AI_EMAIL_UNAVAILABLE_COPY }\n\n // A run cut off after its design was written reports that design, found by the id the job recorded.\n const draftId = aiJobDraftId(job, 'email')\n const written = await design.read({ hostId, id: draftId })\n if (written) {\n const hostSubdomain = await aiSiteSubdomain(firestore, hostId)\n return aiUnspentOutcome(model, {\n outputs: [aiEmailDesignOutput(written, { hostId, hostSubdomain }, null)],\n })\n }\n\n const [inventory, orgSnapshot, products, hostSubdomain] = await Promise.all([\n readInventory(job.orgId, hostId, { firestore }),\n firestore.collection('orgs').doc(job.orgId).get(),\n resolveAiEmailProducts({ hostId, brief: job.brief, inputs: job.inputs }),\n aiSiteSubdomain(firestore, hostId),\n ])\n const org = (orgSnapshot.data() ?? null) as Record<string, unknown> | null\n const context = { orgId: job.orgId, hostId, uid: job.createdBy, org, now }\n const refusal = await design.refusal(context)\n if (refusal) {\n return refusal.status === 403\n ? aiUnspentOutcome(model, { review: aiLimitReview(refusal.error) })\n : { ...aiUnspentOutcome(model), failure: refusal.error }\n }\n\n const plan = aiConfirmedPlan(job)\n const name =\n aiPlanCreation(plan, 'email')?.name || aiJobInputName(job) || AI_JOB_EMAIL_DEFAULT_NAME\n const generated = await generateAiEmail({\n job,\n step: 'job.email',\n model,\n heading: `Email name: ${name}`,\n plan,\n inventory,\n hostId,\n products,\n design,\n ...(signal ? { signal } : {}),\n })\n const { result } = generated\n const spent = aiGenerationSpent(result)\n if (result.status === 'refused') return { ...spent, refused: true }\n if (result.status === 'needs_input') return { ...spent, review: aiDoctrineReview(result) }\n if (!generated.copy || !generated.nodes) return { ...spent, failure: AI_EMAIL_SAVE_FAILURE_COPY }\n\n const write = await design.write({\n ...context,\n id: draftId,\n name,\n content: aiEmailDesignContent(generated.nodes, generated.copy),\n })\n if (write.ok === false) {\n return write.status === 403\n ? { ...spent, review: aiLimitReview(write.error) }\n : { ...spent, failure: AI_EMAIL_SAVE_FAILURE_COPY }\n }\n return {\n ...spent,\n outputs: [aiEmailDesignOutput(write, { hostId, hostSubdomain }, aiEmailProductNote(products))],\n }\n }\n}\n\nexport const runAiJobEmailStep = createAiJobEmailStep()\n\n/** Registers the email step and the check a email job passes before it is created or resumed. */\nexport function registerAiEmailJob(): void {\n registerAiJobStep('email', runAiJobEmailStep, { minimumMs: AI_JOB_EMAIL_STEP_MINIMUM_MS })\n registerAiJobAdmission('email', aiEmailJobAdmission)\n}\n"],"names":["AI_STEP_TIERS","AI_ROUTING_TABLE","aiModelForStep","aiDoctrineTreeTool","runValidatedGeneration","walkTree","AI_TEXT_LIMITS","readSiteInventory","aiEmailProductNote","resolveAiEmailProducts","registerAiJobAdmission","aiJobPlanCreationsRefusal","aiJobStepBudget","aiJobDraftId","aiSiteSubdomain","aiConfirmedPlan","aiDoctrineReview","aiGenerationSpent","aiJobBriefLine","aiLimitReview","aiModelNodeIds","aiPlanCreation","aiPlanReferenceLines","aiUnspentOutcome","aiPluginDraftAdmissionRefusal","aiPluginDraftWriter","registerAiJobStep","AI_EMAIL_DESIGN_RESOURCE","AI_JOB_EMAIL_MAX_TOKENS","maxTokens","AI_JOB_EMAIL_STEP_BUDGETS","tier","AI_JOB_EMAIL_STEP_MINIMUM_MS","minimumMs","AI_EMAIL_VARIANTS","AI_EMAIL_SUBJECT_MAX_CHARS","headline","AI_EMAIL_PREHEADER_MAX_CHARS","AI_JOB_EMAIL_DEFAULT_NAME","AI_EMAIL_UNAVAILABLE_COPY","AI_EMAIL_SAVE_FAILURE_COPY","AI_EMAIL_TYPES","welcome","newsletter","launch","abandonedCart","eventReminder","reply","AI_EMAIL_TEXT_MERGE_TOKENS","AI_EMAIL_SITE_URL_TOKEN","AI_JOB_EMAIL_INSTRUCTIONS","text","join","DOCTRINE_EMAIL_TOOL","AI_JOB_EMAIL_TOOL","name","description","strict","inputSchema","type","additionalProperties","required","properties","tree","subjects","items","preheaders","aiEmailTypeLabel","value","Object","prototype","hasOwnProperty","call","aiJobInputName","job","raw","inputs","replace","trim","slice","aiJobEmailPrompt","plan","heading","products","lines","push","oneLine","parseAiEmailCopy","answer","Array","isArray","filter","entry","map","CLAUSE_END_TOKEN","INNER_TOKEN","aiSettleEmailHeaderLine","line","includes","opened","test","out","toUpperCase","aiSettleEmailCopy","copy","settle","settled","length","seen","Set","distinct","key","toLowerCase","has","add","aiEmailCopyViolations","violations","Boolean","size","rule","code","message","some","MERGE_TOKEN","visitsOf","rootId","nodes","RECIPIENT_TOKEN_ALIASES","firstname","fullname","email","emailaddress","isRecord","aiSettleEmailTree","input","siteUrl","id","node","entries","props","next","startsWith","token","alias","aiEmailTreeViolations","loose","componentId","nodeIds","sourceIds","tokens","tokenNodes","unlinked","placed","values","match","matchAll","aiEmailDesignNodes","productIds","changed","href","productId","aiEmailDesignContent","subject","preheader","subjectVariants","preheaderVariants","aiEmailRenderViolations","design","hostId","checked","check","ok","problems","problem","generateAiEmail","last","result","step","model","instructions","inventory","messages","role","content","ids","tool","thinking","complete","extend","signal","kept","status","aiEmailDesignOutput","record","place","note","resource","versionId","hostSubdomain","label","aiEmailJobAdmission","context","refusal","kind","drafts","createAiJobEmailStep","deps","readInventory","writerFor","now","firestore","modelFor","orgSnapshot","Error","failure","draftId","written","read","outputs","Promise","all","orgId","collection","doc","get","brief","org","data","uid","createdBy","review","error","generated","spent","refused","write","runAiJobEmailStep","registerAiEmailJob"],"mappings":"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"}
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
17
|
import type { NodesMap } from '@aglyn/aglyn/types/nodes';
|
|
18
|
+
import { type AiLayoutDesign } from '../layout-language/ai-layout-design';
|
|
18
19
|
import type { AiLayoutPage, AiLayoutTargets } from '../layout-language/ai-layout-links';
|
|
19
20
|
import type { AiJob } from '../model/ai-jobs.types';
|
|
20
21
|
import { type AiSiteInventory } from '../model/ai-site-inventory';
|
|
@@ -73,6 +74,8 @@ export declare function aiLayoutFrameCheck(input: {
|
|
|
73
74
|
pages: readonly AiLayoutPage[];
|
|
74
75
|
targets: AiLayoutTargets;
|
|
75
76
|
extend: (tree: AiValidatedTree) => AiDoctrineViolation[];
|
|
77
|
+
/** The site's design (`aiLayoutDesignOf`), whose pages' closing band the footer sits under. */
|
|
78
|
+
design?: AiLayoutDesign | null;
|
|
76
79
|
}): AiGenerationCheck<AiLayoutFrameBuilt>;
|
|
77
80
|
/** What a frame's links resolve against: the site's pages and its form, as a page's do. */
|
|
78
81
|
export declare function aiLayoutFrameTargets(job: Pick<AiJob, 'brief' | 'inputs'>, inventory: AiSiteInventory | null): AiLayoutTargets;
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
17
|
+
import { aiLayoutDesignChoices } from "../layout-language/ai-layout-design.js";
|
|
17
18
|
import { aiCompileLayoutFrame } from "../layout-language/ai-layout-frame.js";
|
|
18
19
|
import { AI_LAYOUT_LANGUAGE_TEXT, aiReadLayoutFrame } from "../layout-language/ai-layout-language.js";
|
|
19
20
|
import { aiLayoutCopyCheck } from "../layout-language/ai-layout-gaps.js";
|
|
@@ -116,7 +117,8 @@ import { AI_SITE_NAME_TOKEN } from "./ai-layout-site-facts.js";
|
|
|
116
117
|
const compiled = aiCompileLayoutFrame(frame, {
|
|
117
118
|
siteName: input.siteName,
|
|
118
119
|
homeId: input.homeId,
|
|
119
|
-
navPages: input.pages
|
|
120
|
+
navPages: input.pages,
|
|
121
|
+
closesDark: input.design ? aiLayoutDesignChoices(input.design).coverClose : false
|
|
120
122
|
}, input.targets);
|
|
121
123
|
const context = {
|
|
122
124
|
screenIds: [
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-layout-language.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 type { NodesMap } from '@aglyn/aglyn/types/nodes'\nimport { aiCompileLayoutFrame } from '../layout-language/ai-layout-frame'\nimport {\n AI_LAYOUT_FRAME_TOOL,\n AI_LAYOUT_LANGUAGE_TEXT,\n aiReadLayoutFrame,\n} from '../layout-language/ai-layout-language'\nimport type {\n AiLayoutPage,\n AiLayoutTargets,\n} from '../layout-language/ai-layout-links'\nimport { aiLayoutCopyCheck } from '../layout-language/ai-layout-gaps'\nimport { aiLayoutStoredTree } from '../layout-language/ai-layout-store'\nimport { AI_GENERATION_MAX_ATTEMPTS } from '../runtime/ai-generation-bounds'\nimport type { AiJob } from '../model/ai-jobs.types'\nimport {\n aiHomeScreenIds,\n type AiSiteInventory,\n} from '../model/ai-site-inventory'\nimport { AI_STEP_TIERS } from '../providers/catalog'\nimport type { AiGenerationCheck, AiValidatedTree } from '../runtime/ai-doctrine'\nimport {\n validateAiDoctrineTree,\n type AiDoctrineViolation,\n} from '../runtime/ai-doctrine-validators'\nimport type { AiLoadEstimate } from '../runtime/ai-palette'\nimport { aiSiteKindDesignLines } from '../model/ai-site-kinds'\nimport type { AiSystemBlock } from '../runtime/ai-runtime'\nimport { aiJobStepBudget } from './ai-job-budget'\nimport { aiJobBriefLine } from './ai-job-generation'\nimport {\n AI_LAYOUT_SITE_PAGES_MAX,\n aiLayoutIsHomeSlug,\n aiLayoutSitePages,\n} from './ai-job-layout-site-pages'\nimport {\n AI_LAYOUT_FRAME_THINKING_TOKENS,\n aiLayoutPageTargets,\n} from './ai-job-page-language'\nimport { AI_SITE_NAME_TOKEN } from './ai-layout-site-facts'\n\n/**\n * A site's layout built in the compact layout language (AGL-3660): the layout\n * step's pass for a guided start's and a build's layout unit. The model\n * designs the header and the footer — their bands, the call to action, the\n * footer's columns and every word — in one short answer through\n * `submit_frame`, and the compiler writes the layout around what the site\n * owes it: its name as the brand, its pages as the navigation and the phone\n * menu, the Layout Slot, and the footer at the bottom of a short page\n * (`ai-layout-frame.ts`).\n */\n\n/**\n * The most a frame's answer asks for: two sections of a few blocks each (the\n * live runs of 2026-10-07 wrote 442 to 583), and the thinking before them.\n */\nexport const AI_JOB_LAYOUT_LANGUAGE_TOKENS = 1_200 + AI_LAYOUT_FRAME_THINKING_TOKENS\n\n/** A language layout pass's time, on the tier the layout step is served from. */\nexport const AI_JOB_LAYOUT_LANGUAGE_BUDGET = aiJobStepBudget({\n tier: AI_STEP_TIERS['job.layout'],\n maxTokens: AI_JOB_LAYOUT_LANGUAGE_TOKENS,\n})\n\n/** The generation kind a frame is asked under, which the doctrine scopes. */\nexport const AI_LAYOUT_FRAME_KIND = 'layout-frame'\n\n/** The language, as the frame door's cached instructions. */\nexport const AI_JOB_LAYOUT_LANGUAGE_INSTRUCTIONS: readonly AiSystemBlock[] = [\n {\n text: [\n AI_LAYOUT_LANGUAGE_TEXT,\n '',\n \"Now you design a site's header and footer, each one section of the language, answered with submit_frame.\",\n \"- header: the platform writes the site's name and its navigation itself. Choose its band and align (start, or center for a centered navigation), and give it at most one button: the call to action every page shows.\",\n '- footer: a section like any other, usually a row of two or three columns: a few words about the business, the details the brief gives, and a list of links (items with to). Put no contact detail, opening hours or address in it that the brief does not give, and no gap in square brackets for one.',\n ].join('\\n'),\n cacheBreakpoint: true,\n },\n]\n\n/** The site's name as the brand: the name the job was given, else the token the site's own name fills. */\nexport function aiLayoutSiteName(job: Pick<AiJob, 'inputs'>): string {\n const given = job.inputs?.['businessName']\n return typeof given === 'string' && given.trim()\n ? given.trim()\n : AI_SITE_NAME_TOKEN\n}\n\n/** The pages a frame's navigation links: the planned pages, else the site's own, in order. */\nexport function aiLayoutNavPages(\n job: Pick<AiJob, 'inputs'>,\n inventory: AiSiteInventory | null,\n): AiLayoutPage[] {\n // The header and the footer link the pages the job was told (AGL-3660),\n // else the site's own, the home page always and first, labelled Home. The\n // model draws neither list, so it cannot drop a page.\n const planned = aiLayoutSitePages(job.inputs)\n const own = (inventory?.screens ?? [])\n .filter((row) => !row.template)\n .map((row) => ({ id: row.id, label: row.name, slug: row.slug }))\n const siteHome = planned.some((page) => aiLayoutIsHomeSlug(page.slug))\n ? []\n : own.filter((page) => aiLayoutIsHomeSlug(page.slug)).slice(0, 1)\n const pages = [...siteHome, ...(planned.length ? planned : own)].filter((page) => page.label.trim())\n const home = pages.find((page) => aiLayoutIsHomeSlug(page.slug))\n return [\n ...(home ? [{ ...home, label: 'Home' }] : []),\n ...pages.filter((page) => page !== home && !aiLayoutIsHomeSlug(page.slug)),\n ].slice(0, AI_LAYOUT_SITE_PAGES_MAX)\n}\n\n/** The home page the brand links: the planned home, else the site's. */\nexport function aiLayoutHomeId(\n pages: readonly AiLayoutPage[],\n inventory: AiSiteInventory | null,\n): string | null {\n const planned = pages.find((page) => aiLayoutIsHomeSlug(page.slug))\n return planned?.id ?? aiHomeScreenIds(inventory)[0] ?? null\n}\n\n/** The frame's user turn: the site, its brief, its pages and its form. */\nexport function aiLayoutFramePrompt(input: {\n job: Pick<AiJob, 'brief' | '$id'> & Partial<Pick<AiJob, 'inputs'>>\n siteName: string\n pages: readonly AiLayoutPage[]\n targets: AiLayoutTargets\n}): string {\n const { job, pages, targets } = input\n return [\n `Site: ${input.siteName === AI_SITE_NAME_TOKEN ? 'the business the brief describes' : `\"${input.siteName}\"`}`,\n aiJobBriefLine(job),\n ...aiSiteKindDesignLines(job.inputs),\n pages.length\n ? `Its pages, which the navigation links: ${pages.map((page) => `${page.label} (page:${page.id})`).join(', ')}.`\n : 'It has no pages yet.',\n targets.forms.length || targets.formPageId\n ? 'A call to action that asks a visitor to get in touch goes to form.'\n : 'The site has no form yet.',\n `Design seed: ${job.$id.slice(-6)}.`,\n ].join('\\n')\n}\n\n/** A validated language layout. */\nexport interface AiLayoutFrameBuilt {\n rootId: string\n nodes: NodesMap\n load: AiLoadEstimate | null\n /** What the last answer's gaps took out of the frame (`ai-layout-gaps.ts`). */\n dropped: string[]\n}\n\n/** The check a frame answer is held to: read, compiled, stored and checked as a layout, with the layout door's own checks. */\nexport function aiLayoutFrameCheck(input: {\n /** The header arrangement the site's look chose (AGL-3660); the answer's otherwise. */\n headerAlign?: 'start' | 'center'\n siteName: string\n homeId: string | null\n pages: readonly AiLayoutPage[]\n targets: AiLayoutTargets\n extend: (tree: AiValidatedTree) => AiDoctrineViolation[]\n}): AiGenerationCheck<AiLayoutFrameBuilt> {\n let answers = 0\n return (answer) => {\n // The last answer a generation takes has its gaps taken out rather than asked about again.\n answers += 1\n const last = answers >= AI_GENERATION_MAX_ATTEMPTS\n const frame = aiReadLayoutFrame(answer)\n if (input.headerAlign && frame.header) frame.header.align = input.headerAlign\n const compiled = aiCompileLayoutFrame(\n frame,\n { siteName: input.siteName, homeId: input.homeId, navPages: input.pages },\n input.targets,\n )\n const context = {\n screenIds: [\n ...new Set([\n ...input.targets.pages.map((page) => page.id),\n ...input.pages.map((page) => page.id),\n ...(input.homeId ? [input.homeId] : []),\n ]),\n ],\n formIds: input.targets.forms.map((form) => form.id),\n homeScreenIds: input.targets.homeIds,\n codeBuilt: true,\n }\n const stored = aiLayoutStoredTree(compiled.tree, 'layout', context)\n if (stored.ok === false) {\n console.error('ai layout frame: a compiled layout did not store', {\n error: stored.error,\n })\n return {\n value: null,\n violations: [\n {\n rule: null,\n code: 'layout-compile',\n message: 'This layout could not be built. Try again.',\n detail: stored.error,\n },\n ],\n }\n }\n // No gap and no internal reference in the words of a frame every published page shows.\n const navPages = [...input.targets.pages, ...input.pages]\n const copy = aiLayoutCopyCheck(\n stored.nodes,\n stored.rootId,\n {\n pages: navPages,\n ids: [...navPages.map((page) => page.id), ...input.targets.forms.map((form) => form.id)],\n },\n last,\n )\n const { nodes, dropped } = copy\n const report = validateAiDoctrineTree(\n { rootId: stored.rootId, nodes },\n 'layout',\n context,\n )\n const violations = [\n ...report.violations,\n ...copy.violations,\n ...input.extend({\n rootId: stored.rootId,\n nodes,\n repairs: [],\n sourceIds: {},\n score: report.score as AiValidatedTree['score'],\n load: report.load,\n }),\n ]\n return {\n value: violations.length\n ? null\n : { rootId: stored.rootId, nodes, load: report.load, dropped },\n violations,\n }\n }\n}\n\n/** What a frame's links resolve against: the site's pages and its form, as a page's do. */\nexport function aiLayoutFrameTargets(\n job: Pick<AiJob, 'brief' | 'inputs'>,\n inventory: AiSiteInventory | null,\n): AiLayoutTargets {\n return aiLayoutPageTargets({ job, inventory, own: [] })\n}\n"],"names":["aiCompileLayoutFrame","AI_LAYOUT_LANGUAGE_TEXT","aiReadLayoutFrame","aiLayoutCopyCheck","aiLayoutStoredTree","AI_GENERATION_MAX_ATTEMPTS","aiHomeScreenIds","AI_STEP_TIERS","validateAiDoctrineTree","aiSiteKindDesignLines","aiJobStepBudget","aiJobBriefLine","AI_LAYOUT_SITE_PAGES_MAX","aiLayoutIsHomeSlug","aiLayoutSitePages","AI_LAYOUT_FRAME_THINKING_TOKENS","aiLayoutPageTargets","AI_SITE_NAME_TOKEN","AI_JOB_LAYOUT_LANGUAGE_TOKENS","AI_JOB_LAYOUT_LANGUAGE_BUDGET","tier","maxTokens","AI_LAYOUT_FRAME_KIND","AI_JOB_LAYOUT_LANGUAGE_INSTRUCTIONS","text","join","cacheBreakpoint","aiLayoutSiteName","job","given","inputs","trim","aiLayoutNavPages","inventory","planned","own","screens","filter","row","template","map","id","label","name","slug","siteHome","some","page","slice","pages","length","home","find","aiLayoutHomeId","aiLayoutFramePrompt","input","targets","siteName","forms","formPageId","$id","aiLayoutFrameCheck","answers","answer","last","frame","headerAlign","header","align","compiled","homeId","navPages","context","screenIds","Set","formIds","form","homeScreenIds","homeIds","codeBuilt","stored","tree","ok","console","error","value","violations","rule","code","message","detail","copy","nodes","rootId","ids","dropped","report","extend","repairs","sourceIds","score","load","aiLayoutFrameTargets"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-layout-language.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 type { NodesMap } from '@aglyn/aglyn/types/nodes'\nimport { aiLayoutDesignChoices, type AiLayoutDesign } from '../layout-language/ai-layout-design'\nimport { aiCompileLayoutFrame } from '../layout-language/ai-layout-frame'\nimport {\n AI_LAYOUT_FRAME_TOOL,\n AI_LAYOUT_LANGUAGE_TEXT,\n aiReadLayoutFrame,\n} from '../layout-language/ai-layout-language'\nimport type {\n AiLayoutPage,\n AiLayoutTargets,\n} from '../layout-language/ai-layout-links'\nimport { aiLayoutCopyCheck } from '../layout-language/ai-layout-gaps'\nimport { aiLayoutStoredTree } from '../layout-language/ai-layout-store'\nimport { AI_GENERATION_MAX_ATTEMPTS } from '../runtime/ai-generation-bounds'\nimport type { AiJob } from '../model/ai-jobs.types'\nimport {\n aiHomeScreenIds,\n type AiSiteInventory,\n} from '../model/ai-site-inventory'\nimport { AI_STEP_TIERS } from '../providers/catalog'\nimport type { AiGenerationCheck, AiValidatedTree } from '../runtime/ai-doctrine'\nimport {\n validateAiDoctrineTree,\n type AiDoctrineViolation,\n} from '../runtime/ai-doctrine-validators'\nimport type { AiLoadEstimate } from '../runtime/ai-palette'\nimport { aiSiteKindDesignLines } from '../model/ai-site-kinds'\nimport type { AiSystemBlock } from '../runtime/ai-runtime'\nimport { aiJobStepBudget } from './ai-job-budget'\nimport { aiJobBriefLine } from './ai-job-generation'\nimport {\n AI_LAYOUT_SITE_PAGES_MAX,\n aiLayoutIsHomeSlug,\n aiLayoutSitePages,\n} from './ai-job-layout-site-pages'\nimport {\n AI_LAYOUT_FRAME_THINKING_TOKENS,\n aiLayoutPageTargets,\n} from './ai-job-page-language'\nimport { AI_SITE_NAME_TOKEN } from './ai-layout-site-facts'\n\n/**\n * A site's layout built in the compact layout language (AGL-3660): the layout\n * step's pass for a guided start's and a build's layout unit. The model\n * designs the header and the footer — their bands, the call to action, the\n * footer's columns and every word — in one short answer through\n * `submit_frame`, and the compiler writes the layout around what the site\n * owes it: its name as the brand, its pages as the navigation and the phone\n * menu, the Layout Slot, and the footer at the bottom of a short page\n * (`ai-layout-frame.ts`).\n */\n\n/**\n * The most a frame's answer asks for: two sections of a few blocks each (the\n * live runs of 2026-10-07 wrote 442 to 583), and the thinking before them.\n */\nexport const AI_JOB_LAYOUT_LANGUAGE_TOKENS = 1_200 + AI_LAYOUT_FRAME_THINKING_TOKENS\n\n/** A language layout pass's time, on the tier the layout step is served from. */\nexport const AI_JOB_LAYOUT_LANGUAGE_BUDGET = aiJobStepBudget({\n tier: AI_STEP_TIERS['job.layout'],\n maxTokens: AI_JOB_LAYOUT_LANGUAGE_TOKENS,\n})\n\n/** The generation kind a frame is asked under, which the doctrine scopes. */\nexport const AI_LAYOUT_FRAME_KIND = 'layout-frame'\n\n/** The language, as the frame door's cached instructions. */\nexport const AI_JOB_LAYOUT_LANGUAGE_INSTRUCTIONS: readonly AiSystemBlock[] = [\n {\n text: [\n AI_LAYOUT_LANGUAGE_TEXT,\n '',\n \"Now you design a site's header and footer, each one section of the language, answered with submit_frame.\",\n \"- header: the platform writes the site's name and its navigation itself. Choose its band and align (start, or center for a centered navigation), and give it at most one button: the call to action every page shows.\",\n '- footer: a section like any other, usually a row of two or three columns: a few words about the business, the details the brief gives, and a list of links (items with to). Put no contact detail, opening hours or address in it that the brief does not give, and no gap in square brackets for one.',\n ].join('\\n'),\n cacheBreakpoint: true,\n },\n]\n\n/** The site's name as the brand: the name the job was given, else the token the site's own name fills. */\nexport function aiLayoutSiteName(job: Pick<AiJob, 'inputs'>): string {\n const given = job.inputs?.['businessName']\n return typeof given === 'string' && given.trim()\n ? given.trim()\n : AI_SITE_NAME_TOKEN\n}\n\n/** The pages a frame's navigation links: the planned pages, else the site's own, in order. */\nexport function aiLayoutNavPages(\n job: Pick<AiJob, 'inputs'>,\n inventory: AiSiteInventory | null,\n): AiLayoutPage[] {\n // The header and the footer link the pages the job was told (AGL-3660),\n // else the site's own, the home page always and first, labelled Home. The\n // model draws neither list, so it cannot drop a page.\n const planned = aiLayoutSitePages(job.inputs)\n const own = (inventory?.screens ?? [])\n .filter((row) => !row.template)\n .map((row) => ({ id: row.id, label: row.name, slug: row.slug }))\n const siteHome = planned.some((page) => aiLayoutIsHomeSlug(page.slug))\n ? []\n : own.filter((page) => aiLayoutIsHomeSlug(page.slug)).slice(0, 1)\n const pages = [...siteHome, ...(planned.length ? planned : own)].filter((page) => page.label.trim())\n const home = pages.find((page) => aiLayoutIsHomeSlug(page.slug))\n return [\n ...(home ? [{ ...home, label: 'Home' }] : []),\n ...pages.filter((page) => page !== home && !aiLayoutIsHomeSlug(page.slug)),\n ].slice(0, AI_LAYOUT_SITE_PAGES_MAX)\n}\n\n/** The home page the brand links: the planned home, else the site's. */\nexport function aiLayoutHomeId(\n pages: readonly AiLayoutPage[],\n inventory: AiSiteInventory | null,\n): string | null {\n const planned = pages.find((page) => aiLayoutIsHomeSlug(page.slug))\n return planned?.id ?? aiHomeScreenIds(inventory)[0] ?? null\n}\n\n/** The frame's user turn: the site, its brief, its pages and its form. */\nexport function aiLayoutFramePrompt(input: {\n job: Pick<AiJob, 'brief' | '$id'> & Partial<Pick<AiJob, 'inputs'>>\n siteName: string\n pages: readonly AiLayoutPage[]\n targets: AiLayoutTargets\n}): string {\n const { job, pages, targets } = input\n return [\n `Site: ${input.siteName === AI_SITE_NAME_TOKEN ? 'the business the brief describes' : `\"${input.siteName}\"`}`,\n aiJobBriefLine(job),\n ...aiSiteKindDesignLines(job.inputs),\n pages.length\n ? `Its pages, which the navigation links: ${pages.map((page) => `${page.label} (page:${page.id})`).join(', ')}.`\n : 'It has no pages yet.',\n targets.forms.length || targets.formPageId\n ? 'A call to action that asks a visitor to get in touch goes to form.'\n : 'The site has no form yet.',\n `Design seed: ${job.$id.slice(-6)}.`,\n ].join('\\n')\n}\n\n/** A validated language layout. */\nexport interface AiLayoutFrameBuilt {\n rootId: string\n nodes: NodesMap\n load: AiLoadEstimate | null\n /** What the last answer's gaps took out of the frame (`ai-layout-gaps.ts`). */\n dropped: string[]\n}\n\n/** The check a frame answer is held to: read, compiled, stored and checked as a layout, with the layout door's own checks. */\nexport function aiLayoutFrameCheck(input: {\n /** The header arrangement the site's look chose (AGL-3660); the answer's otherwise. */\n headerAlign?: 'start' | 'center'\n siteName: string\n homeId: string | null\n pages: readonly AiLayoutPage[]\n targets: AiLayoutTargets\n extend: (tree: AiValidatedTree) => AiDoctrineViolation[]\n /** The site's design (`aiLayoutDesignOf`), whose pages' closing band the footer sits under. */\n design?: AiLayoutDesign | null\n}): AiGenerationCheck<AiLayoutFrameBuilt> {\n let answers = 0\n return (answer) => {\n // The last answer a generation takes has its gaps taken out rather than asked about again.\n answers += 1\n const last = answers >= AI_GENERATION_MAX_ATTEMPTS\n const frame = aiReadLayoutFrame(answer)\n if (input.headerAlign && frame.header) frame.header.align = input.headerAlign\n const compiled = aiCompileLayoutFrame(\n frame,\n {\n siteName: input.siteName,\n homeId: input.homeId,\n navPages: input.pages,\n closesDark: input.design ? aiLayoutDesignChoices(input.design).coverClose : false,\n },\n input.targets,\n )\n const context = {\n screenIds: [\n ...new Set([\n ...input.targets.pages.map((page) => page.id),\n ...input.pages.map((page) => page.id),\n ...(input.homeId ? [input.homeId] : []),\n ]),\n ],\n formIds: input.targets.forms.map((form) => form.id),\n homeScreenIds: input.targets.homeIds,\n codeBuilt: true,\n }\n const stored = aiLayoutStoredTree(compiled.tree, 'layout', context)\n if (stored.ok === false) {\n console.error('ai layout frame: a compiled layout did not store', {\n error: stored.error,\n })\n return {\n value: null,\n violations: [\n {\n rule: null,\n code: 'layout-compile',\n message: 'This layout could not be built. Try again.',\n detail: stored.error,\n },\n ],\n }\n }\n // No gap and no internal reference in the words of a frame every published page shows.\n const navPages = [...input.targets.pages, ...input.pages]\n const copy = aiLayoutCopyCheck(\n stored.nodes,\n stored.rootId,\n {\n pages: navPages,\n ids: [...navPages.map((page) => page.id), ...input.targets.forms.map((form) => form.id)],\n },\n last,\n )\n const { nodes, dropped } = copy\n const report = validateAiDoctrineTree(\n { rootId: stored.rootId, nodes },\n 'layout',\n context,\n )\n const violations = [\n ...report.violations,\n ...copy.violations,\n ...input.extend({\n rootId: stored.rootId,\n nodes,\n repairs: [],\n sourceIds: {},\n score: report.score as AiValidatedTree['score'],\n load: report.load,\n }),\n ]\n return {\n value: violations.length\n ? null\n : { rootId: stored.rootId, nodes, load: report.load, dropped },\n violations,\n }\n }\n}\n\n/** What a frame's links resolve against: the site's pages and its form, as a page's do. */\nexport function aiLayoutFrameTargets(\n job: Pick<AiJob, 'brief' | 'inputs'>,\n inventory: AiSiteInventory | null,\n): AiLayoutTargets {\n return aiLayoutPageTargets({ job, inventory, own: [] })\n}\n"],"names":["aiLayoutDesignChoices","aiCompileLayoutFrame","AI_LAYOUT_LANGUAGE_TEXT","aiReadLayoutFrame","aiLayoutCopyCheck","aiLayoutStoredTree","AI_GENERATION_MAX_ATTEMPTS","aiHomeScreenIds","AI_STEP_TIERS","validateAiDoctrineTree","aiSiteKindDesignLines","aiJobStepBudget","aiJobBriefLine","AI_LAYOUT_SITE_PAGES_MAX","aiLayoutIsHomeSlug","aiLayoutSitePages","AI_LAYOUT_FRAME_THINKING_TOKENS","aiLayoutPageTargets","AI_SITE_NAME_TOKEN","AI_JOB_LAYOUT_LANGUAGE_TOKENS","AI_JOB_LAYOUT_LANGUAGE_BUDGET","tier","maxTokens","AI_LAYOUT_FRAME_KIND","AI_JOB_LAYOUT_LANGUAGE_INSTRUCTIONS","text","join","cacheBreakpoint","aiLayoutSiteName","job","given","inputs","trim","aiLayoutNavPages","inventory","planned","own","screens","filter","row","template","map","id","label","name","slug","siteHome","some","page","slice","pages","length","home","find","aiLayoutHomeId","aiLayoutFramePrompt","input","targets","siteName","forms","formPageId","$id","aiLayoutFrameCheck","answers","answer","last","frame","headerAlign","header","align","compiled","homeId","navPages","closesDark","design","coverClose","context","screenIds","Set","formIds","form","homeScreenIds","homeIds","codeBuilt","stored","tree","ok","console","error","value","violations","rule","code","message","detail","copy","nodes","rootId","ids","dropped","report","extend","repairs","sourceIds","score","load","aiLayoutFrameTargets"],"mappings":"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"}
|
|
@@ -44,6 +44,8 @@ export interface AiLayoutSitePage {
|
|
|
44
44
|
id: string;
|
|
45
45
|
label: string;
|
|
46
46
|
slug: string;
|
|
47
|
+
/** The path of a destination that is no page, such as the blog a start writes (AGL-3660). */
|
|
48
|
+
href?: string;
|
|
47
49
|
}
|
|
48
50
|
/** Whether a slug is the site's root, the home page. */
|
|
49
51
|
export declare function aiLayoutIsHomeSlug(slug: string): boolean;
|
|
@@ -65,14 +65,16 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
65
65
|
const pages = [];
|
|
66
66
|
for (const entry of raw){
|
|
67
67
|
if (!entry || typeof entry !== 'object') continue;
|
|
68
|
-
const { id, label, slug } = entry;
|
|
68
|
+
const { id, label, slug, href } = entry;
|
|
69
69
|
if (typeof id !== 'string' || !id || typeof label !== 'string' || !label.trim()) continue;
|
|
70
70
|
if (pages.some((page)=>page.id === id)) continue;
|
|
71
|
-
pages.push({
|
|
71
|
+
pages.push(_extends({
|
|
72
72
|
id,
|
|
73
73
|
label: label.trim(),
|
|
74
74
|
slug: typeof slug === 'string' ? slug : ''
|
|
75
|
-
})
|
|
75
|
+
}, typeof href === 'string' && /^\/[a-z0-9-]+(?:\/[a-z0-9-]+)*$/.test(href) ? {
|
|
76
|
+
href
|
|
77
|
+
} : {}));
|
|
76
78
|
}
|
|
77
79
|
return pages.slice(0, AI_LAYOUT_SITE_PAGES_MAX);
|
|
78
80
|
}
|
|
@@ -170,20 +172,30 @@ const LINK_COMPONENTS = new Set([
|
|
|
170
172
|
const id = freshId(`aiNavLink_${page.id.replace(/[^A-Za-z0-9_]/g, '')}`);
|
|
171
173
|
nodes[id] = {
|
|
172
174
|
componentId: 'muiScreenLink',
|
|
173
|
-
props: {
|
|
174
|
-
children: page.label.slice(0, 40)
|
|
175
|
-
|
|
175
|
+
props: _extends({
|
|
176
|
+
children: page.label.slice(0, 40)
|
|
177
|
+
}, page.href ? {
|
|
178
|
+
href: page.href
|
|
179
|
+
} : {
|
|
180
|
+
screenId: page.id
|
|
181
|
+
}, {
|
|
176
182
|
renderAs: 'link',
|
|
177
183
|
color: 'inherit'
|
|
178
|
-
}
|
|
184
|
+
})
|
|
179
185
|
};
|
|
180
186
|
return id;
|
|
181
187
|
});
|
|
182
|
-
const linkedInHeader = new Set(header.map((id)=>nodes[id]).filter((node)=>LINK_COMPONENTS.has(String(node.componentId))).
|
|
183
|
-
var _node_props;
|
|
184
|
-
return
|
|
188
|
+
const linkedInHeader = new Set(header.map((id)=>nodes[id]).filter((node)=>LINK_COMPONENTS.has(String(node.componentId))).flatMap((node)=>{
|
|
189
|
+
var _node_props, _node_props1;
|
|
190
|
+
return [
|
|
191
|
+
(_node_props = node.props) == null ? void 0 : _node_props['screenId'],
|
|
192
|
+
(_node_props1 = node.props) == null ? void 0 : _node_props1['href']
|
|
193
|
+
];
|
|
185
194
|
}).filter((value)=>typeof value === 'string'));
|
|
186
|
-
const missing = pages.filter((page)
|
|
195
|
+
const missing = pages.filter((page)=>{
|
|
196
|
+
var _page_href;
|
|
197
|
+
return !linkedInHeader.has((_page_href = page.href) != null ? _page_href : page.id);
|
|
198
|
+
});
|
|
187
199
|
const navs = order.filter((id)=>elementOf(nodes[id]) === 'nav');
|
|
188
200
|
const headerNav = (_navs_find = navs.find((id)=>header.includes(id))) != null ? _navs_find : null;
|
|
189
201
|
if (missing.length) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-layout-site-pages.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 { normalizeScreenSlug, SCREEN_ROOT_PATH } from '@aglyn/aglyn/app-utils/screen-route'\nimport type { AiBuildPlanScreen } from '../model/ai-build-plan'\n\n/**\n * The navigation of a layout a site scaffold builds before its pages (AGL-3596).\n *\n * A scaffold builds its layout FIRST, because every page renders inside it,\n * so when the layout is generated none of the site's pages exists yet: the\n * model cannot link them, and the layout instructions forbid inventing a page\n * id. A plan that gave the layout a nav region then left the model one way to\n * keep the promise — a nav element with nothing in it — which rule 16 refuses\n * as an empty container, twice, and the guided start stopped there.\n *\n * The scaffold does know the pages: their ids are minted when the plan is\n * kept (`AiBuildPlanScreen.id`), and each page unit writes its draft under\n * that id. So the layout unit is handed them (`sitePages`), and the header's\n * navigation is written by the platform rather than asked of the model: one\n * Page Link per planned page, inside the nav element the model left (filled,\n * never left empty) or a nav row the platform adds to the Toolbar Content.\n * The completed tree is what the doctrine checks and what is written, so rule\n * 16 holds unchanged — nothing it sees is empty — and the publish step, which\n * adds only the entries a header does not already carry, adds nothing twice.\n */\n\n/** The unit input a site layout's planned pages travel under. */\nexport const AI_LAYOUT_SITE_PAGES_INPUT = 'sitePages'\n\n/** The most pages the header links, as the layout instructions cap it. */\nexport const AI_LAYOUT_SITE_PAGES_MAX = 7\n\n/** A page the scaffold builds after the layout, as the layout links it. */\nexport interface AiLayoutSitePage {\n id: string\n label: string\n slug: string\n}\n\n/** Whether a slug is the site's root, the home page. */\nexport function aiLayoutIsHomeSlug(slug: string): boolean {\n return slug.replace(/^\\/+|\\/+$/g, '') === ''\n}\n\n/** A page that confirms what a visitor just did, which no navigation links. */\nconst CONFIRMATION = /thank|confirm|success|submitted/i\n\n/**\n * The pages a plan puts in the navigation, record templates aside, the home\n * page first whatever its `nav` flag says (AGL-3660): a guided start's plan\n * marked its Home `nav: false`, and the header linked only Contact. Every\n * other page keeps its `nav` flag, except in a guided start (`guided`), whose\n * pages are the ones the person asked for: each is linked, but a\n * confirmation page — a thank-you, a \"submitted\" — stays out.\n */\nexport function aiLayoutSitePagesOfPlan(\n screens: readonly AiBuildPlanScreen[],\n options: { guided?: boolean } = {},\n): AiLayoutSitePage[] {\n const linked = (screen: AiBuildPlanScreen) =>\n aiLayoutIsHomeSlug(screen.slug) ||\n (options.guided ? !CONFIRMATION.test(`${screen.slug} ${screen.title}`) : !!screen.nav)\n const pages = screens\n .filter((screen) => !screen.record && typeof screen.id === 'string' && screen.id && linked(screen))\n .map((screen) => ({ id: screen.id as string, label: screen.title.trim(), slug: screen.slug }))\n .filter((page) => page.label)\n return [...pages.filter((page) => aiLayoutIsHomeSlug(page.slug)), ...pages.filter((page) => !aiLayoutIsHomeSlug(page.slug))].slice(\n 0,\n AI_LAYOUT_SITE_PAGES_MAX,\n )\n}\n\n/** The planned pages a layout unit's inputs carry; none for any other layout. */\nexport function aiLayoutSitePages(inputs: Readonly<Record<string, unknown>> | null | undefined): AiLayoutSitePage[] {\n const raw = inputs?.[AI_LAYOUT_SITE_PAGES_INPUT]\n if (!Array.isArray(raw)) return []\n const pages: AiLayoutSitePage[] = []\n for (const entry of raw) {\n if (!entry || typeof entry !== 'object') continue\n const { id, label, slug } = entry as Record<string, unknown>\n if (typeof id !== 'string' || !id || typeof label !== 'string' || !label.trim()) continue\n if (pages.some((page) => page.id === id)) continue\n pages.push({ id, label: label.trim(), slug: typeof slug === 'string' ? slug : '' })\n }\n return pages.slice(0, AI_LAYOUT_SITE_PAGES_MAX)\n}\n\n/**\n * What the layout's user turn says about those pages: that the platform\n * writes the navigation, so the model builds none and leaves no empty element\n * for it. The same promise the completion below keeps.\n */\nexport function aiLayoutSitePagesLines(pages: readonly AiLayoutSitePage[]): string[] {\n if (!pages.length) return []\n return [\n `This site’s pages are built after this layout: ${pages\n .map((page) => `${page.label} (${screenPath(page.slug)})`)\n .join(', ')}.`,\n 'The platform writes the header’s navigation to those pages itself, as a nav row of Page Links inside the Toolbar Content. Build no navigation links to them and no nav element for them: leave no empty element where the links will go.',\n ]\n}\n\nfunction screenPath(slug: string): string {\n const normalized = normalizeScreenSlug(slug)\n if (!normalized || normalized === SCREEN_ROOT_PATH) return '/'\n return `/${normalized.replace(/^\\/+/, '')}`\n}\n\ntype RawNode = { componentId?: unknown; props?: Record<string, unknown>; nodes?: unknown; [key: string]: unknown }\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n Boolean(value) && typeof value === 'object' && !Array.isArray(value)\n\nconst childrenOf = (node: RawNode | undefined): string[] =>\n Array.isArray(node?.nodes) ? (node.nodes as unknown[]).filter((id): id is string => typeof id === 'string') : []\n\nconst elementOf = (node: RawNode | undefined): string => {\n const value = node?.props?.['component'] ?? node?.props?.['element']\n return typeof value === 'string' ? value : ''\n}\n\nconst LINK_COMPONENTS = new Set(['muiScreenLink', 'muiButton'])\n\n/**\n * The model's layout with the header's navigation written in: a Page Link to\n * every planned page the header does not already link, placed in the first\n * nav element above the Layout Slot, else in a nav row the platform appends\n * to the Toolbar Content; and every OTHER nav element the model left empty\n * given the same links, so no nav is ever empty. A link to the site's current\n * home page is pointed at the planned home, which replaces it when the site\n * is published. Anything that is not a node map, or has no planned pages, is\n * returned as it came, for the doctrine to judge.\n */\nexport function aiLayoutWithSitePages(\n tree: unknown,\n pages: readonly AiLayoutSitePage[],\n options: { homeScreenIds?: readonly string[] } = {},\n): unknown {\n if (!pages.length || !isRecord(tree) || !isRecord(tree['nodes'])) return tree\n const rootId = typeof tree['rootId'] === 'string' ? tree['rootId'] : null\n const source = tree['nodes'] as Record<string, unknown>\n if (!rootId || !isRecord(source[rootId])) return tree\n const nodes: Record<string, RawNode> = {}\n for (const [id, node] of Object.entries(source)) {\n if (!isRecord(node)) continue\n nodes[id] = {\n ...(node as RawNode),\n ...(isRecord(node['props']) ? { props: { ...(node['props'] as Record<string, unknown>) } } : {}),\n ...(Array.isArray(node['nodes']) ? { nodes: [...(node['nodes'] as unknown[])] } : {}),\n }\n }\n\n // Document order, and where the header ends: the Layout Slot.\n const order: string[] = []\n const seen = new Set<string>()\n const visit = (id: string) => {\n if (seen.has(id) || !nodes[id]) return\n seen.add(id)\n order.push(id)\n for (const child of childrenOf(nodes[id])) visit(child)\n }\n visit(rootId)\n const slotAt = order.findIndex((id) => nodes[id].componentId === 'layoutSlot')\n const header = slotAt === -1 ? order : order.slice(0, slotAt)\n\n // The site's current home is replaced by the planned one at publish.\n const plannedHome = pages.find((page) => screenPath(page.slug) === '/')\n const oldHomes = new Set(options.homeScreenIds ?? [])\n let changed = false\n if (plannedHome) {\n for (const id of order) {\n const node = nodes[id]\n const screenId = node.props?.['screenId']\n if (LINK_COMPONENTS.has(String(node.componentId)) && typeof screenId === 'string' && oldHomes.has(screenId)) {\n node.props = { ...node.props, screenId: plannedHome.id }\n changed = true\n }\n }\n }\n\n const used = new Set(Object.keys(nodes))\n const freshId = (base: string): string => {\n let id = base\n for (let n = 2; used.has(id); n += 1) id = `${base}${n}`\n used.add(id)\n return id\n }\n const linkNodes = (entries: readonly AiLayoutSitePage[]): string[] =>\n entries.map((page) => {\n const id = freshId(`aiNavLink_${page.id.replace(/[^A-Za-z0-9_]/g, '')}`)\n nodes[id] = {\n componentId: 'muiScreenLink',\n props: { children: page.label.slice(0, 40), screenId: page.id, renderAs: 'link', color: 'inherit' },\n }\n return id\n })\n\n const linkedInHeader = new Set(\n header\n .map((id) => nodes[id])\n .filter((node) => LINK_COMPONENTS.has(String(node.componentId)))\n .map((node) => node.props?.['screenId'])\n .filter((value): value is string => typeof value === 'string'),\n )\n const missing = pages.filter((page) => !linkedInHeader.has(page.id))\n const navs = order.filter((id) => elementOf(nodes[id]) === 'nav')\n const headerNav = navs.find((id) => header.includes(id)) ?? null\n\n if (missing.length) {\n if (headerNav) {\n nodes[headerNav].nodes = [...childrenOf(nodes[headerNav]), ...linkNodes(missing)]\n changed = true\n } else {\n const toolbar = header.find((id) => nodes[id].componentId === 'muiToolbar')\n if (toolbar) {\n const navId = freshId('aiSiteNav')\n nodes[navId] = {\n componentId: 'muiStack',\n props: { component: 'nav', direction: 'row', alignItems: 'center' },\n sx: { gap: 2 },\n nodes: linkNodes(missing),\n }\n nodes[toolbar].nodes = [...childrenOf(nodes[toolbar]), navId]\n changed = true\n }\n }\n }\n // A nav element the model left empty anywhere else — a footer's, say — links the same pages.\n for (const id of navs) {\n if (id === headerNav && missing.length) continue\n if (childrenOf(nodes[id]).some((child) => nodes[child])) continue\n nodes[id].nodes = linkNodes(pages)\n changed = true\n }\n return changed ? { ...tree, nodes } : tree\n}\n"],"names":["normalizeScreenSlug","SCREEN_ROOT_PATH","AI_LAYOUT_SITE_PAGES_INPUT","AI_LAYOUT_SITE_PAGES_MAX","aiLayoutIsHomeSlug","slug","replace","CONFIRMATION","aiLayoutSitePagesOfPlan","screens","options","linked","screen","guided","test","title","nav","pages","filter","record","id","map","label","trim","page","slice","aiLayoutSitePages","inputs","raw","Array","isArray","entry","some","push","aiLayoutSitePagesLines","length","screenPath","join","normalized","isRecord","value","Boolean","childrenOf","node","nodes","elementOf","props","LINK_COMPONENTS","Set","aiLayoutWithSitePages","tree","navs","rootId","source","Object","entries","order","seen","visit","has","add","child","slotAt","findIndex","componentId","header","plannedHome","find","oldHomes","homeScreenIds","changed","screenId","String","used","keys","freshId","base","n","linkNodes","children","renderAs","color","linkedInHeader","missing","headerNav","includes","toolbar","navId","component","direction","alignItems","sx","gap"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,mBAAmB,EAAEC,gBAAgB,QAAQ,sCAAqC;AAG3F;;;;;;;;;;;;;;;;;;;CAmBC,GAED,+DAA+D,GAC/D,OAAO,MAAMC,6BAA6B,YAAW;AAErD,wEAAwE,GACxE,OAAO,MAAMC,2BAA2B,EAAC;AASzC,sDAAsD,GACtD,OAAO,SAASC,mBAAmBC,IAAY;IAC7C,OAAOA,KAAKC,OAAO,CAAC,cAAc,QAAQ;AAC5C;AAEA,6EAA6E,GAC7E,MAAMC,eAAe;AAErB;;;;;;;CAOC,GACD,OAAO,SAASC,wBACdC,OAAqC,EACrCC,UAAgC,CAAC,CAAC;IAElC,MAAMC,SAAS,CAACC,SACdR,mBAAmBQ,OAAOP,IAAI,KAC7BK,CAAAA,QAAQG,MAAM,GAAG,CAACN,aAAaO,IAAI,CAAC,GAAGF,OAAOP,IAAI,CAAC,CAAC,EAAEO,OAAOG,KAAK,EAAE,IAAI,CAAC,CAACH,OAAOI,GAAG,AAAD;IACtF,MAAMC,QAAQR,QACXS,MAAM,CAAC,CAACN,SAAW,CAACA,OAAOO,MAAM,IAAI,OAAOP,OAAOQ,EAAE,KAAK,YAAYR,OAAOQ,EAAE,IAAIT,OAAOC,SAC1FS,GAAG,CAAC,CAACT,SAAY,CAAA;YAAEQ,IAAIR,OAAOQ,EAAE;YAAYE,OAAOV,OAAOG,KAAK,CAACQ,IAAI;YAAIlB,MAAMO,OAAOP,IAAI;QAAC,CAAA,GAC1Fa,MAAM,CAAC,CAACM,OAASA,KAAKF,KAAK;IAC9B,OAAO;WAAIL,MAAMC,MAAM,CAAC,CAACM,OAASpB,mBAAmBoB,KAAKnB,IAAI;WAAOY,MAAMC,MAAM,CAAC,CAACM,OAAS,CAACpB,mBAAmBoB,KAAKnB,IAAI;KAAG,CAACoB,KAAK,CAChI,GACAtB;AAEJ;AAEA,+EAA+E,GAC/E,OAAO,SAASuB,kBAAkBC,MAA4D;IAC5F,MAAMC,MAAMD,0BAAAA,MAAQ,CAACzB,2BAA2B;IAChD,IAAI,CAAC2B,MAAMC,OAAO,CAACF,MAAM,OAAO,EAAE;IAClC,MAAMX,QAA4B,EAAE;IACpC,KAAK,MAAMc,SAASH,IAAK;QACvB,IAAI,CAACG,SAAS,OAAOA,UAAU,UAAU;QACzC,MAAM,EAAEX,EAAE,EAAEE,KAAK,EAAEjB,IAAI,EAAE,GAAG0B;QAC5B,IAAI,OAAOX,OAAO,YAAY,CAACA,MAAM,OAAOE,UAAU,YAAY,CAACA,MAAMC,IAAI,IAAI;QACjF,IAAIN,MAAMe,IAAI,CAAC,CAACR,OAASA,KAAKJ,EAAE,KAAKA,KAAK;QAC1CH,MAAMgB,IAAI,CAAC;YAAEb;YAAIE,OAAOA,MAAMC,IAAI;YAAIlB,MAAM,OAAOA,SAAS,WAAWA,OAAO;QAAG;IACnF;IACA,OAAOY,MAAMQ,KAAK,CAAC,GAAGtB;AACxB;AAEA;;;;CAIC,GACD,OAAO,SAAS+B,uBAAuBjB,KAAkC;IACvE,IAAI,CAACA,MAAMkB,MAAM,EAAE,OAAO,EAAE;IAC5B,OAAO;QACL,CAAC,+CAA+C,EAAElB,MAC/CI,GAAG,CAAC,CAACG,OAAS,GAAGA,KAAKF,KAAK,CAAC,EAAE,EAAEc,WAAWZ,KAAKnB,IAAI,EAAE,CAAC,CAAC,EACxDgC,IAAI,CAAC,MAAM,CAAC,CAAC;QAChB;KACD;AACH;AAEA,SAASD,WAAW/B,IAAY;IAC9B,MAAMiC,aAAatC,oBAAoBK;IACvC,IAAI,CAACiC,cAAcA,eAAerC,kBAAkB,OAAO;IAC3D,OAAO,CAAC,CAAC,EAAEqC,WAAWhC,OAAO,CAAC,QAAQ,KAAK;AAC7C;AAIA,MAAMiC,WAAW,CAACC,QAChBC,QAAQD,UAAU,OAAOA,UAAU,YAAY,CAACX,MAAMC,OAAO,CAACU;AAEhE,MAAME,aAAa,CAACC,OAClBd,MAAMC,OAAO,CAACa,wBAAAA,KAAMC,KAAK,IAAI,AAACD,KAAKC,KAAK,CAAe1B,MAAM,CAAC,CAACE,KAAqB,OAAOA,OAAO,YAAY,EAAE;AAElH,MAAMyB,YAAY,CAACF;;QACHA,aAA8BA;IAA5C,MAAMH,gBAAQG,yBAAAA,cAAAA,KAAMG,KAAK,qBAAXH,WAAa,CAAC,YAAY,mBAAIA,yBAAAA,eAAAA,KAAMG,KAAK,qBAAXH,YAAa,CAAC,UAAU;IACpE,OAAO,OAAOH,UAAU,WAAWA,QAAQ;AAC7C;AAEA,MAAMO,kBAAkB,IAAIC,IAAI;IAAC;IAAiB;CAAY;AAE9D;;;;;;;;;CASC,GACD,OAAO,SAASC,sBACdC,IAAa,EACbjC,KAAkC,EAClCP,UAAiD,CAAC,CAAC;QA+B1BA,wBAuCPyC;IApElB,IAAI,CAAClC,MAAMkB,MAAM,IAAI,CAACI,SAASW,SAAS,CAACX,SAASW,IAAI,CAAC,QAAQ,GAAG,OAAOA;IACzE,MAAME,SAAS,OAAOF,IAAI,CAAC,SAAS,KAAK,WAAWA,IAAI,CAAC,SAAS,GAAG;IACrE,MAAMG,SAASH,IAAI,CAAC,QAAQ;IAC5B,IAAI,CAACE,UAAU,CAACb,SAASc,MAAM,CAACD,OAAO,GAAG,OAAOF;IACjD,MAAMN,QAAiC,CAAC;IACxC,KAAK,MAAM,CAACxB,IAAIuB,KAAK,IAAIW,OAAOC,OAAO,CAACF,QAAS;QAC/C,IAAI,CAACd,SAASI,OAAO;QACrBC,KAAK,CAACxB,GAAG,GAAG,aACNuB,MACAJ,SAASI,IAAI,CAAC,QAAQ,IAAI;YAAEG,OAAO,aAAMH,IAAI,CAAC,QAAQ;QAA+B,IAAI,CAAC,GAC1Fd,MAAMC,OAAO,CAACa,IAAI,CAAC,QAAQ,IAAI;YAAEC,OAAO;mBAAKD,IAAI,CAAC,QAAQ;aAAe;QAAC,IAAI,CAAC;IAEvF;IAEA,8DAA8D;IAC9D,MAAMa,QAAkB,EAAE;IAC1B,MAAMC,OAAO,IAAIT;IACjB,MAAMU,QAAQ,CAACtC;QACb,IAAIqC,KAAKE,GAAG,CAACvC,OAAO,CAACwB,KAAK,CAACxB,GAAG,EAAE;QAChCqC,KAAKG,GAAG,CAACxC;QACToC,MAAMvB,IAAI,CAACb;QACX,KAAK,MAAMyC,SAASnB,WAAWE,KAAK,CAACxB,GAAG,EAAGsC,MAAMG;IACnD;IACAH,MAAMN;IACN,MAAMU,SAASN,MAAMO,SAAS,CAAC,CAAC3C,KAAOwB,KAAK,CAACxB,GAAG,CAAC4C,WAAW,KAAK;IACjE,MAAMC,SAASH,WAAW,CAAC,IAAIN,QAAQA,MAAM/B,KAAK,CAAC,GAAGqC;IAEtD,qEAAqE;IACrE,MAAMI,cAAcjD,MAAMkD,IAAI,CAAC,CAAC3C,OAASY,WAAWZ,KAAKnB,IAAI,MAAM;IACnE,MAAM+D,WAAW,IAAIpB,KAAItC,yBAAAA,QAAQ2D,aAAa,YAArB3D,yBAAyB,EAAE;IACpD,IAAI4D,UAAU;IACd,IAAIJ,aAAa;QACf,KAAK,MAAM9C,MAAMoC,MAAO;gBAELb;YADjB,MAAMA,OAAOC,KAAK,CAACxB,GAAG;YACtB,MAAMmD,YAAW5B,cAAAA,KAAKG,KAAK,qBAAVH,WAAY,CAAC,WAAW;YACzC,IAAII,gBAAgBY,GAAG,CAACa,OAAO7B,KAAKqB,WAAW,MAAM,OAAOO,aAAa,YAAYH,SAAST,GAAG,CAACY,WAAW;gBAC3G5B,KAAKG,KAAK,GAAG,aAAKH,KAAKG,KAAK;oBAAEyB,UAAUL,YAAY9C,EAAE;;gBACtDkD,UAAU;YACZ;QACF;IACF;IAEA,MAAMG,OAAO,IAAIzB,IAAIM,OAAOoB,IAAI,CAAC9B;IACjC,MAAM+B,UAAU,CAACC;QACf,IAAIxD,KAAKwD;QACT,IAAK,IAAIC,IAAI,GAAGJ,KAAKd,GAAG,CAACvC,KAAKyD,KAAK,EAAGzD,KAAK,GAAGwD,OAAOC,GAAG;QACxDJ,KAAKb,GAAG,CAACxC;QACT,OAAOA;IACT;IACA,MAAM0D,YAAY,CAACvB,UACjBA,QAAQlC,GAAG,CAAC,CAACG;YACX,MAAMJ,KAAKuD,QAAQ,CAAC,UAAU,EAAEnD,KAAKJ,EAAE,CAACd,OAAO,CAAC,kBAAkB,KAAK;YACvEsC,KAAK,CAACxB,GAAG,GAAG;gBACV4C,aAAa;gBACblB,OAAO;oBAAEiC,UAAUvD,KAAKF,KAAK,CAACG,KAAK,CAAC,GAAG;oBAAK8C,UAAU/C,KAAKJ,EAAE;oBAAE4D,UAAU;oBAAQC,OAAO;gBAAU;YACpG;YACA,OAAO7D;QACT;IAEF,MAAM8D,iBAAiB,IAAIlC,IACzBiB,OACG5C,GAAG,CAAC,CAACD,KAAOwB,KAAK,CAACxB,GAAG,EACrBF,MAAM,CAAC,CAACyB,OAASI,gBAAgBY,GAAG,CAACa,OAAO7B,KAAKqB,WAAW,IAC5D3C,GAAG,CAAC,CAACsB;YAASA;gBAAAA,cAAAA,KAAKG,KAAK,qBAAVH,WAAY,CAAC,WAAW;OACtCzB,MAAM,CAAC,CAACsB,QAA2B,OAAOA,UAAU;IAEzD,MAAM2C,UAAUlE,MAAMC,MAAM,CAAC,CAACM,OAAS,CAAC0D,eAAevB,GAAG,CAACnC,KAAKJ,EAAE;IAClE,MAAM+B,OAAOK,MAAMtC,MAAM,CAAC,CAACE,KAAOyB,UAAUD,KAAK,CAACxB,GAAG,MAAM;IAC3D,MAAMgE,aAAYjC,aAAAA,KAAKgB,IAAI,CAAC,CAAC/C,KAAO6C,OAAOoB,QAAQ,CAACjE,gBAAlC+B,aAA0C;IAE5D,IAAIgC,QAAQhD,MAAM,EAAE;QAClB,IAAIiD,WAAW;YACbxC,KAAK,CAACwC,UAAU,CAACxC,KAAK,GAAG;mBAAIF,WAAWE,KAAK,CAACwC,UAAU;mBAAMN,UAAUK;aAAS;YACjFb,UAAU;QACZ,OAAO;YACL,MAAMgB,UAAUrB,OAAOE,IAAI,CAAC,CAAC/C,KAAOwB,KAAK,CAACxB,GAAG,CAAC4C,WAAW,KAAK;YAC9D,IAAIsB,SAAS;gBACX,MAAMC,QAAQZ,QAAQ;gBACtB/B,KAAK,CAAC2C,MAAM,GAAG;oBACbvB,aAAa;oBACblB,OAAO;wBAAE0C,WAAW;wBAAOC,WAAW;wBAAOC,YAAY;oBAAS;oBAClEC,IAAI;wBAAEC,KAAK;oBAAE;oBACbhD,OAAOkC,UAAUK;gBACnB;gBACAvC,KAAK,CAAC0C,QAAQ,CAAC1C,KAAK,GAAG;uBAAIF,WAAWE,KAAK,CAAC0C,QAAQ;oBAAGC;iBAAM;gBAC7DjB,UAAU;YACZ;QACF;IACF;IACA,6FAA6F;IAC7F,KAAK,MAAMlD,MAAM+B,KAAM;QACrB,IAAI/B,OAAOgE,aAAaD,QAAQhD,MAAM,EAAE;QACxC,IAAIO,WAAWE,KAAK,CAACxB,GAAG,EAAEY,IAAI,CAAC,CAAC6B,QAAUjB,KAAK,CAACiB,MAAM,GAAG;QACzDjB,KAAK,CAACxB,GAAG,CAACwB,KAAK,GAAGkC,UAAU7D;QAC5BqD,UAAU;IACZ;IACA,OAAOA,UAAU,aAAKpB;QAAMN;SAAUM;AACxC"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-layout-site-pages.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 { normalizeScreenSlug, SCREEN_ROOT_PATH } from '@aglyn/aglyn/app-utils/screen-route'\nimport type { AiBuildPlanScreen } from '../model/ai-build-plan'\n\n/**\n * The navigation of a layout a site scaffold builds before its pages (AGL-3596).\n *\n * A scaffold builds its layout FIRST, because every page renders inside it,\n * so when the layout is generated none of the site's pages exists yet: the\n * model cannot link them, and the layout instructions forbid inventing a page\n * id. A plan that gave the layout a nav region then left the model one way to\n * keep the promise — a nav element with nothing in it — which rule 16 refuses\n * as an empty container, twice, and the guided start stopped there.\n *\n * The scaffold does know the pages: their ids are minted when the plan is\n * kept (`AiBuildPlanScreen.id`), and each page unit writes its draft under\n * that id. So the layout unit is handed them (`sitePages`), and the header's\n * navigation is written by the platform rather than asked of the model: one\n * Page Link per planned page, inside the nav element the model left (filled,\n * never left empty) or a nav row the platform adds to the Toolbar Content.\n * The completed tree is what the doctrine checks and what is written, so rule\n * 16 holds unchanged — nothing it sees is empty — and the publish step, which\n * adds only the entries a header does not already carry, adds nothing twice.\n */\n\n/** The unit input a site layout's planned pages travel under. */\nexport const AI_LAYOUT_SITE_PAGES_INPUT = 'sitePages'\n\n/** The most pages the header links, as the layout instructions cap it. */\nexport const AI_LAYOUT_SITE_PAGES_MAX = 7\n\n/** A page the scaffold builds after the layout, as the layout links it. */\nexport interface AiLayoutSitePage {\n id: string\n label: string\n slug: string\n /** The path of a destination that is no page, such as the blog a start writes (AGL-3660). */\n href?: string\n}\n\n/** Whether a slug is the site's root, the home page. */\nexport function aiLayoutIsHomeSlug(slug: string): boolean {\n return slug.replace(/^\\/+|\\/+$/g, '') === ''\n}\n\n/** A page that confirms what a visitor just did, which no navigation links. */\nconst CONFIRMATION = /thank|confirm|success|submitted/i\n\n/**\n * The pages a plan puts in the navigation, record templates aside, the home\n * page first whatever its `nav` flag says (AGL-3660): a guided start's plan\n * marked its Home `nav: false`, and the header linked only Contact. Every\n * other page keeps its `nav` flag, except in a guided start (`guided`), whose\n * pages are the ones the person asked for: each is linked, but a\n * confirmation page — a thank-you, a \"submitted\" — stays out.\n */\nexport function aiLayoutSitePagesOfPlan(\n screens: readonly AiBuildPlanScreen[],\n options: { guided?: boolean } = {},\n): AiLayoutSitePage[] {\n const linked = (screen: AiBuildPlanScreen) =>\n aiLayoutIsHomeSlug(screen.slug) ||\n (options.guided ? !CONFIRMATION.test(`${screen.slug} ${screen.title}`) : !!screen.nav)\n const pages = screens\n .filter((screen) => !screen.record && typeof screen.id === 'string' && screen.id && linked(screen))\n .map((screen) => ({ id: screen.id as string, label: screen.title.trim(), slug: screen.slug }))\n .filter((page) => page.label)\n return [...pages.filter((page) => aiLayoutIsHomeSlug(page.slug)), ...pages.filter((page) => !aiLayoutIsHomeSlug(page.slug))].slice(\n 0,\n AI_LAYOUT_SITE_PAGES_MAX,\n )\n}\n\n/** The planned pages a layout unit's inputs carry; none for any other layout. */\nexport function aiLayoutSitePages(inputs: Readonly<Record<string, unknown>> | null | undefined): AiLayoutSitePage[] {\n const raw = inputs?.[AI_LAYOUT_SITE_PAGES_INPUT]\n if (!Array.isArray(raw)) return []\n const pages: AiLayoutSitePage[] = []\n for (const entry of raw) {\n if (!entry || typeof entry !== 'object') continue\n const { id, label, slug, href } = entry as Record<string, unknown>\n if (typeof id !== 'string' || !id || typeof label !== 'string' || !label.trim()) continue\n if (pages.some((page) => page.id === id)) continue\n pages.push({\n id,\n label: label.trim(),\n slug: typeof slug === 'string' ? slug : '',\n // Only a path on this site, never an address elsewhere.\n ...(typeof href === 'string' && /^\\/[a-z0-9-]+(?:\\/[a-z0-9-]+)*$/.test(href) ? { href } : {}),\n })\n }\n return pages.slice(0, AI_LAYOUT_SITE_PAGES_MAX)\n}\n\n/**\n * What the layout's user turn says about those pages: that the platform\n * writes the navigation, so the model builds none and leaves no empty element\n * for it. The same promise the completion below keeps.\n */\nexport function aiLayoutSitePagesLines(pages: readonly AiLayoutSitePage[]): string[] {\n if (!pages.length) return []\n return [\n `This site’s pages are built after this layout: ${pages\n .map((page) => `${page.label} (${screenPath(page.slug)})`)\n .join(', ')}.`,\n 'The platform writes the header’s navigation to those pages itself, as a nav row of Page Links inside the Toolbar Content. Build no navigation links to them and no nav element for them: leave no empty element where the links will go.',\n ]\n}\n\nfunction screenPath(slug: string): string {\n const normalized = normalizeScreenSlug(slug)\n if (!normalized || normalized === SCREEN_ROOT_PATH) return '/'\n return `/${normalized.replace(/^\\/+/, '')}`\n}\n\ntype RawNode = { componentId?: unknown; props?: Record<string, unknown>; nodes?: unknown; [key: string]: unknown }\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n Boolean(value) && typeof value === 'object' && !Array.isArray(value)\n\nconst childrenOf = (node: RawNode | undefined): string[] =>\n Array.isArray(node?.nodes) ? (node.nodes as unknown[]).filter((id): id is string => typeof id === 'string') : []\n\nconst elementOf = (node: RawNode | undefined): string => {\n const value = node?.props?.['component'] ?? node?.props?.['element']\n return typeof value === 'string' ? value : ''\n}\n\nconst LINK_COMPONENTS = new Set(['muiScreenLink', 'muiButton'])\n\n/**\n * The model's layout with the header's navigation written in: a Page Link to\n * every planned page the header does not already link, placed in the first\n * nav element above the Layout Slot, else in a nav row the platform appends\n * to the Toolbar Content; and every OTHER nav element the model left empty\n * given the same links, so no nav is ever empty. A link to the site's current\n * home page is pointed at the planned home, which replaces it when the site\n * is published. Anything that is not a node map, or has no planned pages, is\n * returned as it came, for the doctrine to judge.\n */\nexport function aiLayoutWithSitePages(\n tree: unknown,\n pages: readonly AiLayoutSitePage[],\n options: { homeScreenIds?: readonly string[] } = {},\n): unknown {\n if (!pages.length || !isRecord(tree) || !isRecord(tree['nodes'])) return tree\n const rootId = typeof tree['rootId'] === 'string' ? tree['rootId'] : null\n const source = tree['nodes'] as Record<string, unknown>\n if (!rootId || !isRecord(source[rootId])) return tree\n const nodes: Record<string, RawNode> = {}\n for (const [id, node] of Object.entries(source)) {\n if (!isRecord(node)) continue\n nodes[id] = {\n ...(node as RawNode),\n ...(isRecord(node['props']) ? { props: { ...(node['props'] as Record<string, unknown>) } } : {}),\n ...(Array.isArray(node['nodes']) ? { nodes: [...(node['nodes'] as unknown[])] } : {}),\n }\n }\n\n // Document order, and where the header ends: the Layout Slot.\n const order: string[] = []\n const seen = new Set<string>()\n const visit = (id: string) => {\n if (seen.has(id) || !nodes[id]) return\n seen.add(id)\n order.push(id)\n for (const child of childrenOf(nodes[id])) visit(child)\n }\n visit(rootId)\n const slotAt = order.findIndex((id) => nodes[id].componentId === 'layoutSlot')\n const header = slotAt === -1 ? order : order.slice(0, slotAt)\n\n // The site's current home is replaced by the planned one at publish.\n const plannedHome = pages.find((page) => screenPath(page.slug) === '/')\n const oldHomes = new Set(options.homeScreenIds ?? [])\n let changed = false\n if (plannedHome) {\n for (const id of order) {\n const node = nodes[id]\n const screenId = node.props?.['screenId']\n if (LINK_COMPONENTS.has(String(node.componentId)) && typeof screenId === 'string' && oldHomes.has(screenId)) {\n node.props = { ...node.props, screenId: plannedHome.id }\n changed = true\n }\n }\n }\n\n const used = new Set(Object.keys(nodes))\n const freshId = (base: string): string => {\n let id = base\n for (let n = 2; used.has(id); n += 1) id = `${base}${n}`\n used.add(id)\n return id\n }\n const linkNodes = (entries: readonly AiLayoutSitePage[]): string[] =>\n entries.map((page) => {\n const id = freshId(`aiNavLink_${page.id.replace(/[^A-Za-z0-9_]/g, '')}`)\n nodes[id] = {\n componentId: 'muiScreenLink',\n props: {\n children: page.label.slice(0, 40),\n ...(page.href ? { href: page.href } : { screenId: page.id }),\n renderAs: 'link',\n color: 'inherit',\n },\n }\n return id\n })\n\n const linkedInHeader = new Set(\n header\n .map((id) => nodes[id])\n .filter((node) => LINK_COMPONENTS.has(String(node.componentId)))\n .flatMap((node) => [node.props?.['screenId'], node.props?.['href']])\n .filter((value): value is string => typeof value === 'string'),\n )\n const missing = pages.filter((page) => !linkedInHeader.has(page.href ?? page.id))\n const navs = order.filter((id) => elementOf(nodes[id]) === 'nav')\n const headerNav = navs.find((id) => header.includes(id)) ?? null\n\n if (missing.length) {\n if (headerNav) {\n nodes[headerNav].nodes = [...childrenOf(nodes[headerNav]), ...linkNodes(missing)]\n changed = true\n } else {\n const toolbar = header.find((id) => nodes[id].componentId === 'muiToolbar')\n if (toolbar) {\n const navId = freshId('aiSiteNav')\n nodes[navId] = {\n componentId: 'muiStack',\n props: { component: 'nav', direction: 'row', alignItems: 'center' },\n sx: { gap: 2 },\n nodes: linkNodes(missing),\n }\n nodes[toolbar].nodes = [...childrenOf(nodes[toolbar]), navId]\n changed = true\n }\n }\n }\n // A nav element the model left empty anywhere else — a footer's, say — links the same pages.\n for (const id of navs) {\n if (id === headerNav && missing.length) continue\n if (childrenOf(nodes[id]).some((child) => nodes[child])) continue\n nodes[id].nodes = linkNodes(pages)\n changed = true\n }\n return changed ? { ...tree, nodes } : tree\n}\n"],"names":["normalizeScreenSlug","SCREEN_ROOT_PATH","AI_LAYOUT_SITE_PAGES_INPUT","AI_LAYOUT_SITE_PAGES_MAX","aiLayoutIsHomeSlug","slug","replace","CONFIRMATION","aiLayoutSitePagesOfPlan","screens","options","linked","screen","guided","test","title","nav","pages","filter","record","id","map","label","trim","page","slice","aiLayoutSitePages","inputs","raw","Array","isArray","entry","href","some","push","aiLayoutSitePagesLines","length","screenPath","join","normalized","isRecord","value","Boolean","childrenOf","node","nodes","elementOf","props","LINK_COMPONENTS","Set","aiLayoutWithSitePages","tree","navs","rootId","source","Object","entries","order","seen","visit","has","add","child","slotAt","findIndex","componentId","header","plannedHome","find","oldHomes","homeScreenIds","changed","screenId","String","used","keys","freshId","base","n","linkNodes","children","renderAs","color","linkedInHeader","flatMap","missing","headerNav","includes","toolbar","navId","component","direction","alignItems","sx","gap"],"mappings":"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"}
|
|
@@ -34,7 +34,7 @@ import { aiLayoutInventedContactViolations, aiLayoutWithSiteName } from "./ai-la
|
|
|
34
34
|
import { aiLayoutWithFullHeight } from "./ai-layout-full-height.js";
|
|
35
35
|
import { AI_LAYOUT_FRAME_TOOL } from "../layout-language/ai-layout-language.js";
|
|
36
36
|
import { AI_JOB_LAYOUT_LANGUAGE_BUDGET, AI_JOB_LAYOUT_LANGUAGE_INSTRUCTIONS, AI_LAYOUT_FRAME_KIND, aiLayoutFrameCheck, aiLayoutFramePrompt, aiLayoutFrameTargets, aiLayoutHomeId, aiLayoutNavPages, aiLayoutSiteName } from "./ai-job-layout-language.js";
|
|
37
|
-
import { AI_LAYOUT_LANGUAGE_THINKING, aiJobUsesLayoutLanguage } from "./ai-job-page-language.js";
|
|
37
|
+
import { AI_LAYOUT_LANGUAGE_THINKING, aiJobUsesLayoutLanguage, aiLayoutDesignOf } from "./ai-job-page-language.js";
|
|
38
38
|
import { aiJobStepBudget } from "./ai-job-budget.js";
|
|
39
39
|
import { registerAiJobStep } from "./ai-jobs.js";
|
|
40
40
|
/**
|
|
@@ -363,7 +363,8 @@ export function createAiJobLayoutStep(deps = {}) {
|
|
|
363
363
|
homeId: aiLayoutHomeId(pages, inventory),
|
|
364
364
|
pages,
|
|
365
365
|
targets,
|
|
366
|
-
extend: aiLayoutChecks(inventory, plan, job.brief)
|
|
366
|
+
extend: aiLayoutChecks(inventory, plan, job.brief),
|
|
367
|
+
design: aiLayoutDesignOf(job, '/')
|
|
367
368
|
}))
|
|
368
369
|
}, signal ? {
|
|
369
370
|
signal
|