@aglyn/plugins-ai 1.0.0-beta.232 → 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/declarations.server.js +7 -4
- package/src/lib/declarations.server.js.map +1 -1
- package/src/lib/jobs/ai-build-capabilities.js +8 -2
- package/src/lib/jobs/ai-build-capabilities.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 +40 -1
- package/src/lib/jobs/ai-job-plan-step.js +121 -16
- 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-notify.d.ts +14 -4
- package/src/lib/jobs/ai-jobs-notify.js +70 -4
- package/src/lib/jobs/ai-jobs-notify.js.map +1 -1
- package/src/lib/jobs/ai-jobs.d.ts +22 -4
- package/src/lib/jobs/ai-jobs.js +47 -12
- 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 +23 -0
- package/src/lib/model/ai-plan-capabilities.js +5 -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/operator-alerts.d.ts +18 -0
- package/src/lib/operator-alerts.js +42 -0
- package/src/lib/operator-alerts.js.map +1 -1
- package/src/lib/runtime/ai-doctrine-validators.d.ts +39 -1
- package/src/lib/runtime/ai-doctrine-validators.js +236 -6
- 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
|
@@ -137,8 +137,47 @@ export interface AiEmailCopy {
|
|
|
137
137
|
preheaders: string[];
|
|
138
138
|
}
|
|
139
139
|
export declare function parseAiEmailCopy(answer: Readonly<Record<string, unknown>>): AiEmailCopy;
|
|
140
|
+
/**
|
|
141
|
+
* A subject line or preheader with its merge tokens taken out (AGL-3676). Such
|
|
142
|
+
* a line is never filled — the send fills tokens in the email's text only —
|
|
143
|
+
* so `{{contact.firstName}}` there would reach the inbox as written. The
|
|
144
|
+
* guided start's welcome email kept greeting by name in its subject and
|
|
145
|
+
* stopped on `email-header-token` after its re-ask, so the token is removed
|
|
146
|
+
* and the line around it tidied, never filled with a name:
|
|
147
|
+
*
|
|
148
|
+
* - a token that ends a clause takes the words left dangling in front of it,
|
|
149
|
+
* `Welcome to {{business}}!` → `Welcome!`;
|
|
150
|
+
* - a separator left with nothing after it goes, `Welcome, {{firstName}}!` →
|
|
151
|
+
* `Welcome!`, and a doubled one collapses;
|
|
152
|
+
* - a line that opened with the token opens with what followed it,
|
|
153
|
+
* capitalized: `{{firstName}}, your table is ready` → `Your table is ready`.
|
|
154
|
+
*
|
|
155
|
+
* A line with no token is returned as it was; one that is empty once settled
|
|
156
|
+
* is left empty, for the copy's own rules to refuse.
|
|
157
|
+
*/
|
|
158
|
+
export declare function aiSettleEmailHeaderLine(line: string): string;
|
|
159
|
+
/**
|
|
160
|
+
* The copy as it is kept (AGL-3676): each line's merge tokens taken out, and,
|
|
161
|
+
* where the model wrote more than three distinct lines of one kind, the first
|
|
162
|
+
* three of them — the count rule is a cap, not a judgment.
|
|
163
|
+
*/
|
|
164
|
+
export declare function aiSettleEmailCopy(copy: AiEmailCopy): AiEmailCopy;
|
|
140
165
|
/** The copy's own rules: three different lines of each, within their lengths, with no merge token. */
|
|
141
166
|
export declare function aiEmailCopyViolations(copy: AiEmailCopy): AiDoctrineViolation[];
|
|
167
|
+
/**
|
|
168
|
+
* An email's node map settled where it has one reading (AGL-3676), before the
|
|
169
|
+
* doctrine reads it:
|
|
170
|
+
*
|
|
171
|
+
* - a recipient token under another spelling — `{{firstName}}`,
|
|
172
|
+
* `{{contact.first_name}}`, `{{ email }}` — is written as the token the
|
|
173
|
+
* send fills, in text; any other token is left for the check to refuse;
|
|
174
|
+
* - a link that opens with the site's address token, `{{site.url}}/menu`, is
|
|
175
|
+
* the page address the design completes with that token itself (see
|
|
176
|
+
* `aiEmailDesignNodes`), so it is written as `/menu`.
|
|
177
|
+
*
|
|
178
|
+
* Reads and returns the tree as the model wrote it.
|
|
179
|
+
*/
|
|
180
|
+
export declare function aiSettleEmailTree(input: unknown): unknown;
|
|
142
181
|
/** The design's own rules, on a tree the doctrine admitted. */
|
|
143
182
|
export declare function aiEmailTreeViolations(tree: AiValidatedTree, products: number): AiDoctrineViolation[];
|
|
144
183
|
/**
|
|
@@ -125,12 +125,13 @@ import { registerAiJobStep } from "./ai-jobs.js";
|
|
|
125
125
|
text: [
|
|
126
126
|
'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.',
|
|
127
127
|
'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).',
|
|
128
|
-
'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.',
|
|
128
|
+
'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.',
|
|
129
129
|
'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.',
|
|
130
130
|
'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.',
|
|
131
131
|
'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.',
|
|
132
132
|
'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.',
|
|
133
|
-
`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
|
|
133
|
+
`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.`,
|
|
134
|
+
'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}}".'
|
|
134
135
|
].join('\n')
|
|
135
136
|
}
|
|
136
137
|
];
|
|
@@ -154,14 +155,14 @@ const DOCTRINE_EMAIL_TOOL = aiDoctrineTreeTool('email');
|
|
|
154
155
|
items: {
|
|
155
156
|
type: 'string'
|
|
156
157
|
},
|
|
157
|
-
description: 'Three subject lines that take different approaches, the strongest first.'
|
|
158
|
+
description: 'Three subject lines that take different approaches, the strongest first. Plain text: no {{…}} merge token.'
|
|
158
159
|
},
|
|
159
160
|
preheaders: {
|
|
160
161
|
type: 'array',
|
|
161
162
|
items: {
|
|
162
163
|
type: 'string'
|
|
163
164
|
},
|
|
164
|
-
description: 'Three preheaders, each extending the subject line in the same position.'
|
|
165
|
+
description: 'Three preheaders, each extending the subject line in the same position. Plain text: no {{…}} merge token.'
|
|
165
166
|
}
|
|
166
167
|
}
|
|
167
168
|
}
|
|
@@ -195,6 +196,57 @@ export function parseAiEmailCopy(answer) {
|
|
|
195
196
|
preheaders: lines(answer['preheaders'])
|
|
196
197
|
};
|
|
197
198
|
}
|
|
199
|
+
/**
|
|
200
|
+
* A merge token that ends a clause, with the words it leaves dangling in
|
|
201
|
+
* front of it — "to", "for", "your" — and an apostrophe-s it took.
|
|
202
|
+
*/ const CLAUSE_END_TOKEN = /(?:\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;
|
|
203
|
+
/** Any other merge token, with an apostrophe-s it took. */ const INNER_TOKEN = /\{\{[^{}]*\}\}(?:['’]s\b)?/g;
|
|
204
|
+
/**
|
|
205
|
+
* A subject line or preheader with its merge tokens taken out (AGL-3676). Such
|
|
206
|
+
* a line is never filled — the send fills tokens in the email's text only —
|
|
207
|
+
* so `{{contact.firstName}}` there would reach the inbox as written. The
|
|
208
|
+
* guided start's welcome email kept greeting by name in its subject and
|
|
209
|
+
* stopped on `email-header-token` after its re-ask, so the token is removed
|
|
210
|
+
* and the line around it tidied, never filled with a name:
|
|
211
|
+
*
|
|
212
|
+
* - a token that ends a clause takes the words left dangling in front of it,
|
|
213
|
+
* `Welcome to {{business}}!` → `Welcome!`;
|
|
214
|
+
* - a separator left with nothing after it goes, `Welcome, {{firstName}}!` →
|
|
215
|
+
* `Welcome!`, and a doubled one collapses;
|
|
216
|
+
* - a line that opened with the token opens with what followed it,
|
|
217
|
+
* capitalized: `{{firstName}}, your table is ready` → `Your table is ready`.
|
|
218
|
+
*
|
|
219
|
+
* A line with no token is returned as it was; one that is empty once settled
|
|
220
|
+
* is left empty, for the copy's own rules to refuse.
|
|
221
|
+
*/ export function aiSettleEmailHeaderLine(line) {
|
|
222
|
+
if (!line.includes('{{')) return line;
|
|
223
|
+
const opened = /^\s*\{\{/.test(line);
|
|
224
|
+
let out = line.replace(CLAUSE_END_TOKEN, '').replace(INNER_TOKEN, '').replace(/\s+/g, ' ').replace(/\s+([!?.,;:])/g, '$1').replace(/([,;:—–-])(?:\s*[,;:—–-])+/g, '$1').replace(/[,;:—–-]+\s*(?=[!?.]|$)/g, '').replace(/^[\s,;:—–!?.-]+/, '').trim();
|
|
225
|
+
if (opened && out) out = out[0].toUpperCase() + out.slice(1);
|
|
226
|
+
return out;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* The copy as it is kept (AGL-3676): each line's merge tokens taken out, and,
|
|
230
|
+
* where the model wrote more than three distinct lines of one kind, the first
|
|
231
|
+
* three of them — the count rule is a cap, not a judgment.
|
|
232
|
+
*/ export function aiSettleEmailCopy(copy) {
|
|
233
|
+
const settle = (lines)=>{
|
|
234
|
+
const settled = lines.map(aiSettleEmailHeaderLine);
|
|
235
|
+
if (settled.length <= AI_EMAIL_VARIANTS) return settled;
|
|
236
|
+
const seen = new Set();
|
|
237
|
+
const distinct = settled.filter((line)=>{
|
|
238
|
+
const key = line.toLowerCase();
|
|
239
|
+
if (!line || seen.has(key)) return false;
|
|
240
|
+
seen.add(key);
|
|
241
|
+
return true;
|
|
242
|
+
});
|
|
243
|
+
return distinct.length >= AI_EMAIL_VARIANTS ? distinct.slice(0, AI_EMAIL_VARIANTS) : settled;
|
|
244
|
+
};
|
|
245
|
+
return {
|
|
246
|
+
subjects: settle(copy.subjects),
|
|
247
|
+
preheaders: settle(copy.preheaders)
|
|
248
|
+
};
|
|
249
|
+
}
|
|
198
250
|
/** The copy's own rules: three different lines of each, within their lengths, with no merge token. */ export function aiEmailCopyViolations(copy) {
|
|
199
251
|
const violations = [];
|
|
200
252
|
const distinct = (lines)=>new Set(lines.filter(Boolean).map((line)=>line.toLowerCase())).size;
|
|
@@ -245,6 +297,62 @@ function visitsOf(tree) {
|
|
|
245
297
|
nodes: tree.nodes
|
|
246
298
|
});
|
|
247
299
|
}
|
|
300
|
+
/** The recipient tokens a model writes under another spelling, by that spelling folded. */ const RECIPIENT_TOKEN_ALIASES = {
|
|
301
|
+
firstname: 'contact.firstName',
|
|
302
|
+
name: 'contact.name',
|
|
303
|
+
fullname: 'contact.name',
|
|
304
|
+
email: 'contact.email',
|
|
305
|
+
emailaddress: 'contact.email'
|
|
306
|
+
};
|
|
307
|
+
function isRecord(value) {
|
|
308
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* An email's node map settled where it has one reading (AGL-3676), before the
|
|
312
|
+
* doctrine reads it:
|
|
313
|
+
*
|
|
314
|
+
* - a recipient token under another spelling — `{{firstName}}`,
|
|
315
|
+
* `{{contact.first_name}}`, `{{ email }}` — is written as the token the
|
|
316
|
+
* send fills, in text; any other token is left for the check to refuse;
|
|
317
|
+
* - a link that opens with the site's address token, `{{site.url}}/menu`, is
|
|
318
|
+
* the page address the design completes with that token itself (see
|
|
319
|
+
* `aiEmailDesignNodes`), so it is written as `/menu`.
|
|
320
|
+
*
|
|
321
|
+
* Reads and returns the tree as the model wrote it.
|
|
322
|
+
*/ export function aiSettleEmailTree(input) {
|
|
323
|
+
if (!isRecord(input) || !isRecord(input['nodes'])) return input;
|
|
324
|
+
const siteUrl = /^\s*\{\{\s*site\.url\s*\}\}/;
|
|
325
|
+
let nodes = null;
|
|
326
|
+
for (const [id, node] of Object.entries(input['nodes'])){
|
|
327
|
+
if (!isRecord(node) || !isRecord(node['props'])) continue;
|
|
328
|
+
let props = null;
|
|
329
|
+
for (const [name, value] of Object.entries(node['props'])){
|
|
330
|
+
if (typeof value !== 'string' || !value.includes('{{')) continue;
|
|
331
|
+
let next = value;
|
|
332
|
+
if (name === 'href') {
|
|
333
|
+
if (siteUrl.test(value)) next = value.replace(siteUrl, '').trim() || '/';
|
|
334
|
+
if (!next.startsWith('/')) next = value;
|
|
335
|
+
} else {
|
|
336
|
+
next = value.replace(MERGE_TOKEN, (token, key)=>{
|
|
337
|
+
if (AI_EMAIL_TEXT_MERGE_TOKENS.includes(key)) return token;
|
|
338
|
+
const alias = RECIPIENT_TOKEN_ALIASES[key.replace(/^contact\./i, '').replace(/[\s_-]/g, '').toLowerCase()];
|
|
339
|
+
return alias ? `{{${alias}}}` : token;
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
if (next === value) continue;
|
|
343
|
+
props != null ? props : props = _extends({}, node['props']);
|
|
344
|
+
props[name] = next;
|
|
345
|
+
}
|
|
346
|
+
if (!props) continue;
|
|
347
|
+
nodes != null ? nodes : nodes = _extends({}, input['nodes']);
|
|
348
|
+
nodes[id] = _extends({}, node, {
|
|
349
|
+
props
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
return nodes ? _extends({}, input, {
|
|
353
|
+
nodes
|
|
354
|
+
}) : input;
|
|
355
|
+
}
|
|
248
356
|
/** The design's own rules, on a tree the doctrine admitted. */ export function aiEmailTreeViolations(tree, products) {
|
|
249
357
|
var _ref;
|
|
250
358
|
var _tree_nodes_tree_rootId;
|
|
@@ -382,8 +490,11 @@ function visitsOf(tree) {
|
|
|
382
490
|
tool: AI_JOB_EMAIL_TOOL,
|
|
383
491
|
maxTokens: AI_JOB_EMAIL_STEP_BUDGETS[input.step].maxTokens(input.model),
|
|
384
492
|
thinking: 'off',
|
|
493
|
+
// What has one reading is settled, not re-asked (AGL-3676): the tree's
|
|
494
|
+
// token spellings and site-address links, the copy's header tokens.
|
|
495
|
+
complete: aiSettleEmailTree,
|
|
385
496
|
extend: (tree, answer)=>{
|
|
386
|
-
const copy = parseAiEmailCopy(answer);
|
|
497
|
+
const copy = aiSettleEmailCopy(parseAiEmailCopy(answer));
|
|
387
498
|
const nodes = aiEmailDesignNodes(tree, input.products.ids);
|
|
388
499
|
last = {
|
|
389
500
|
copy,
|
|
@@ -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;
|