@aglyn/plugins-ai 1.0.0-beta.221 → 1.0.0-beta.223
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-plan.component.js +1 -4
- package/src/lib/components/ai-job-plan.component.js.map +1 -1
- package/src/lib/jobs/ai-job-crm-step.d.ts +44 -3
- package/src/lib/jobs/ai-job-crm-step.js +298 -8
- package/src/lib/jobs/ai-job-crm-step.js.map +1 -1
- package/src/lib/jobs/ai-job-page-sections.d.ts +33 -1
- package/src/lib/jobs/ai-job-page-sections.js +55 -1
- package/src/lib/jobs/ai-job-page-sections.js.map +1 -1
- package/src/lib/jobs/ai-job-page-step.js +20 -11
- package/src/lib/jobs/ai-job-page-step.js.map +1 -1
- package/src/lib/jobs/ai-job-plan-step.js +6 -2
- package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
- package/src/lib/jobs/ai-job-seo-step.js +52 -15
- package/src/lib/jobs/ai-job-seo-step.js.map +1 -1
- package/src/lib/jobs/ai-job-site-step.js +6 -0
- package/src/lib/jobs/ai-job-site-step.js.map +1 -1
- package/src/lib/jobs/ai-job-workflow-step.js +8 -7
- package/src/lib/jobs/ai-job-workflow-step.js.map +1 -1
- package/src/lib/jobs/ai-workflow-records.d.ts +2 -2
- package/src/lib/jobs/ai-workflow-records.js +2 -2
- package/src/lib/jobs/ai-workflow-records.js.map +1 -1
- package/src/lib/jobs/fixtures/ai-free-page-built-recording.js +1 -0
- package/src/lib/jobs/fixtures/ai-free-page-built-recording.js.map +1 -1
- package/src/lib/jobs/fixtures/ai-free-page-recording.js +2 -0
- package/src/lib/jobs/fixtures/ai-free-page-recording.js.map +1 -1
- package/src/lib/jobs/fixtures/ai-page-briefs.js +2 -0
- package/src/lib/jobs/fixtures/ai-page-briefs.js.map +1 -1
- package/src/lib/model/ai-automation-draft.d.ts +4 -4
- package/src/lib/model/ai-automation-draft.js +15 -10
- package/src/lib/model/ai-automation-draft.js.map +1 -1
- package/src/lib/model/ai-automation-format.d.ts +139 -0
- package/src/lib/model/ai-automation-format.js +42 -0
- package/src/lib/model/ai-automation-format.js.map +1 -0
- package/src/lib/model/ai-automation-outline.d.ts +4 -4
- package/src/lib/model/ai-automation-outline.js +10 -8
- package/src/lib/model/ai-automation-outline.js.map +1 -1
- package/src/lib/model/ai-build-plan.d.ts +39 -2
- package/src/lib/model/ai-build-plan.js +80 -3
- package/src/lib/model/ai-build-plan.js.map +1 -1
- package/src/lib/model/ai-site-inventory.d.ts +5 -0
- package/src/lib/model/ai-site-inventory.js.map +1 -1
- package/src/lib/model/ai-workflow-job.js +2 -2
- package/src/lib/model/ai-workflow-job.js.map +1 -1
- package/src/lib/providers/catalog.d.ts +23 -0
- package/src/lib/providers/catalog.js +29 -1
- package/src/lib/providers/catalog.js.map +1 -1
- package/src/lib/runtime/ai-doctrine-validators.d.ts +8 -0
- package/src/lib/runtime/ai-doctrine-validators.js +34 -3
- package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
- package/src/lib/runtime/ai-doctrine.js +1 -1
- package/src/lib/runtime/ai-doctrine.js.map +1 -1
- package/src/lib/runtime/ai-eval.d.ts +2 -2
- package/src/lib/runtime/ai-eval.js +3 -3
- package/src/lib/runtime/ai-eval.js.map +1 -1
- package/src/lib/runtime/ai-palette.generated.js +23 -1
- package/src/lib/runtime/ai-palette.generated.js.map +1 -1
- package/src/lib/runtime/seo-audit.d.ts +5 -2
- package/src/lib/runtime/seo-audit.js +16 -7
- package/src/lib/runtime/seo-audit.js.map +1 -1
- package/src/lib/runtime/site-inventory.js +10 -3
- package/src/lib/runtime/site-inventory.js.map +1 -1
- package/src/lib/testing/stand-in-dataset-index.js +31 -5
- package/src/lib/testing/stand-in-dataset-index.js.map +1 -1
- package/src/lib/tools/ai-seo-tool.d.ts +5 -1
- package/src/lib/tools/ai-seo-tool.js +6 -2
- package/src/lib/tools/ai-seo-tool.js.map +1 -1
- package/src/lib/tools/ai-workflow-tool.d.ts +15 -4
- package/src/lib/tools/ai-workflow-tool.js +70 -12
- package/src/lib/tools/ai-workflow-tool.js.map +1 -1
- package/src/lib/usage-axes.js +14 -1
- package/src/lib/usage-axes.js.map +1 -1
|
@@ -313,8 +313,59 @@ const EMBEDS_SCHEMA = {
|
|
|
313
313
|
})
|
|
314
314
|
})
|
|
315
315
|
});
|
|
316
|
-
|
|
317
|
-
|
|
316
|
+
const RECORD_SCHEMA = {
|
|
317
|
+
anyOf: [
|
|
318
|
+
{
|
|
319
|
+
type: 'object',
|
|
320
|
+
additionalProperties: false,
|
|
321
|
+
required: [
|
|
322
|
+
'dataset',
|
|
323
|
+
'base'
|
|
324
|
+
],
|
|
325
|
+
properties: {
|
|
326
|
+
dataset: string('The dataset: an inventory id or new:<name>.'),
|
|
327
|
+
base: string('The path its record pages live under, e.g. services for /services/roofing.')
|
|
328
|
+
}
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
type: 'null'
|
|
332
|
+
}
|
|
333
|
+
],
|
|
334
|
+
description: "When this page is one dataset's record template, serving a page per record at /<base>/<record address> with its copy bound as {{item.<field>}}: the dataset and the base. Otherwise null."
|
|
335
|
+
};
|
|
336
|
+
/** A plan tool whose screens each name the dataset they are the record template of, or null. */ function withRecords(tool) {
|
|
337
|
+
const properties = tool.inputSchema['properties'];
|
|
338
|
+
const screens = properties['screens'];
|
|
339
|
+
return _extends({}, tool, {
|
|
340
|
+
inputSchema: _extends({}, tool.inputSchema, {
|
|
341
|
+
properties: _extends({}, properties, {
|
|
342
|
+
screens: _extends({}, screens, {
|
|
343
|
+
items: _extends({}, screens.items, {
|
|
344
|
+
required: [
|
|
345
|
+
...screens.items['required'],
|
|
346
|
+
'record'
|
|
347
|
+
],
|
|
348
|
+
properties: _extends({}, screens.items['properties'], {
|
|
349
|
+
record: RECORD_SCHEMA
|
|
350
|
+
})
|
|
351
|
+
})
|
|
352
|
+
})
|
|
353
|
+
})
|
|
354
|
+
})
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* The plan tools with a record template on every screen (AGL-3475), offered
|
|
359
|
+
* only where the job may bind a dataset: a site with a dataset, or a job that
|
|
360
|
+
* may create one. Record pages are a Starter feature, and every other plan's
|
|
361
|
+
* request is the plain tool, byte for byte, so the field costs a Free taste
|
|
362
|
+
* nothing and a model planning a site with no dataset has none to bind.
|
|
363
|
+
*/ export const AI_BUILD_PLAN_RECORDS_TOOL = withRecords(AI_BUILD_PLAN_TOOL);
|
|
364
|
+
export const AI_BUILD_PLAN_EMBEDS_RECORDS_TOOL = withRecords(AI_BUILD_PLAN_EMBEDS_TOOL);
|
|
365
|
+
/** The plan tool a brief is answered with, on a job that may or may not bind a dataset. */ export function aiBuildPlanToolFor(brief, options = {}) {
|
|
366
|
+
const video = AI_PLAN_ASKS_FOR_VIDEO.test(brief);
|
|
367
|
+
if (options.records) return video ? AI_BUILD_PLAN_EMBEDS_RECORDS_TOOL : AI_BUILD_PLAN_RECORDS_TOOL;
|
|
368
|
+
return video ? AI_BUILD_PLAN_EMBEDS_TOOL : AI_BUILD_PLAN_TOOL;
|
|
318
369
|
}
|
|
319
370
|
function isRecord(value) {
|
|
320
371
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
@@ -364,6 +415,21 @@ let PlanShapeError = class PlanShapeError extends Error {
|
|
|
364
415
|
}
|
|
365
416
|
return value;
|
|
366
417
|
};
|
|
418
|
+
// Absent or null is a page that is one page. A base is held to the shape a
|
|
419
|
+
// record page's address takes, so the binding the member saves from it is
|
|
420
|
+
// one the save route admits.
|
|
421
|
+
const planRecord = (value, path)=>{
|
|
422
|
+
if (value === null || value === undefined) return null;
|
|
423
|
+
const entry = record(value, path);
|
|
424
|
+
const dataset = text(entry['dataset'], `${path}.dataset`);
|
|
425
|
+
if (!dataset) throw new PlanShapeError(`${path}.dataset is empty`);
|
|
426
|
+
const base = aiPlanRecordBase(text(entry['base'], `${path}.base`));
|
|
427
|
+
if (!base) throw new PlanShapeError(`${path}.base is not a path of lowercase words joined by hyphens`);
|
|
428
|
+
return {
|
|
429
|
+
dataset,
|
|
430
|
+
base
|
|
431
|
+
};
|
|
432
|
+
};
|
|
367
433
|
try {
|
|
368
434
|
var _root_embeds;
|
|
369
435
|
const root = record(input, 'plan');
|
|
@@ -423,7 +489,8 @@ let PlanShapeError = class PlanShapeError extends Error {
|
|
|
423
489
|
uses: list((_section_uses = section['uses']) != null ? _section_uses : [], `${path}.uses`, AI_BUILD_PLAN_LIMITS.uses).map((ref, refIndex)=>text(ref, `${path}.uses[${refIndex}]`)).filter(Boolean),
|
|
424
490
|
items
|
|
425
491
|
};
|
|
426
|
-
})
|
|
492
|
+
}),
|
|
493
|
+
record: planRecord(entry['record'], `screens[${index}].record`)
|
|
427
494
|
};
|
|
428
495
|
});
|
|
429
496
|
const embeds = list((_root_embeds = root['embeds']) != null ? _root_embeds : [], 'embeds', AI_BUILD_PLAN_LIMITS.embeds).map((raw, index)=>{
|
|
@@ -457,6 +524,16 @@ let PlanShapeError = class PlanShapeError extends Error {
|
|
|
457
524
|
throw error;
|
|
458
525
|
}
|
|
459
526
|
}
|
|
527
|
+
/**
|
|
528
|
+
* A record template's base as the data plugin's save route stores it
|
|
529
|
+
* (`normalizeRecordPageBase`, which this plugin may not import): one to four
|
|
530
|
+
* lowercase segments of letters, digits and hyphens, with no leading or
|
|
531
|
+
* trailing slash; null for a base that is no such path.
|
|
532
|
+
*/ export function aiPlanRecordBase(base) {
|
|
533
|
+
const segments = base.trim().toLowerCase().split('/').filter(Boolean);
|
|
534
|
+
if (!segments.length || segments.length > 4) return null;
|
|
535
|
+
return segments.every((segment)=>segment.length <= 60 && /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(segment)) ? segments.join('/') : null;
|
|
536
|
+
}
|
|
460
537
|
/** Whether a reference names something the plan creates. */ export function isAiPlanNewRef(ref) {
|
|
461
538
|
return typeof ref === 'string' && ref.startsWith(AI_PLAN_NEW_REF_PREFIX);
|
|
462
539
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-build-plan.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 { AiTool } from '../providers/contract'\n\n/**\n * The plan a generation job builds from (AGL-2935): what the job's plan step\n * (`jobs/ai-job-plan-step.ts`) returns, before a single node is generated.\n *\n * This module is the plan's MODEL — its shape, its tool schema and its\n * reader — and imports nothing at runtime, so the doctrine validators, the\n * plan step, the job document and the proposal can all read it without\n * importing each other.\n *\n * A plan is REFERENCES, never documents (AGL-2937). It names what the site\n * already has by inventory id, what the job will create by a short name, and\n * which screens it builds from which of those — so the doctrine can be held\n * against the plan while fixing it costs a sentence, not a regenerated page,\n * and the customer reads what the job will do before it spends a credit on\n * doing it.\n *\n * `new:<name>` is how an entry refers to something the plan itself creates;\n * any other reference is an id from the site inventory.\n */\n\nexport const AI_PLAN_NEW_REF_PREFIX = 'new:'\n\n/** Records a plan may reuse, by the inventory list they come from. */\nexport type AiBuildPlanReuseKind =\n | 'component'\n | 'layout'\n | 'template'\n | 'form'\n | 'dataset'\n | 'collection'\n | 'screen'\n | 'theme'\n\nexport const AI_BUILD_PLAN_REUSE_KINDS: readonly AiBuildPlanReuseKind[] = [\n 'component',\n 'layout',\n 'template',\n 'form',\n 'dataset',\n 'collection',\n 'screen',\n 'theme',\n]\n\n/** What a plan may create. A theme change is a draft through the theme door, never a literal. */\nexport type AiBuildPlanCreateKind =\n | 'component'\n | 'layout'\n | 'template'\n | 'form'\n | 'theme-change'\n | 'dataset'\n | 'email'\n\n/**\n * What each creation is called in a sentence, and where a member makes one.\n *\n * Total over the union deliberately: a refusal that cannot name a creation is\n * a refusal a member cannot act on, so widening `AiBuildPlanCreateKind`\n * without saying where the new thing is made does not compile.\n */\nexport const AI_BUILD_PLAN_CREATION_NOUNS: Record<\n AiBuildPlanCreateKind,\n { noun: string; where: string }\n> = {\n component: { noun: 'component', where: 'on the Components page' },\n form: { noun: 'form', where: 'on the Forms page' },\n layout: { noun: 'layout', where: 'on the Layouts page' },\n template: { noun: 'template', where: 'in the Templates library' },\n 'theme-change': { noun: 'theme change', where: 'in the Theme section' },\n dataset: { noun: 'dataset', where: 'on the Datasets page' },\n email: { noun: 'email design', where: 'in Emails → Templates' },\n}\n\nexport const AI_BUILD_PLAN_CREATE_KINDS: readonly AiBuildPlanCreateKind[] = [\n 'component',\n 'layout',\n 'template',\n 'form',\n 'theme-change',\n 'dataset',\n 'email',\n]\n\nexport interface AiBuildPlanReuse {\n kind: AiBuildPlanReuseKind\n /** The inventory id. */\n id: string\n /** What the plan uses it for, in a few words. */\n purpose: string\n}\n\nexport interface AiBuildPlanCreate {\n kind: AiBuildPlanCreateKind\n /** Unique within the plan; referenced as `new:<name>`. */\n name: string\n /** Why nothing the site has will do — shown in the proposal. */\n why: string\n /** The inventory id this starts from as a duplicate, or null. */\n duplicateOf: string | null\n /**\n * A component: its props as `name:type`, an icon as `name:icon`\n * (AGL-3054). A layout: the regions it has, from the closed vocabulary\n * `AI_LAYOUT_REGIONS` holds (AGL-3024) — a layout has no props, and a\n * region named here is what the built layout is held to. A template: the\n * binding tokens its page shows, from the subject's own closed catalog\n * (`AI_TEMPLATE_SUBJECT_DEFINITIONS[subject].tokens`, AGL-3143) — the\n * template's analogue of a layout's regions, and what the built template is\n * held to. A form or a dataset: its field names. A theme change: the\n * palette paths it adds or changes.\n */\n fields: string[]\n /**\n * The id the draft this creation becomes is written under, where a unit of\n * the job builds it (AGL-3079). Minted when the job keeps the plan, never\n * part of the model's answer.\n */\n id?: string\n}\n\nexport interface AiBuildPlanSection {\n /** A few words: \"hero\", \"services grid\", \"contact form\". */\n name: string\n /** What the section places: inventory ids or `new:<name>` references. */\n uses: string[]\n /** How many repeated items it shows (cards, rows, tiles); 0 when none. */\n items: number\n}\n\nexport interface AiBuildPlanScreen {\n title: string\n slug: string\n /** The layout it renders inside. */\n layout: string | null\n /** The template it applies, or null for a one-off page. */\n template: string | null\n /** The inventory screen it starts from as a duplicate, or null. */\n duplicateOf: string | null\n /** Whether the site navigation gains an entry for it. */\n nav: boolean\n seoTitle: string\n seoDescription: string\n sections: AiBuildPlanSection[]\n /**\n * The id the draft screen is written under, where a unit of the job builds\n * it (AGL-3079). Minted when the job keeps the plan, never part of the\n * model's answer.\n */\n id?: string\n}\n\n/** The third-party players a plan may embed: the hosts the Video embed element plays. */\nexport type AiBuildPlanEmbedHost = 'youtube' | 'vimeo'\n\nexport const AI_BUILD_PLAN_EMBED_HOSTS: readonly AiBuildPlanEmbedHost[] = ['youtube', 'vimeo']\n\n/** What each host is called in a sentence. */\nexport const AI_BUILD_PLAN_EMBED_HOST_NAMES: Record<AiBuildPlanEmbedHost, string> = {\n youtube: 'YouTube',\n vimeo: 'Vimeo',\n}\n\n/**\n * A third-party player the brief asks for (rule 16, AGL-3433). It loads its\n * host's code on the page that carries it, so it is planned, where the member\n * reads that cost before confirming, and a build admits only what the\n * confirmed plan lists.\n */\nexport interface AiBuildPlanEmbed {\n host: AiBuildPlanEmbedHost\n /** Where it plays: a screen of the plan by its slug, or `new:<name>` for a component the plan creates. */\n where: string\n /** The brief's own words asking for it, which the plan step finds in the brief. */\n asked: string\n /** The video's link as the brief gives it, or null for the site owner to paste. */\n url: string | null\n}\n\nexport interface AiBuildPlan {\n reuse: AiBuildPlanReuse[]\n create: AiBuildPlanCreate[]\n screens: AiBuildPlanScreen[]\n /**\n * The third-party players the brief asks for. Absent is none: a plan kept\n * before the list existed, and every plan whose brief asks for no player.\n */\n embeds?: AiBuildPlanEmbed[]\n}\n\n/**\n * How much one plan may hold. A job builds what one proposal can describe\n * and one person can read before confirming; a brief asking for more is\n * several jobs.\n */\nexport const AI_BUILD_PLAN_LIMITS = {\n reuse: 40,\n create: 20,\n screens: 12,\n sections: 16,\n uses: 8,\n fields: 30,\n embeds: 4,\n text: 200,\n seoDescription: 320,\n items: 500,\n} as const\n\nconst string = (description: string) => ({ type: 'string', description })\nconst nullableString = (description: string) => ({\n anyOf: [{ type: 'string' }, { type: 'null' }],\n description,\n})\nconst strings = (description: string) => ({\n type: 'array',\n items: { type: 'string' },\n description,\n})\n\n/**\n * The ceiling `parseAiBuildPlan` cuts a field's text at, in words (AGL-3022).\n * The reader repairs an overlong value by cutting it, and a model never told\n * where the cut falls writes a rationale that stops mid-sentence there.\n */\nconst atMost = (limit: number = AI_BUILD_PLAN_LIMITS.text) => `at most ${limit} characters`\n\n/**\n * The plan as a strict tool's input schema. Only the keywords strict\n * structured output accepts: every object closes its properties and lists\n * every one as required, an optional value is a `null` branch, and no\n * length or count bound appears as a keyword — those are enforced by\n * `parseAiBuildPlan`, which every answer passes through anyway.\n *\n * A text ceiling is stated in words instead, on each field the model writes.\n * A reference is copied from the inventory or from a creation's name, so its\n * length is not the model's to choose. A search title and description are\n * held by rule 10 to ceilings far inside the reader's, which the doctrine\n * states, so naming the reader's here would contradict the rule.\n */\nexport const AI_BUILD_PLAN_TOOL: AiTool = {\n name: 'submit_build_plan',\n description:\n 'Submit the build plan: what the site already has that this job reuses, what it creates and why, and the pages it builds from them.',\n strict: true,\n inputSchema: {\n type: 'object',\n additionalProperties: false,\n required: ['reuse', 'create', 'screens'],\n properties: {\n reuse: {\n type: 'array',\n description: 'Records the site already has that this plan uses, by inventory id.',\n items: {\n type: 'object',\n additionalProperties: false,\n required: ['kind', 'id', 'purpose'],\n properties: {\n kind: { type: 'string', enum: [...AI_BUILD_PLAN_REUSE_KINDS] },\n id: string('The id from the site inventory.'),\n purpose: string(`What the plan uses it for, in a few words, ${atMost()}.`),\n },\n },\n },\n create: {\n type: 'array',\n description:\n 'What the plan creates. Creating is the exception: each entry says why nothing listed will do.',\n items: {\n type: 'object',\n additionalProperties: false,\n required: ['kind', 'name', 'why', 'duplicateOf', 'fields'],\n properties: {\n kind: { type: 'string', enum: [...AI_BUILD_PLAN_CREATE_KINDS] },\n name: string(\n `Unique within the plan, ${atMost()}; other entries refer to it as new:<name>.`,\n ),\n why: string(`Why nothing the site already has will do, in one sentence of ${atMost()}.`),\n duplicateOf: nullableString(\n 'The inventory id this starts from as a duplicate, or null.',\n ),\n fields: strings(\n `A component: its props as name:type, an icon as name:icon. A layout: the regions it has, each one of header, nav, sidebar, main, footer — list every region the finished layout has, not only the ones it gains. A form or dataset: its field names. A theme change: the palette paths it adds or changes. Each ${atMost()}.`,\n ),\n },\n },\n },\n screens: {\n type: 'array',\n description: 'The pages the job builds, each a new draft.',\n items: {\n type: 'object',\n additionalProperties: false,\n required: [\n 'title',\n 'slug',\n 'layout',\n 'template',\n 'duplicateOf',\n 'nav',\n 'seoTitle',\n 'seoDescription',\n 'sections',\n ],\n properties: {\n title: string(`The page name, ${atMost()}.`),\n slug: string(\n `The path, ${atMost()}: lowercase words joined by hyphens, e.g. /services/roof-repair.`,\n ),\n layout: nullableString(\n 'The layout it renders inside: an inventory id or new:<name>.',\n ),\n template: nullableString(\n 'The template it applies (inventory id or new:<name>), or null for a one-off page.',\n ),\n duplicateOf: nullableString(\n 'The inventory page it starts from as a duplicate, or null.',\n ),\n nav: {\n type: 'boolean',\n description: 'Whether the site navigation gains an entry for it.',\n },\n seoTitle: string('The search-result title.'),\n seoDescription: string('The search-result description.'),\n sections: {\n type: 'array',\n description: 'The sections, top to bottom.',\n items: {\n type: 'object',\n additionalProperties: false,\n required: ['name', 'uses', 'items'],\n properties: {\n name: string(`A few words, ${atMost()}: hero, services grid, contact form.`),\n uses: strings('What the section places: inventory ids or new:<name>.'),\n items: {\n type: 'integer',\n description: 'How many repeated items it shows; 0 when none.',\n },\n },\n },\n },\n },\n },\n },\n },\n },\n}\n\n/** Words that ask for a video, or a link to one. */\nexport const AI_PLAN_ASKS_FOR_VIDEO = /\\b(?:videos?|films?|clips?|youtube|vimeo|player)\\b|youtu\\.be/i\n\nconst EMBEDS_SCHEMA = {\n type: 'array',\n description:\n 'A YouTube or Vimeo player the brief asks for, on a screen or a created component. Empty unless the brief asks; a library film is a Video.',\n items: {\n type: 'object',\n additionalProperties: false,\n required: ['host', 'where', 'asked', 'url'],\n properties: {\n host: { type: 'string', enum: [...AI_BUILD_PLAN_EMBED_HOSTS] },\n where: string('A screen slug, or new:<name> of a component.'),\n asked: string('The words of the brief that ask for it, copied exactly.'),\n url: nullableString('The video link the brief gives, or null.'),\n },\n },\n}\n\n/**\n * The plan tool with its list of third-party players, offered only to a brief\n * that asks for a video (AGL-3433). Every other plan's request is the plain\n * tool, byte for byte, so the list costs nothing on the plans that cannot use\n * it, and a model answering a brief that names no video has nowhere to plan a\n * player it was never asked for.\n */\nexport const AI_BUILD_PLAN_EMBEDS_TOOL: AiTool = {\n ...AI_BUILD_PLAN_TOOL,\n inputSchema: {\n ...AI_BUILD_PLAN_TOOL.inputSchema,\n required: [...(AI_BUILD_PLAN_TOOL.inputSchema['required'] as string[]), 'embeds'],\n properties: {\n ...(AI_BUILD_PLAN_TOOL.inputSchema['properties'] as Record<string, unknown>),\n embeds: EMBEDS_SCHEMA,\n },\n },\n}\n\n/** The plan tool a brief is answered with. */\nexport function aiBuildPlanToolFor(brief: string): AiTool {\n return AI_PLAN_ASKS_FOR_VIDEO.test(brief) ? AI_BUILD_PLAN_EMBEDS_TOOL : AI_BUILD_PLAN_TOOL\n}\n\nexport type AiBuildPlanParse =\n | { ok: true; plan: AiBuildPlan; repairs: string[] }\n | { ok: false; error: string }\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\nclass PlanShapeError extends Error {}\n\n/**\n * Read a plan out of a tool call, or name what makes it unreadable.\n *\n * Structure is refused — a missing list, an unknown kind, more screens than\n * a job holds, two creations under one name — because a plan that reads\n * differently from what the model meant is worse than a re-ask. Length is\n * repaired: copy past a ceiling is cut and the cut is listed.\n */\nexport function parseAiBuildPlan(input: unknown): AiBuildPlanParse {\n const repairs: string[] = []\n const text = (\n value: unknown,\n path: string,\n limit: number = AI_BUILD_PLAN_LIMITS.text,\n ): string => {\n if (typeof value !== 'string') throw new PlanShapeError(`${path} is not text`)\n const trimmed = value.trim()\n if (trimmed.length > limit) {\n repairs.push(`${path} was over ${limit} characters; truncated`)\n // On the last word break inside the ceiling, not through a word: a\n // rationale is read by the member confirming the plan, and half a word\n // reads as a broken answer rather than a long one (AGL-3022). A run of\n // `limit` characters with no break in it still has to be cut where the\n // ceiling falls.\n const cut = trimmed.slice(0, limit)\n const brk = cut.search(/\\s\\S*$/)\n return (brk > 0 ? cut.slice(0, brk) : cut).trimEnd()\n }\n return trimmed\n }\n const nullable = (value: unknown, path: string): string | null =>\n value === null || value === undefined || value === '' ? null : text(value, path) || null\n const list = (value: unknown, path: string, limit: number): unknown[] => {\n if (!Array.isArray(value)) throw new PlanShapeError(`${path} is not a list`)\n if (value.length > limit) {\n throw new PlanShapeError(`${path} lists ${value.length}; one plan holds at most ${limit}`)\n }\n return value\n }\n const record = (value: unknown, path: string): Record<string, unknown> => {\n if (!isRecord(value)) throw new PlanShapeError(`${path} is not an object`)\n return value\n }\n const oneOf = <T extends string>(value: unknown, allowed: readonly T[], path: string): T => {\n if (typeof value !== 'string' || !(allowed as readonly string[]).includes(value)) {\n throw new PlanShapeError(`${path} is not one of ${allowed.join(', ')}`)\n }\n return value as T\n }\n\n try {\n const root = record(input, 'plan')\n const reuse = list(root['reuse'], 'reuse', AI_BUILD_PLAN_LIMITS.reuse).map((raw, index) => {\n const entry = record(raw, `reuse[${index}]`)\n const id = text(entry['id'], `reuse[${index}].id`)\n if (!id) throw new PlanShapeError(`reuse[${index}].id is empty`)\n return {\n kind: oneOf(entry['kind'], AI_BUILD_PLAN_REUSE_KINDS, `reuse[${index}].kind`),\n id,\n purpose: text(entry['purpose'], `reuse[${index}].purpose`),\n }\n })\n const names = new Set<string>()\n const create = list(root['create'], 'create', AI_BUILD_PLAN_LIMITS.create).map(\n (raw, index) => {\n const entry = record(raw, `create[${index}]`)\n const name = text(entry['name'], `create[${index}].name`)\n if (!name) throw new PlanShapeError(`create[${index}].name is empty`)\n const key = name.toLowerCase()\n if (names.has(key)) {\n throw new PlanShapeError(`create[${index}].name \"${name}\" is used twice`)\n }\n names.add(key)\n return {\n kind: oneOf(entry['kind'], AI_BUILD_PLAN_CREATE_KINDS, `create[${index}].kind`),\n name,\n why: text(entry['why'], `create[${index}].why`),\n duplicateOf: nullable(entry['duplicateOf'], `create[${index}].duplicateOf`),\n fields: list(entry['fields'] ?? [], `create[${index}].fields`, AI_BUILD_PLAN_LIMITS.fields)\n .map((field, fieldIndex) => text(field, `create[${index}].fields[${fieldIndex}]`))\n .filter(Boolean),\n }\n },\n )\n const screens = list(root['screens'], 'screens', AI_BUILD_PLAN_LIMITS.screens).map(\n (raw, index) => {\n const entry = record(raw, `screens[${index}]`)\n if (typeof entry['nav'] !== 'boolean') {\n throw new PlanShapeError(`screens[${index}].nav is not true or false`)\n }\n return {\n title: text(entry['title'], `screens[${index}].title`),\n slug: text(entry['slug'], `screens[${index}].slug`),\n layout: nullable(entry['layout'], `screens[${index}].layout`),\n template: nullable(entry['template'], `screens[${index}].template`),\n duplicateOf: nullable(entry['duplicateOf'], `screens[${index}].duplicateOf`),\n nav: entry['nav'],\n seoTitle: text(entry['seoTitle'], `screens[${index}].seoTitle`),\n seoDescription: text(\n entry['seoDescription'],\n `screens[${index}].seoDescription`,\n AI_BUILD_PLAN_LIMITS.seoDescription,\n ),\n sections: list(\n entry['sections'],\n `screens[${index}].sections`,\n AI_BUILD_PLAN_LIMITS.sections,\n ).map((rawSection, sectionIndex) => {\n const path = `screens[${index}].sections[${sectionIndex}]`\n const section = record(rawSection, path)\n const items = Number(section['items'] ?? 0)\n if (!Number.isInteger(items) || items < 0 || items > AI_BUILD_PLAN_LIMITS.items) {\n throw new PlanShapeError(`${path}.items is not a count`)\n }\n return {\n name: text(section['name'], `${path}.name`),\n uses: list(section['uses'] ?? [], `${path}.uses`, AI_BUILD_PLAN_LIMITS.uses)\n .map((ref, refIndex) => text(ref, `${path}.uses[${refIndex}]`))\n .filter(Boolean),\n items,\n }\n }),\n }\n },\n )\n const embeds = list(root['embeds'] ?? [], 'embeds', AI_BUILD_PLAN_LIMITS.embeds).map(\n (raw, index) => {\n const entry = record(raw, `embeds[${index}]`)\n const where = text(entry['where'], `embeds[${index}].where`)\n if (!where) throw new PlanShapeError(`embeds[${index}].where is empty`)\n return {\n host: oneOf(entry['host'], AI_BUILD_PLAN_EMBED_HOSTS, `embeds[${index}].host`),\n where,\n asked: text(entry['asked'], `embeds[${index}].asked`),\n url: nullable(entry['url'], `embeds[${index}].url`),\n }\n },\n )\n // A plan whose brief asks for no player carries no list, as it did before the list existed.\n return {\n ok: true,\n plan: { reuse, create, screens, ...(embeds.length ? { embeds } : {}) },\n repairs,\n }\n } catch (error) {\n if (error instanceof PlanShapeError) return { ok: false, error: error.message }\n throw error\n }\n}\n\n/** Whether a reference names something the plan creates. */\nexport function isAiPlanNewRef(ref: string | null | undefined): ref is string {\n return typeof ref === 'string' && ref.startsWith(AI_PLAN_NEW_REF_PREFIX)\n}\n\n/** The creation a `new:<name>` reference names, case-insensitively. */\nexport function aiPlanCreateFor(\n plan: AiBuildPlan,\n ref: string,\n): AiBuildPlanCreate | undefined {\n if (!isAiPlanNewRef(ref)) return undefined\n const name = ref.slice(AI_PLAN_NEW_REF_PREFIX.length).trim().toLowerCase()\n return plan.create.find((entry) => entry.name.toLowerCase() === name)\n}\n\n/** A slug as two spellings of one path compare: lowercase, one leading slash, no trailing one. */\nexport function aiPlanSlugKey(slug: string): string {\n const path = slug.trim().toLowerCase().replace(/^\\/*/, '/').replace(/\\/+$/, '')\n return path || '/'\n}\n\n/**\n * The players the plan lists for one output (AGL-3433): a screen by its slug,\n * or a creation by its name. A build admits these and no other.\n */\nexport function aiPlanEmbedsFor(\n plan: Pick<AiBuildPlan, 'embeds'> | null | undefined,\n target: { slug: string } | { create: string },\n): AiBuildPlanEmbed[] {\n const key =\n 'slug' in target\n ? aiPlanSlugKey(target.slug)\n : `${AI_PLAN_NEW_REF_PREFIX}${target.create.trim().toLowerCase()}`\n return (plan?.embeds ?? []).filter((embed) =>\n 'slug' in target\n ? !isAiPlanNewRef(embed.where) && aiPlanSlugKey(embed.where) === key\n : embed.where.trim().toLowerCase() === key,\n )\n}\n\n/** The host a video link plays from, or null for a link neither host serves. */\nexport function aiEmbedHostOf(url: string): AiBuildPlanEmbedHost | null {\n try {\n const parsed = new URL(url.trim())\n if (parsed.protocol !== 'https:') return null\n const host = parsed.hostname.replace(/^(?:www|m)\\./, '')\n if (host === 'youtu.be' || host === 'youtube.com' || host === 'youtube-nocookie.com') {\n return 'youtube'\n }\n if (host === 'vimeo.com' || host === 'player.vimeo.com') return 'vimeo'\n } catch {\n /* not a link */\n }\n return null\n}\n\n/**\n * One video however its link is spelled — `youtu.be/<id>`, a watch link, an\n * embed or a Shorts link — as `<host>:<id>`, the way the Video embed element\n * reads a link; null for a link it cannot play.\n */\nexport function aiEmbedVideoKey(url: string): string | null {\n const host = aiEmbedHostOf(url)\n if (!host) return null\n const parsed = new URL(url.trim())\n const parts = parsed.pathname.split('/').filter(Boolean)\n const id =\n host === 'vimeo'\n ? parts.find((part) => /^\\d{6,}$/.test(part))\n : parsed.hostname.endsWith('youtu.be')\n ? parts[0]\n : (parsed.searchParams.get('v') ??\n (parts[0] === 'embed' || parts[0] === 'shorts' ? parts[1] : undefined))\n return id && /^[\\w-]{6,20}$/.test(id) ? `${host}:${id}` : null\n}\n\n/**\n * A `new:<name>` reference that names no entry of the plan's create list: a\n * screen's own layout or template, or what one of its sections places, with\n * that section's index.\n */\nexport type AiPlanUndeclaredRef = {\n /** The name the reference gives the creation, as written after `new:`. */\n name: string\n /** Where the plan writes it: `screens[0].template`, `screens[0].sections[4].uses[0]`. */\n path: string\n /** The screen that writes it, by index. */\n screenIndex: number\n} & ({ field: 'layout' | 'template'; sectionIndex: null } | { field: 'uses'; sectionIndex: number })\n\n/**\n * Every `new:<name>` reference the plan's create list does not carry, in plan\n * order: each screen's layout, then its template, then what its sections\n * place. A plan that places a creation it never declares has nothing to build\n * it from, so the plan rules refuse each (AGL-3040), and the page and\n * scaffold doors still name one in a plan confirmed before they did.\n */\nexport function aiPlanUndeclaredRefs(plan: AiBuildPlan): AiPlanUndeclaredRef[] {\n const found: AiPlanUndeclaredRef[] = []\n const undeclared = (ref: string | null): ref is string =>\n isAiPlanNewRef(ref) && !aiPlanCreateFor(plan, ref)\n const nameOf = (ref: string) => ref.slice(AI_PLAN_NEW_REF_PREFIX.length).trim()\n plan.screens.forEach((screen, screenIndex) => {\n for (const field of ['layout', 'template'] as const) {\n const ref = screen[field]\n if (!undeclared(ref)) continue\n found.push({\n name: nameOf(ref),\n path: `screens[${screenIndex}].${field}`,\n screenIndex,\n field,\n sectionIndex: null,\n })\n }\n screen.sections.forEach((section, sectionIndex) => {\n section.uses.forEach((ref, useIndex) => {\n if (!undeclared(ref)) return\n found.push({\n name: nameOf(ref),\n path: `screens[${screenIndex}].sections[${sectionIndex}].uses[${useIndex}]`,\n screenIndex,\n field: 'uses',\n sectionIndex,\n })\n })\n })\n })\n return found\n}\n"],"names":["AI_PLAN_NEW_REF_PREFIX","AI_BUILD_PLAN_REUSE_KINDS","AI_BUILD_PLAN_CREATION_NOUNS","component","noun","where","form","layout","template","dataset","email","AI_BUILD_PLAN_CREATE_KINDS","AI_BUILD_PLAN_EMBED_HOSTS","AI_BUILD_PLAN_EMBED_HOST_NAMES","youtube","vimeo","AI_BUILD_PLAN_LIMITS","reuse","create","screens","sections","uses","fields","embeds","text","seoDescription","items","string","description","type","nullableString","anyOf","strings","atMost","limit","AI_BUILD_PLAN_TOOL","name","strict","inputSchema","additionalProperties","required","properties","kind","enum","id","purpose","why","duplicateOf","title","slug","nav","seoTitle","AI_PLAN_ASKS_FOR_VIDEO","EMBEDS_SCHEMA","host","asked","url","AI_BUILD_PLAN_EMBEDS_TOOL","aiBuildPlanToolFor","brief","test","isRecord","value","Array","isArray","PlanShapeError","Error","parseAiBuildPlan","input","repairs","path","trimmed","trim","length","push","cut","slice","brk","search","trimEnd","nullable","undefined","list","record","oneOf","allowed","includes","join","root","map","raw","index","entry","names","Set","key","toLowerCase","has","add","field","fieldIndex","filter","Boolean","rawSection","sectionIndex","section","Number","isInteger","ref","refIndex","ok","plan","error","message","isAiPlanNewRef","startsWith","aiPlanCreateFor","find","aiPlanSlugKey","replace","aiPlanEmbedsFor","target","embed","aiEmbedHostOf","parsed","URL","protocol","hostname","aiEmbedVideoKey","parts","pathname","split","part","endsWith","searchParams","get","aiPlanUndeclaredRefs","found","undeclared","nameOf","forEach","screen","screenIndex","useIndex"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC;AAID;;;;;;;;;;;;;;;;;;CAkBC,GAED,OAAO,MAAMA,yBAAyB,OAAM;AAa5C,OAAO,MAAMC,4BAA6D;IACxE;IACA;IACA;IACA;IACA;IACA;IACA;IACA;CACD,CAAA;AAYD;;;;;;CAMC,GACD,OAAO,MAAMC,+BAGT;IACFC,WAAW;QAAEC,MAAM;QAAaC,OAAO;IAAyB;IAChEC,MAAM;QAAEF,MAAM;QAAQC,OAAO;IAAoB;IACjDE,QAAQ;QAAEH,MAAM;QAAUC,OAAO;IAAsB;IACvDG,UAAU;QAAEJ,MAAM;QAAYC,OAAO;IAA2B;IAChE,gBAAgB;QAAED,MAAM;QAAgBC,OAAO;IAAuB;IACtEI,SAAS;QAAEL,MAAM;QAAWC,OAAO;IAAuB;IAC1DK,OAAO;QAAEN,MAAM;QAAgBC,OAAO;IAAwB;AAChE,EAAC;AAED,OAAO,MAAMM,6BAA+D;IAC1E;IACA;IACA;IACA;IACA;IACA;IACA;CACD,CAAA;AAwED,OAAO,MAAMC,4BAA6D;IAAC;IAAW;CAAQ,CAAA;AAE9F,4CAA4C,GAC5C,OAAO,MAAMC,iCAAuE;IAClFC,SAAS;IACTC,OAAO;AACT,EAAC;AA6BD;;;;CAIC,GACD,OAAO,MAAMC,uBAAuB;IAClCC,OAAO;IACPC,QAAQ;IACRC,SAAS;IACTC,UAAU;IACVC,MAAM;IACNC,QAAQ;IACRC,QAAQ;IACRC,MAAM;IACNC,gBAAgB;IAChBC,OAAO;AACT,EAAU;AAEV,MAAMC,SAAS,CAACC,cAAyB,CAAA;QAAEC,MAAM;QAAUD;IAAY,CAAA;AACvE,MAAME,iBAAiB,CAACF,cAAyB,CAAA;QAC/CG,OAAO;YAAC;gBAAEF,MAAM;YAAS;YAAG;gBAAEA,MAAM;YAAO;SAAE;QAC7CD;IACF,CAAA;AACA,MAAMI,UAAU,CAACJ,cAAyB,CAAA;QACxCC,MAAM;QACNH,OAAO;YAAEG,MAAM;QAAS;QACxBD;IACF,CAAA;AAEA;;;;CAIC,GACD,MAAMK,SAAS,CAACC,QAAgBlB,qBAAqBQ,IAAI,GAAK,CAAC,QAAQ,EAAEU,MAAM,WAAW,CAAC;AAE3F;;;;;;;;;;;;CAYC,GACD,OAAO,MAAMC,qBAA6B;IACxCC,MAAM;IACNR,aACE;IACFS,QAAQ;IACRC,aAAa;QACXT,MAAM;QACNU,sBAAsB;QACtBC,UAAU;YAAC;YAAS;YAAU;SAAU;QACxCC,YAAY;YACVxB,OAAO;gBACLY,MAAM;gBACND,aAAa;gBACbF,OAAO;oBACLG,MAAM;oBACNU,sBAAsB;oBACtBC,UAAU;wBAAC;wBAAQ;wBAAM;qBAAU;oBACnCC,YAAY;wBACVC,MAAM;4BAAEb,MAAM;4BAAUc,MAAM;mCAAI1C;6BAA0B;wBAAC;wBAC7D2C,IAAIjB,OAAO;wBACXkB,SAASlB,OAAO,CAAC,2CAA2C,EAAEM,SAAS,CAAC,CAAC;oBAC3E;gBACF;YACF;YACAf,QAAQ;gBACNW,MAAM;gBACND,aACE;gBACFF,OAAO;oBACLG,MAAM;oBACNU,sBAAsB;oBACtBC,UAAU;wBAAC;wBAAQ;wBAAQ;wBAAO;wBAAe;qBAAS;oBAC1DC,YAAY;wBACVC,MAAM;4BAAEb,MAAM;4BAAUc,MAAM;mCAAIhC;6BAA2B;wBAAC;wBAC9DyB,MAAMT,OACJ,CAAC,wBAAwB,EAAEM,SAAS,0CAA0C,CAAC;wBAEjFa,KAAKnB,OAAO,CAAC,6DAA6D,EAAEM,SAAS,CAAC,CAAC;wBACvFc,aAAajB,eACX;wBAEFR,QAAQU,QACN,CAAC,gTAAgT,EAAEC,SAAS,CAAC,CAAC;oBAElU;gBACF;YACF;YACAd,SAAS;gBACPU,MAAM;gBACND,aAAa;gBACbF,OAAO;oBACLG,MAAM;oBACNU,sBAAsB;oBACtBC,UAAU;wBACR;wBACA;wBACA;wBACA;wBACA;wBACA;wBACA;wBACA;wBACA;qBACD;oBACDC,YAAY;wBACVO,OAAOrB,OAAO,CAAC,eAAe,EAAEM,SAAS,CAAC,CAAC;wBAC3CgB,MAAMtB,OACJ,CAAC,UAAU,EAAEM,SAAS,gEAAgE,CAAC;wBAEzF1B,QAAQuB,eACN;wBAEFtB,UAAUsB,eACR;wBAEFiB,aAAajB,eACX;wBAEFoB,KAAK;4BACHrB,MAAM;4BACND,aAAa;wBACf;wBACAuB,UAAUxB,OAAO;wBACjBF,gBAAgBE,OAAO;wBACvBP,UAAU;4BACRS,MAAM;4BACND,aAAa;4BACbF,OAAO;gCACLG,MAAM;gCACNU,sBAAsB;gCACtBC,UAAU;oCAAC;oCAAQ;oCAAQ;iCAAQ;gCACnCC,YAAY;oCACVL,MAAMT,OAAO,CAAC,aAAa,EAAEM,SAAS,oCAAoC,CAAC;oCAC3EZ,MAAMW,QAAQ;oCACdN,OAAO;wCACLG,MAAM;wCACND,aAAa;oCACf;gCACF;4BACF;wBACF;oBACF;gBACF;YACF;QACF;IACF;AACF,EAAC;AAED,kDAAkD,GAClD,OAAO,MAAMwB,yBAAyB,gEAA+D;AAErG,MAAMC,gBAAgB;IACpBxB,MAAM;IACND,aACE;IACFF,OAAO;QACLG,MAAM;QACNU,sBAAsB;QACtBC,UAAU;YAAC;YAAQ;YAAS;YAAS;SAAM;QAC3CC,YAAY;YACVa,MAAM;gBAAEzB,MAAM;gBAAUc,MAAM;uBAAI/B;iBAA0B;YAAC;YAC7DP,OAAOsB,OAAO;YACd4B,OAAO5B,OAAO;YACd6B,KAAK1B,eAAe;QACtB;IACF;AACF;AAEA;;;;;;CAMC,GACD,OAAO,MAAM2B,4BAAoC,aAC5CtB;IACHG,aAAa,aACRH,mBAAmBG,WAAW;QACjCE,UAAU;eAAKL,mBAAmBG,WAAW,CAAC,WAAW;YAAe;SAAS;QACjFG,YAAY,aACNN,mBAAmBG,WAAW,CAAC,aAAa;YAChDf,QAAQ8B;;;GAGb;AAED,4CAA4C,GAC5C,OAAO,SAASK,mBAAmBC,KAAa;IAC9C,OAAOP,uBAAuBQ,IAAI,CAACD,SAASF,4BAA4BtB;AAC1E;AAMA,SAAS0B,SAASC,KAAc;IAC9B,OAAO,OAAOA,UAAU,YAAYA,UAAU,QAAQ,CAACC,MAAMC,OAAO,CAACF;AACvE;AAEA,IAAA,AAAMG,iBAAN,MAAMA,uBAAuBC;AAAO;AAEpC;;;;;;;CAOC,GACD,OAAO,SAASC,iBAAiBC,KAAc;IAC7C,MAAMC,UAAoB,EAAE;IAC5B,MAAM7C,OAAO,CACXsC,OACAQ,MACApC,QAAgBlB,qBAAqBQ,IAAI;QAEzC,IAAI,OAAOsC,UAAU,UAAU,MAAM,IAAIG,eAAe,GAAGK,KAAK,YAAY,CAAC;QAC7E,MAAMC,UAAUT,MAAMU,IAAI;QAC1B,IAAID,QAAQE,MAAM,GAAGvC,OAAO;YAC1BmC,QAAQK,IAAI,CAAC,GAAGJ,KAAK,UAAU,EAAEpC,MAAM,sBAAsB,CAAC;YAC9D,mEAAmE;YACnE,uEAAuE;YACvE,uEAAuE;YACvE,uEAAuE;YACvE,iBAAiB;YACjB,MAAMyC,MAAMJ,QAAQK,KAAK,CAAC,GAAG1C;YAC7B,MAAM2C,MAAMF,IAAIG,MAAM,CAAC;YACvB,OAAO,AAACD,CAAAA,MAAM,IAAIF,IAAIC,KAAK,CAAC,GAAGC,OAAOF,GAAE,EAAGI,OAAO;QACpD;QACA,OAAOR;IACT;IACA,MAAMS,WAAW,CAAClB,OAAgBQ,OAChCR,UAAU,QAAQA,UAAUmB,aAAanB,UAAU,KAAK,OAAOtC,KAAKsC,OAAOQ,SAAS;IACtF,MAAMY,OAAO,CAACpB,OAAgBQ,MAAcpC;QAC1C,IAAI,CAAC6B,MAAMC,OAAO,CAACF,QAAQ,MAAM,IAAIG,eAAe,GAAGK,KAAK,cAAc,CAAC;QAC3E,IAAIR,MAAMW,MAAM,GAAGvC,OAAO;YACxB,MAAM,IAAI+B,eAAe,GAAGK,KAAK,OAAO,EAAER,MAAMW,MAAM,CAAC,yBAAyB,EAAEvC,OAAO;QAC3F;QACA,OAAO4B;IACT;IACA,MAAMqB,SAAS,CAACrB,OAAgBQ;QAC9B,IAAI,CAACT,SAASC,QAAQ,MAAM,IAAIG,eAAe,GAAGK,KAAK,iBAAiB,CAAC;QACzE,OAAOR;IACT;IACA,MAAMsB,QAAQ,CAAmBtB,OAAgBuB,SAAuBf;QACtE,IAAI,OAAOR,UAAU,YAAY,CAAC,AAACuB,QAA8BC,QAAQ,CAACxB,QAAQ;YAChF,MAAM,IAAIG,eAAe,GAAGK,KAAK,eAAe,EAAEe,QAAQE,IAAI,CAAC,OAAO;QACxE;QACA,OAAOzB;IACT;IAEA,IAAI;YA2EkB0B;QA1EpB,MAAMA,OAAOL,OAAOf,OAAO;QAC3B,MAAMnD,QAAQiE,KAAKM,IAAI,CAAC,QAAQ,EAAE,SAASxE,qBAAqBC,KAAK,EAAEwE,GAAG,CAAC,CAACC,KAAKC;YAC/E,MAAMC,QAAQT,OAAOO,KAAK,CAAC,MAAM,EAAEC,MAAM,CAAC,CAAC;YAC3C,MAAM/C,KAAKpB,KAAKoE,KAAK,CAAC,KAAK,EAAE,CAAC,MAAM,EAAED,MAAM,IAAI,CAAC;YACjD,IAAI,CAAC/C,IAAI,MAAM,IAAIqB,eAAe,CAAC,MAAM,EAAE0B,MAAM,aAAa,CAAC;YAC/D,OAAO;gBACLjD,MAAM0C,MAAMQ,KAAK,CAAC,OAAO,EAAE3F,2BAA2B,CAAC,MAAM,EAAE0F,MAAM,MAAM,CAAC;gBAC5E/C;gBACAC,SAASrB,KAAKoE,KAAK,CAAC,UAAU,EAAE,CAAC,MAAM,EAAED,MAAM,SAAS,CAAC;YAC3D;QACF;QACA,MAAME,QAAQ,IAAIC;QAClB,MAAM5E,SAASgE,KAAKM,IAAI,CAAC,SAAS,EAAE,UAAUxE,qBAAqBE,MAAM,EAAEuE,GAAG,CAC5E,CAACC,KAAKC;gBAcWC;YAbf,MAAMA,QAAQT,OAAOO,KAAK,CAAC,OAAO,EAAEC,MAAM,CAAC,CAAC;YAC5C,MAAMvD,OAAOZ,KAAKoE,KAAK,CAAC,OAAO,EAAE,CAAC,OAAO,EAAED,MAAM,MAAM,CAAC;YACxD,IAAI,CAACvD,MAAM,MAAM,IAAI6B,eAAe,CAAC,OAAO,EAAE0B,MAAM,eAAe,CAAC;YACpE,MAAMI,MAAM3D,KAAK4D,WAAW;YAC5B,IAAIH,MAAMI,GAAG,CAACF,MAAM;gBAClB,MAAM,IAAI9B,eAAe,CAAC,OAAO,EAAE0B,MAAM,QAAQ,EAAEvD,KAAK,eAAe,CAAC;YAC1E;YACAyD,MAAMK,GAAG,CAACH;YACV,OAAO;gBACLrD,MAAM0C,MAAMQ,KAAK,CAAC,OAAO,EAAEjF,4BAA4B,CAAC,OAAO,EAAEgF,MAAM,MAAM,CAAC;gBAC9EvD;gBACAU,KAAKtB,KAAKoE,KAAK,CAAC,MAAM,EAAE,CAAC,OAAO,EAAED,MAAM,KAAK,CAAC;gBAC9C5C,aAAaiC,SAASY,KAAK,CAAC,cAAc,EAAE,CAAC,OAAO,EAAED,MAAM,aAAa,CAAC;gBAC1ErE,QAAQ4D,MAAKU,gBAAAA,KAAK,CAAC,SAAS,YAAfA,gBAAmB,EAAE,EAAE,CAAC,OAAO,EAAED,MAAM,QAAQ,CAAC,EAAE3E,qBAAqBM,MAAM,EACvFmE,GAAG,CAAC,CAACU,OAAOC,aAAe5E,KAAK2E,OAAO,CAAC,OAAO,EAAER,MAAM,SAAS,EAAES,WAAW,CAAC,CAAC,GAC/EC,MAAM,CAACC;YACZ;QACF;QAEF,MAAMnF,UAAU+D,KAAKM,IAAI,CAAC,UAAU,EAAE,WAAWxE,qBAAqBG,OAAO,EAAEsE,GAAG,CAChF,CAACC,KAAKC;YACJ,MAAMC,QAAQT,OAAOO,KAAK,CAAC,QAAQ,EAAEC,MAAM,CAAC,CAAC;YAC7C,IAAI,OAAOC,KAAK,CAAC,MAAM,KAAK,WAAW;gBACrC,MAAM,IAAI3B,eAAe,CAAC,QAAQ,EAAE0B,MAAM,0BAA0B,CAAC;YACvE;YACA,OAAO;gBACL3C,OAAOxB,KAAKoE,KAAK,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAED,MAAM,OAAO,CAAC;gBACrD1C,MAAMzB,KAAKoE,KAAK,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAED,MAAM,MAAM,CAAC;gBAClDpF,QAAQyE,SAASY,KAAK,CAAC,SAAS,EAAE,CAAC,QAAQ,EAAED,MAAM,QAAQ,CAAC;gBAC5DnF,UAAUwE,SAASY,KAAK,CAAC,WAAW,EAAE,CAAC,QAAQ,EAAED,MAAM,UAAU,CAAC;gBAClE5C,aAAaiC,SAASY,KAAK,CAAC,cAAc,EAAE,CAAC,QAAQ,EAAED,MAAM,aAAa,CAAC;gBAC3EzC,KAAK0C,KAAK,CAAC,MAAM;gBACjBzC,UAAU3B,KAAKoE,KAAK,CAAC,WAAW,EAAE,CAAC,QAAQ,EAAED,MAAM,UAAU,CAAC;gBAC9DlE,gBAAgBD,KACdoE,KAAK,CAAC,iBAAiB,EACvB,CAAC,QAAQ,EAAED,MAAM,gBAAgB,CAAC,EAClC3E,qBAAqBS,cAAc;gBAErCL,UAAU8D,KACRU,KAAK,CAAC,WAAW,EACjB,CAAC,QAAQ,EAAED,MAAM,UAAU,CAAC,EAC5B3E,qBAAqBI,QAAQ,EAC7BqE,GAAG,CAAC,CAACc,YAAYC;wBAGIC,gBAMRA;oBARb,MAAMnC,OAAO,CAAC,QAAQ,EAAEqB,MAAM,WAAW,EAAEa,aAAa,CAAC,CAAC;oBAC1D,MAAMC,UAAUtB,OAAOoB,YAAYjC;oBACnC,MAAM5C,QAAQgF,QAAOD,iBAAAA,OAAO,CAAC,QAAQ,YAAhBA,iBAAoB;oBACzC,IAAI,CAACC,OAAOC,SAAS,CAACjF,UAAUA,QAAQ,KAAKA,QAAQV,qBAAqBU,KAAK,EAAE;wBAC/E,MAAM,IAAIuC,eAAe,GAAGK,KAAK,qBAAqB,CAAC;oBACzD;oBACA,OAAO;wBACLlC,MAAMZ,KAAKiF,OAAO,CAAC,OAAO,EAAE,GAAGnC,KAAK,KAAK,CAAC;wBAC1CjD,MAAM6D,MAAKuB,gBAAAA,OAAO,CAAC,OAAO,YAAfA,gBAAmB,EAAE,EAAE,GAAGnC,KAAK,KAAK,CAAC,EAAEtD,qBAAqBK,IAAI,EACxEoE,GAAG,CAAC,CAACmB,KAAKC,WAAarF,KAAKoF,KAAK,GAAGtC,KAAK,MAAM,EAAEuC,SAAS,CAAC,CAAC,GAC5DR,MAAM,CAACC;wBACV5E;oBACF;gBACF;YACF;QACF;QAEF,MAAMH,SAAS2D,MAAKM,eAAAA,IAAI,CAAC,SAAS,YAAdA,eAAkB,EAAE,EAAE,UAAUxE,qBAAqBO,MAAM,EAAEkE,GAAG,CAClF,CAACC,KAAKC;YACJ,MAAMC,QAAQT,OAAOO,KAAK,CAAC,OAAO,EAAEC,MAAM,CAAC,CAAC;YAC5C,MAAMtF,QAAQmB,KAAKoE,KAAK,CAAC,QAAQ,EAAE,CAAC,OAAO,EAAED,MAAM,OAAO,CAAC;YAC3D,IAAI,CAACtF,OAAO,MAAM,IAAI4D,eAAe,CAAC,OAAO,EAAE0B,MAAM,gBAAgB,CAAC;YACtE,OAAO;gBACLrC,MAAM8B,MAAMQ,KAAK,CAAC,OAAO,EAAEhF,2BAA2B,CAAC,OAAO,EAAE+E,MAAM,MAAM,CAAC;gBAC7EtF;gBACAkD,OAAO/B,KAAKoE,KAAK,CAAC,QAAQ,EAAE,CAAC,OAAO,EAAED,MAAM,OAAO,CAAC;gBACpDnC,KAAKwB,SAASY,KAAK,CAAC,MAAM,EAAE,CAAC,OAAO,EAAED,MAAM,KAAK,CAAC;YACpD;QACF;QAEF,4FAA4F;QAC5F,OAAO;YACLmB,IAAI;YACJC,MAAM;gBAAE9F;gBAAOC;gBAAQC;eAAaI,OAAOkD,MAAM,GAAG;gBAAElD;YAAO,IAAI,CAAC;YAClE8C;QACF;IACF,EAAE,OAAO2C,OAAO;QACd,IAAIA,iBAAiB/C,gBAAgB,OAAO;YAAE6C,IAAI;YAAOE,OAAOA,MAAMC,OAAO;QAAC;QAC9E,MAAMD;IACR;AACF;AAEA,0DAA0D,GAC1D,OAAO,SAASE,eAAeN,GAA8B;IAC3D,OAAO,OAAOA,QAAQ,YAAYA,IAAIO,UAAU,CAACnH;AACnD;AAEA,qEAAqE,GACrE,OAAO,SAASoH,gBACdL,IAAiB,EACjBH,GAAW;IAEX,IAAI,CAACM,eAAeN,MAAM,OAAO3B;IACjC,MAAM7C,OAAOwE,IAAIhC,KAAK,CAAC5E,uBAAuByE,MAAM,EAAED,IAAI,GAAGwB,WAAW;IACxE,OAAOe,KAAK7F,MAAM,CAACmG,IAAI,CAAC,CAACzB,QAAUA,MAAMxD,IAAI,CAAC4D,WAAW,OAAO5D;AAClE;AAEA,gGAAgG,GAChG,OAAO,SAASkF,cAAcrE,IAAY;IACxC,MAAMqB,OAAOrB,KAAKuB,IAAI,GAAGwB,WAAW,GAAGuB,OAAO,CAAC,QAAQ,KAAKA,OAAO,CAAC,QAAQ;IAC5E,OAAOjD,QAAQ;AACjB;AAEA;;;CAGC,GACD,OAAO,SAASkD,gBACdT,IAAoD,EACpDU,MAA6C;;IAE7C,MAAM1B,MACJ,UAAU0B,SACNH,cAAcG,OAAOxE,IAAI,IACzB,GAAGjD,yBAAyByH,OAAOvG,MAAM,CAACsD,IAAI,GAAGwB,WAAW,IAAI;IACtE,OAAO,SAACe,wBAAAA,KAAMxF,MAAM,mBAAI,EAAE,EAAE8E,MAAM,CAAC,CAACqB,QAClC,UAAUD,SACN,CAACP,eAAeQ,MAAMrH,KAAK,KAAKiH,cAAcI,MAAMrH,KAAK,MAAM0F,MAC/D2B,MAAMrH,KAAK,CAACmE,IAAI,GAAGwB,WAAW,OAAOD;AAE7C;AAEA,8EAA8E,GAC9E,OAAO,SAAS4B,cAAcnE,GAAW;IACvC,IAAI;QACF,MAAMoE,SAAS,IAAIC,IAAIrE,IAAIgB,IAAI;QAC/B,IAAIoD,OAAOE,QAAQ,KAAK,UAAU,OAAO;QACzC,MAAMxE,OAAOsE,OAAOG,QAAQ,CAACR,OAAO,CAAC,gBAAgB;QACrD,IAAIjE,SAAS,cAAcA,SAAS,iBAAiBA,SAAS,wBAAwB;YACpF,OAAO;QACT;QACA,IAAIA,SAAS,eAAeA,SAAS,oBAAoB,OAAO;IAClE,EAAE,eAAM;IACN,cAAc,GAChB;IACA,OAAO;AACT;AAEA;;;;CAIC,GACD,OAAO,SAAS0E,gBAAgBxE,GAAW;QAUhCoE;IATT,MAAMtE,OAAOqE,cAAcnE;IAC3B,IAAI,CAACF,MAAM,OAAO;IAClB,MAAMsE,SAAS,IAAIC,IAAIrE,IAAIgB,IAAI;IAC/B,MAAMyD,QAAQL,OAAOM,QAAQ,CAACC,KAAK,CAAC,KAAK9B,MAAM,CAACC;IAChD,MAAM1D,KACJU,SAAS,UACL2E,MAAMZ,IAAI,CAAC,CAACe,OAAS,WAAWxE,IAAI,CAACwE,SACrCR,OAAOG,QAAQ,CAACM,QAAQ,CAAC,cACvBJ,KAAK,CAAC,EAAE,IACPL,2BAAAA,OAAOU,YAAY,CAACC,GAAG,CAAC,gBAAxBX,2BACAK,KAAK,CAAC,EAAE,KAAK,WAAWA,KAAK,CAAC,EAAE,KAAK,WAAWA,KAAK,CAAC,EAAE,GAAGhD;IACpE,OAAOrC,MAAM,gBAAgBgB,IAAI,CAAChB,MAAM,GAAGU,KAAK,CAAC,EAAEV,IAAI,GAAG;AAC5D;AAgBA;;;;;;CAMC,GACD,OAAO,SAAS4F,qBAAqBzB,IAAiB;IACpD,MAAM0B,QAA+B,EAAE;IACvC,MAAMC,aAAa,CAAC9B,MAClBM,eAAeN,QAAQ,CAACQ,gBAAgBL,MAAMH;IAChD,MAAM+B,SAAS,CAAC/B,MAAgBA,IAAIhC,KAAK,CAAC5E,uBAAuByE,MAAM,EAAED,IAAI;IAC7EuC,KAAK5F,OAAO,CAACyH,OAAO,CAAC,CAACC,QAAQC;QAC5B,KAAK,MAAM3C,SAAS;YAAC;YAAU;SAAW,CAAW;YACnD,MAAMS,MAAMiC,MAAM,CAAC1C,MAAM;YACzB,IAAI,CAACuC,WAAW9B,MAAM;YACtB6B,MAAM/D,IAAI,CAAC;gBACTtC,MAAMuG,OAAO/B;gBACbtC,MAAM,CAAC,QAAQ,EAAEwE,YAAY,EAAE,EAAE3C,OAAO;gBACxC2C;gBACA3C;gBACAK,cAAc;YAChB;QACF;QACAqC,OAAOzH,QAAQ,CAACwH,OAAO,CAAC,CAACnC,SAASD;YAChCC,QAAQpF,IAAI,CAACuH,OAAO,CAAC,CAAChC,KAAKmC;gBACzB,IAAI,CAACL,WAAW9B,MAAM;gBACtB6B,MAAM/D,IAAI,CAAC;oBACTtC,MAAMuG,OAAO/B;oBACbtC,MAAM,CAAC,QAAQ,EAAEwE,YAAY,WAAW,EAAEtC,aAAa,OAAO,EAAEuC,SAAS,CAAC,CAAC;oBAC3ED;oBACA3C,OAAO;oBACPK;gBACF;YACF;QACF;IACF;IACA,OAAOiC;AACT"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-build-plan.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 { AiTool } from '../providers/contract'\n\n/**\n * The plan a generation job builds from (AGL-2935): what the job's plan step\n * (`jobs/ai-job-plan-step.ts`) returns, before a single node is generated.\n *\n * This module is the plan's MODEL — its shape, its tool schema and its\n * reader — and imports nothing at runtime, so the doctrine validators, the\n * plan step, the job document and the proposal can all read it without\n * importing each other.\n *\n * A plan is REFERENCES, never documents (AGL-2937). It names what the site\n * already has by inventory id, what the job will create by a short name, and\n * which screens it builds from which of those — so the doctrine can be held\n * against the plan while fixing it costs a sentence, not a regenerated page,\n * and the customer reads what the job will do before it spends a credit on\n * doing it.\n *\n * `new:<name>` is how an entry refers to something the plan itself creates;\n * any other reference is an id from the site inventory.\n */\n\nexport const AI_PLAN_NEW_REF_PREFIX = 'new:'\n\n/** Records a plan may reuse, by the inventory list they come from. */\nexport type AiBuildPlanReuseKind =\n | 'component'\n | 'layout'\n | 'template'\n | 'form'\n | 'dataset'\n | 'collection'\n | 'screen'\n | 'theme'\n\nexport const AI_BUILD_PLAN_REUSE_KINDS: readonly AiBuildPlanReuseKind[] = [\n 'component',\n 'layout',\n 'template',\n 'form',\n 'dataset',\n 'collection',\n 'screen',\n 'theme',\n]\n\n/** What a plan may create. A theme change is a draft through the theme door, never a literal. */\nexport type AiBuildPlanCreateKind =\n | 'component'\n | 'layout'\n | 'template'\n | 'form'\n | 'theme-change'\n | 'dataset'\n | 'email'\n\n/**\n * What each creation is called in a sentence, and where a member makes one.\n *\n * Total over the union deliberately: a refusal that cannot name a creation is\n * a refusal a member cannot act on, so widening `AiBuildPlanCreateKind`\n * without saying where the new thing is made does not compile.\n */\nexport const AI_BUILD_PLAN_CREATION_NOUNS: Record<\n AiBuildPlanCreateKind,\n { noun: string; where: string }\n> = {\n component: { noun: 'component', where: 'on the Components page' },\n form: { noun: 'form', where: 'on the Forms page' },\n layout: { noun: 'layout', where: 'on the Layouts page' },\n template: { noun: 'template', where: 'in the Templates library' },\n 'theme-change': { noun: 'theme change', where: 'in the Theme section' },\n dataset: { noun: 'dataset', where: 'on the Datasets page' },\n email: { noun: 'email design', where: 'in Emails → Templates' },\n}\n\nexport const AI_BUILD_PLAN_CREATE_KINDS: readonly AiBuildPlanCreateKind[] = [\n 'component',\n 'layout',\n 'template',\n 'form',\n 'theme-change',\n 'dataset',\n 'email',\n]\n\nexport interface AiBuildPlanReuse {\n kind: AiBuildPlanReuseKind\n /** The inventory id. */\n id: string\n /** What the plan uses it for, in a few words. */\n purpose: string\n}\n\nexport interface AiBuildPlanCreate {\n kind: AiBuildPlanCreateKind\n /** Unique within the plan; referenced as `new:<name>`. */\n name: string\n /** Why nothing the site has will do — shown in the proposal. */\n why: string\n /** The inventory id this starts from as a duplicate, or null. */\n duplicateOf: string | null\n /**\n * A component: its props as `name:type`, an icon as `name:icon`\n * (AGL-3054). A layout: the regions it has, from the closed vocabulary\n * `AI_LAYOUT_REGIONS` holds (AGL-3024) — a layout has no props, and a\n * region named here is what the built layout is held to. A template: the\n * binding tokens its page shows, from the subject's own closed catalog\n * (`AI_TEMPLATE_SUBJECT_DEFINITIONS[subject].tokens`, AGL-3143) — the\n * template's analogue of a layout's regions, and what the built template is\n * held to. A form or a dataset: its field names. A theme change: the\n * palette paths it adds or changes.\n */\n fields: string[]\n /**\n * The id the draft this creation becomes is written under, where a unit of\n * the job builds it (AGL-3079). Minted when the job keeps the plan, never\n * part of the model's answer.\n */\n id?: string\n}\n\nexport interface AiBuildPlanSection {\n /** A few words: \"hero\", \"services grid\", \"contact form\". */\n name: string\n /** What the section places: inventory ids or `new:<name>` references. */\n uses: string[]\n /** How many repeated items it shows (cards, rows, tiles); 0 when none. */\n items: number\n}\n\n/**\n * A screen that is one dataset's record template (AGL-3475): the data plugin\n * serves it once per record at `/{base}/{slug}`, its copy bound with\n * `{{item.<field>}}`. The plan names the binding and the build drafts the\n * page; the member saves the binding in Page Properties → Record pages, since\n * a binding is a route and a job writes drafts only (rule 13).\n */\nexport interface AiBuildPlanRecord {\n /** The dataset: an inventory id or `new:<name>`. */\n dataset: string\n /** The path its record pages live under, one or more segments: `services`, `services/residential`. */\n base: string\n}\n\nexport interface AiBuildPlanScreen {\n title: string\n slug: string\n /** The layout it renders inside. */\n layout: string | null\n /** The template it applies, or null for a one-off page. */\n template: string | null\n /** The inventory screen it starts from as a duplicate, or null. */\n duplicateOf: string | null\n /** Whether the site navigation gains an entry for it. */\n nav: boolean\n seoTitle: string\n seoDescription: string\n sections: AiBuildPlanSection[]\n /**\n * The dataset this page is the record template of, or null for a page that\n * is one page. A plan kept before record templates carries none, which\n * reads as null.\n */\n record: AiBuildPlanRecord | null\n /**\n * The id the draft screen is written under, where a unit of the job builds\n * it (AGL-3079). Minted when the job keeps the plan, never part of the\n * model's answer.\n */\n id?: string\n}\n\n/** The third-party players a plan may embed: the hosts the Video embed element plays. */\nexport type AiBuildPlanEmbedHost = 'youtube' | 'vimeo'\n\nexport const AI_BUILD_PLAN_EMBED_HOSTS: readonly AiBuildPlanEmbedHost[] = ['youtube', 'vimeo']\n\n/** What each host is called in a sentence. */\nexport const AI_BUILD_PLAN_EMBED_HOST_NAMES: Record<AiBuildPlanEmbedHost, string> = {\n youtube: 'YouTube',\n vimeo: 'Vimeo',\n}\n\n/**\n * A third-party player the brief asks for (rule 16, AGL-3433). It loads its\n * host's code on the page that carries it, so it is planned, where the member\n * reads that cost before confirming, and a build admits only what the\n * confirmed plan lists.\n */\nexport interface AiBuildPlanEmbed {\n host: AiBuildPlanEmbedHost\n /** Where it plays: a screen of the plan by its slug, or `new:<name>` for a component the plan creates. */\n where: string\n /** The brief's own words asking for it, which the plan step finds in the brief. */\n asked: string\n /** The video's link as the brief gives it, or null for the site owner to paste. */\n url: string | null\n}\n\nexport interface AiBuildPlan {\n reuse: AiBuildPlanReuse[]\n create: AiBuildPlanCreate[]\n screens: AiBuildPlanScreen[]\n /**\n * The third-party players the brief asks for. Absent is none: a plan kept\n * before the list existed, and every plan whose brief asks for no player.\n */\n embeds?: AiBuildPlanEmbed[]\n}\n\n/**\n * How much one plan may hold. A job builds what one proposal can describe\n * and one person can read before confirming; a brief asking for more is\n * several jobs.\n */\nexport const AI_BUILD_PLAN_LIMITS = {\n reuse: 40,\n create: 20,\n screens: 12,\n sections: 16,\n uses: 8,\n fields: 30,\n embeds: 4,\n text: 200,\n seoDescription: 320,\n items: 500,\n} as const\n\nconst string = (description: string) => ({ type: 'string', description })\nconst nullableString = (description: string) => ({\n anyOf: [{ type: 'string' }, { type: 'null' }],\n description,\n})\nconst strings = (description: string) => ({\n type: 'array',\n items: { type: 'string' },\n description,\n})\n\n/**\n * The ceiling `parseAiBuildPlan` cuts a field's text at, in words (AGL-3022).\n * The reader repairs an overlong value by cutting it, and a model never told\n * where the cut falls writes a rationale that stops mid-sentence there.\n */\nconst atMost = (limit: number = AI_BUILD_PLAN_LIMITS.text) => `at most ${limit} characters`\n\n/**\n * The plan as a strict tool's input schema. Only the keywords strict\n * structured output accepts: every object closes its properties and lists\n * every one as required, an optional value is a `null` branch, and no\n * length or count bound appears as a keyword — those are enforced by\n * `parseAiBuildPlan`, which every answer passes through anyway.\n *\n * A text ceiling is stated in words instead, on each field the model writes.\n * A reference is copied from the inventory or from a creation's name, so its\n * length is not the model's to choose. A search title and description are\n * held by rule 10 to ceilings far inside the reader's, which the doctrine\n * states, so naming the reader's here would contradict the rule.\n */\nexport const AI_BUILD_PLAN_TOOL: AiTool = {\n name: 'submit_build_plan',\n description:\n 'Submit the build plan: what the site already has that this job reuses, what it creates and why, and the pages it builds from them.',\n strict: true,\n inputSchema: {\n type: 'object',\n additionalProperties: false,\n required: ['reuse', 'create', 'screens'],\n properties: {\n reuse: {\n type: 'array',\n description: 'Records the site already has that this plan uses, by inventory id.',\n items: {\n type: 'object',\n additionalProperties: false,\n required: ['kind', 'id', 'purpose'],\n properties: {\n kind: { type: 'string', enum: [...AI_BUILD_PLAN_REUSE_KINDS] },\n id: string('The id from the site inventory.'),\n purpose: string(`What the plan uses it for, in a few words, ${atMost()}.`),\n },\n },\n },\n create: {\n type: 'array',\n description:\n 'What the plan creates. Creating is the exception: each entry says why nothing listed will do.',\n items: {\n type: 'object',\n additionalProperties: false,\n required: ['kind', 'name', 'why', 'duplicateOf', 'fields'],\n properties: {\n kind: { type: 'string', enum: [...AI_BUILD_PLAN_CREATE_KINDS] },\n name: string(\n `Unique within the plan, ${atMost()}; other entries refer to it as new:<name>.`,\n ),\n why: string(`Why nothing the site already has will do, in one sentence of ${atMost()}.`),\n duplicateOf: nullableString(\n 'The inventory id this starts from as a duplicate, or null.',\n ),\n fields: strings(\n `A component: its props as name:type, an icon as name:icon. A layout: the regions it has, each one of header, nav, sidebar, main, footer — list every region the finished layout has, not only the ones it gains. A form or dataset: its field names. A theme change: the palette paths it adds or changes. Each ${atMost()}.`,\n ),\n },\n },\n },\n screens: {\n type: 'array',\n description: 'The pages the job builds, each a new draft.',\n items: {\n type: 'object',\n additionalProperties: false,\n required: [\n 'title',\n 'slug',\n 'layout',\n 'template',\n 'duplicateOf',\n 'nav',\n 'seoTitle',\n 'seoDescription',\n 'sections',\n ],\n properties: {\n title: string(`The page name, ${atMost()}.`),\n slug: string(\n `The path, ${atMost()}: lowercase words joined by hyphens, e.g. /services/roof-repair.`,\n ),\n layout: nullableString(\n 'The layout it renders inside: an inventory id or new:<name>.',\n ),\n template: nullableString(\n 'The template it applies (inventory id or new:<name>), or null for a one-off page.',\n ),\n duplicateOf: nullableString(\n 'The inventory page it starts from as a duplicate, or null.',\n ),\n nav: {\n type: 'boolean',\n description: 'Whether the site navigation gains an entry for it.',\n },\n seoTitle: string('The search-result title.'),\n seoDescription: string('The search-result description.'),\n sections: {\n type: 'array',\n description: 'The sections, top to bottom.',\n items: {\n type: 'object',\n additionalProperties: false,\n required: ['name', 'uses', 'items'],\n properties: {\n name: string(`A few words, ${atMost()}: hero, services grid, contact form.`),\n uses: strings('What the section places: inventory ids or new:<name>.'),\n items: {\n type: 'integer',\n description: 'How many repeated items it shows; 0 when none.',\n },\n },\n },\n },\n },\n },\n },\n },\n },\n}\n\n/** Words that ask for a video, or a link to one. */\nexport const AI_PLAN_ASKS_FOR_VIDEO = /\\b(?:videos?|films?|clips?|youtube|vimeo|player)\\b|youtu\\.be/i\n\nconst EMBEDS_SCHEMA = {\n type: 'array',\n description:\n 'A YouTube or Vimeo player the brief asks for, on a screen or a created component. Empty unless the brief asks; a library film is a Video.',\n items: {\n type: 'object',\n additionalProperties: false,\n required: ['host', 'where', 'asked', 'url'],\n properties: {\n host: { type: 'string', enum: [...AI_BUILD_PLAN_EMBED_HOSTS] },\n where: string('A screen slug, or new:<name> of a component.'),\n asked: string('The words of the brief that ask for it, copied exactly.'),\n url: nullableString('The video link the brief gives, or null.'),\n },\n },\n}\n\n/**\n * The plan tool with its list of third-party players, offered only to a brief\n * that asks for a video (AGL-3433). Every other plan's request is the plain\n * tool, byte for byte, so the list costs nothing on the plans that cannot use\n * it, and a model answering a brief that names no video has nowhere to plan a\n * player it was never asked for.\n */\nexport const AI_BUILD_PLAN_EMBEDS_TOOL: AiTool = {\n ...AI_BUILD_PLAN_TOOL,\n inputSchema: {\n ...AI_BUILD_PLAN_TOOL.inputSchema,\n required: [...(AI_BUILD_PLAN_TOOL.inputSchema['required'] as string[]), 'embeds'],\n properties: {\n ...(AI_BUILD_PLAN_TOOL.inputSchema['properties'] as Record<string, unknown>),\n embeds: EMBEDS_SCHEMA,\n },\n },\n}\n\nconst RECORD_SCHEMA = {\n anyOf: [\n {\n type: 'object',\n additionalProperties: false,\n required: ['dataset', 'base'],\n properties: {\n dataset: string('The dataset: an inventory id or new:<name>.'),\n base: string('The path its record pages live under, e.g. services for /services/roofing.'),\n },\n },\n { type: 'null' },\n ],\n description:\n \"When this page is one dataset's record template, serving a page per record at /<base>/<record address> with its copy bound as {{item.<field>}}: the dataset and the base. Otherwise null.\",\n}\n\n/** A plan tool whose screens each name the dataset they are the record template of, or null. */\nfunction withRecords(tool: AiTool): AiTool {\n const properties = tool.inputSchema['properties'] as Record<string, unknown>\n const screens = properties['screens'] as { items: Record<string, unknown> }\n return {\n ...tool,\n inputSchema: {\n ...tool.inputSchema,\n properties: {\n ...properties,\n screens: {\n ...screens,\n items: {\n ...screens.items,\n required: [...(screens.items['required'] as string[]), 'record'],\n properties: {\n ...(screens.items['properties'] as Record<string, unknown>),\n record: RECORD_SCHEMA,\n },\n },\n },\n },\n },\n }\n}\n\n/**\n * The plan tools with a record template on every screen (AGL-3475), offered\n * only where the job may bind a dataset: a site with a dataset, or a job that\n * may create one. Record pages are a Starter feature, and every other plan's\n * request is the plain tool, byte for byte, so the field costs a Free taste\n * nothing and a model planning a site with no dataset has none to bind.\n */\nexport const AI_BUILD_PLAN_RECORDS_TOOL: AiTool = withRecords(AI_BUILD_PLAN_TOOL)\nexport const AI_BUILD_PLAN_EMBEDS_RECORDS_TOOL: AiTool = withRecords(AI_BUILD_PLAN_EMBEDS_TOOL)\n\n/** The plan tool a brief is answered with, on a job that may or may not bind a dataset. */\nexport function aiBuildPlanToolFor(brief: string, options: { records?: boolean } = {}): AiTool {\n const video = AI_PLAN_ASKS_FOR_VIDEO.test(brief)\n if (options.records) return video ? AI_BUILD_PLAN_EMBEDS_RECORDS_TOOL : AI_BUILD_PLAN_RECORDS_TOOL\n return video ? AI_BUILD_PLAN_EMBEDS_TOOL : AI_BUILD_PLAN_TOOL\n}\n\nexport type AiBuildPlanParse =\n | { ok: true; plan: AiBuildPlan; repairs: string[] }\n | { ok: false; error: string }\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\nclass PlanShapeError extends Error {}\n\n/**\n * Read a plan out of a tool call, or name what makes it unreadable.\n *\n * Structure is refused — a missing list, an unknown kind, more screens than\n * a job holds, two creations under one name — because a plan that reads\n * differently from what the model meant is worse than a re-ask. Length is\n * repaired: copy past a ceiling is cut and the cut is listed.\n */\nexport function parseAiBuildPlan(input: unknown): AiBuildPlanParse {\n const repairs: string[] = []\n const text = (\n value: unknown,\n path: string,\n limit: number = AI_BUILD_PLAN_LIMITS.text,\n ): string => {\n if (typeof value !== 'string') throw new PlanShapeError(`${path} is not text`)\n const trimmed = value.trim()\n if (trimmed.length > limit) {\n repairs.push(`${path} was over ${limit} characters; truncated`)\n // On the last word break inside the ceiling, not through a word: a\n // rationale is read by the member confirming the plan, and half a word\n // reads as a broken answer rather than a long one (AGL-3022). A run of\n // `limit` characters with no break in it still has to be cut where the\n // ceiling falls.\n const cut = trimmed.slice(0, limit)\n const brk = cut.search(/\\s\\S*$/)\n return (brk > 0 ? cut.slice(0, brk) : cut).trimEnd()\n }\n return trimmed\n }\n const nullable = (value: unknown, path: string): string | null =>\n value === null || value === undefined || value === '' ? null : text(value, path) || null\n const list = (value: unknown, path: string, limit: number): unknown[] => {\n if (!Array.isArray(value)) throw new PlanShapeError(`${path} is not a list`)\n if (value.length > limit) {\n throw new PlanShapeError(`${path} lists ${value.length}; one plan holds at most ${limit}`)\n }\n return value\n }\n const record = (value: unknown, path: string): Record<string, unknown> => {\n if (!isRecord(value)) throw new PlanShapeError(`${path} is not an object`)\n return value\n }\n const oneOf = <T extends string>(value: unknown, allowed: readonly T[], path: string): T => {\n if (typeof value !== 'string' || !(allowed as readonly string[]).includes(value)) {\n throw new PlanShapeError(`${path} is not one of ${allowed.join(', ')}`)\n }\n return value as T\n }\n // Absent or null is a page that is one page. A base is held to the shape a\n // record page's address takes, so the binding the member saves from it is\n // one the save route admits.\n const planRecord = (value: unknown, path: string): AiBuildPlanRecord | null => {\n if (value === null || value === undefined) return null\n const entry = record(value, path)\n const dataset = text(entry['dataset'], `${path}.dataset`)\n if (!dataset) throw new PlanShapeError(`${path}.dataset is empty`)\n const base = aiPlanRecordBase(text(entry['base'], `${path}.base`))\n if (!base) throw new PlanShapeError(`${path}.base is not a path of lowercase words joined by hyphens`)\n return { dataset, base }\n }\n\n try {\n const root = record(input, 'plan')\n const reuse = list(root['reuse'], 'reuse', AI_BUILD_PLAN_LIMITS.reuse).map((raw, index) => {\n const entry = record(raw, `reuse[${index}]`)\n const id = text(entry['id'], `reuse[${index}].id`)\n if (!id) throw new PlanShapeError(`reuse[${index}].id is empty`)\n return {\n kind: oneOf(entry['kind'], AI_BUILD_PLAN_REUSE_KINDS, `reuse[${index}].kind`),\n id,\n purpose: text(entry['purpose'], `reuse[${index}].purpose`),\n }\n })\n const names = new Set<string>()\n const create = list(root['create'], 'create', AI_BUILD_PLAN_LIMITS.create).map(\n (raw, index) => {\n const entry = record(raw, `create[${index}]`)\n const name = text(entry['name'], `create[${index}].name`)\n if (!name) throw new PlanShapeError(`create[${index}].name is empty`)\n const key = name.toLowerCase()\n if (names.has(key)) {\n throw new PlanShapeError(`create[${index}].name \"${name}\" is used twice`)\n }\n names.add(key)\n return {\n kind: oneOf(entry['kind'], AI_BUILD_PLAN_CREATE_KINDS, `create[${index}].kind`),\n name,\n why: text(entry['why'], `create[${index}].why`),\n duplicateOf: nullable(entry['duplicateOf'], `create[${index}].duplicateOf`),\n fields: list(entry['fields'] ?? [], `create[${index}].fields`, AI_BUILD_PLAN_LIMITS.fields)\n .map((field, fieldIndex) => text(field, `create[${index}].fields[${fieldIndex}]`))\n .filter(Boolean),\n }\n },\n )\n const screens = list(root['screens'], 'screens', AI_BUILD_PLAN_LIMITS.screens).map(\n (raw, index) => {\n const entry = record(raw, `screens[${index}]`)\n if (typeof entry['nav'] !== 'boolean') {\n throw new PlanShapeError(`screens[${index}].nav is not true or false`)\n }\n return {\n title: text(entry['title'], `screens[${index}].title`),\n slug: text(entry['slug'], `screens[${index}].slug`),\n layout: nullable(entry['layout'], `screens[${index}].layout`),\n template: nullable(entry['template'], `screens[${index}].template`),\n duplicateOf: nullable(entry['duplicateOf'], `screens[${index}].duplicateOf`),\n nav: entry['nav'],\n seoTitle: text(entry['seoTitle'], `screens[${index}].seoTitle`),\n seoDescription: text(\n entry['seoDescription'],\n `screens[${index}].seoDescription`,\n AI_BUILD_PLAN_LIMITS.seoDescription,\n ),\n sections: list(\n entry['sections'],\n `screens[${index}].sections`,\n AI_BUILD_PLAN_LIMITS.sections,\n ).map((rawSection, sectionIndex) => {\n const path = `screens[${index}].sections[${sectionIndex}]`\n const section = record(rawSection, path)\n const items = Number(section['items'] ?? 0)\n if (!Number.isInteger(items) || items < 0 || items > AI_BUILD_PLAN_LIMITS.items) {\n throw new PlanShapeError(`${path}.items is not a count`)\n }\n return {\n name: text(section['name'], `${path}.name`),\n uses: list(section['uses'] ?? [], `${path}.uses`, AI_BUILD_PLAN_LIMITS.uses)\n .map((ref, refIndex) => text(ref, `${path}.uses[${refIndex}]`))\n .filter(Boolean),\n items,\n }\n }),\n record: planRecord(entry['record'], `screens[${index}].record`),\n }\n },\n )\n const embeds = list(root['embeds'] ?? [], 'embeds', AI_BUILD_PLAN_LIMITS.embeds).map(\n (raw, index) => {\n const entry = record(raw, `embeds[${index}]`)\n const where = text(entry['where'], `embeds[${index}].where`)\n if (!where) throw new PlanShapeError(`embeds[${index}].where is empty`)\n return {\n host: oneOf(entry['host'], AI_BUILD_PLAN_EMBED_HOSTS, `embeds[${index}].host`),\n where,\n asked: text(entry['asked'], `embeds[${index}].asked`),\n url: nullable(entry['url'], `embeds[${index}].url`),\n }\n },\n )\n // A plan whose brief asks for no player carries no list, as it did before the list existed.\n return {\n ok: true,\n plan: { reuse, create, screens, ...(embeds.length ? { embeds } : {}) },\n repairs,\n }\n } catch (error) {\n if (error instanceof PlanShapeError) return { ok: false, error: error.message }\n throw error\n }\n}\n\n/**\n * A record template's base as the data plugin's save route stores it\n * (`normalizeRecordPageBase`, which this plugin may not import): one to four\n * lowercase segments of letters, digits and hyphens, with no leading or\n * trailing slash; null for a base that is no such path.\n */\nexport function aiPlanRecordBase(base: string): string | null {\n const segments = base.trim().toLowerCase().split('/').filter(Boolean)\n if (!segments.length || segments.length > 4) return null\n return segments.every(\n (segment) => segment.length <= 60 && /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(segment),\n )\n ? segments.join('/')\n : null\n}\n\n/** Whether a reference names something the plan creates. */\nexport function isAiPlanNewRef(ref: string | null | undefined): ref is string {\n return typeof ref === 'string' && ref.startsWith(AI_PLAN_NEW_REF_PREFIX)\n}\n\n/** The creation a `new:<name>` reference names, case-insensitively. */\nexport function aiPlanCreateFor(\n plan: AiBuildPlan,\n ref: string,\n): AiBuildPlanCreate | undefined {\n if (!isAiPlanNewRef(ref)) return undefined\n const name = ref.slice(AI_PLAN_NEW_REF_PREFIX.length).trim().toLowerCase()\n return plan.create.find((entry) => entry.name.toLowerCase() === name)\n}\n\n/** A slug as two spellings of one path compare: lowercase, one leading slash, no trailing one. */\nexport function aiPlanSlugKey(slug: string): string {\n const path = slug.trim().toLowerCase().replace(/^\\/*/, '/').replace(/\\/+$/, '')\n return path || '/'\n}\n\n/**\n * The players the plan lists for one output (AGL-3433): a screen by its slug,\n * or a creation by its name. A build admits these and no other.\n */\nexport function aiPlanEmbedsFor(\n plan: Pick<AiBuildPlan, 'embeds'> | null | undefined,\n target: { slug: string } | { create: string },\n): AiBuildPlanEmbed[] {\n const key =\n 'slug' in target\n ? aiPlanSlugKey(target.slug)\n : `${AI_PLAN_NEW_REF_PREFIX}${target.create.trim().toLowerCase()}`\n return (plan?.embeds ?? []).filter((embed) =>\n 'slug' in target\n ? !isAiPlanNewRef(embed.where) && aiPlanSlugKey(embed.where) === key\n : embed.where.trim().toLowerCase() === key,\n )\n}\n\n/** The host a video link plays from, or null for a link neither host serves. */\nexport function aiEmbedHostOf(url: string): AiBuildPlanEmbedHost | null {\n try {\n const parsed = new URL(url.trim())\n if (parsed.protocol !== 'https:') return null\n const host = parsed.hostname.replace(/^(?:www|m)\\./, '')\n if (host === 'youtu.be' || host === 'youtube.com' || host === 'youtube-nocookie.com') {\n return 'youtube'\n }\n if (host === 'vimeo.com' || host === 'player.vimeo.com') return 'vimeo'\n } catch {\n /* not a link */\n }\n return null\n}\n\n/**\n * One video however its link is spelled — `youtu.be/<id>`, a watch link, an\n * embed or a Shorts link — as `<host>:<id>`, the way the Video embed element\n * reads a link; null for a link it cannot play.\n */\nexport function aiEmbedVideoKey(url: string): string | null {\n const host = aiEmbedHostOf(url)\n if (!host) return null\n const parsed = new URL(url.trim())\n const parts = parsed.pathname.split('/').filter(Boolean)\n const id =\n host === 'vimeo'\n ? parts.find((part) => /^\\d{6,}$/.test(part))\n : parsed.hostname.endsWith('youtu.be')\n ? parts[0]\n : (parsed.searchParams.get('v') ??\n (parts[0] === 'embed' || parts[0] === 'shorts' ? parts[1] : undefined))\n return id && /^[\\w-]{6,20}$/.test(id) ? `${host}:${id}` : null\n}\n\n/**\n * A `new:<name>` reference that names no entry of the plan's create list: a\n * screen's own layout or template, or what one of its sections places, with\n * that section's index.\n */\nexport type AiPlanUndeclaredRef = {\n /** The name the reference gives the creation, as written after `new:`. */\n name: string\n /** Where the plan writes it: `screens[0].template`, `screens[0].sections[4].uses[0]`. */\n path: string\n /** The screen that writes it, by index. */\n screenIndex: number\n} & ({ field: 'layout' | 'template'; sectionIndex: null } | { field: 'uses'; sectionIndex: number })\n\n/**\n * Every `new:<name>` reference the plan's create list does not carry, in plan\n * order: each screen's layout, then its template, then what its sections\n * place. A plan that places a creation it never declares has nothing to build\n * it from, so the plan rules refuse each (AGL-3040), and the page and\n * scaffold doors still name one in a plan confirmed before they did.\n */\nexport function aiPlanUndeclaredRefs(plan: AiBuildPlan): AiPlanUndeclaredRef[] {\n const found: AiPlanUndeclaredRef[] = []\n const undeclared = (ref: string | null): ref is string =>\n isAiPlanNewRef(ref) && !aiPlanCreateFor(plan, ref)\n const nameOf = (ref: string) => ref.slice(AI_PLAN_NEW_REF_PREFIX.length).trim()\n plan.screens.forEach((screen, screenIndex) => {\n for (const field of ['layout', 'template'] as const) {\n const ref = screen[field]\n if (!undeclared(ref)) continue\n found.push({\n name: nameOf(ref),\n path: `screens[${screenIndex}].${field}`,\n screenIndex,\n field,\n sectionIndex: null,\n })\n }\n screen.sections.forEach((section, sectionIndex) => {\n section.uses.forEach((ref, useIndex) => {\n if (!undeclared(ref)) return\n found.push({\n name: nameOf(ref),\n path: `screens[${screenIndex}].sections[${sectionIndex}].uses[${useIndex}]`,\n screenIndex,\n field: 'uses',\n sectionIndex,\n })\n })\n })\n })\n return found\n}\n"],"names":["AI_PLAN_NEW_REF_PREFIX","AI_BUILD_PLAN_REUSE_KINDS","AI_BUILD_PLAN_CREATION_NOUNS","component","noun","where","form","layout","template","dataset","email","AI_BUILD_PLAN_CREATE_KINDS","AI_BUILD_PLAN_EMBED_HOSTS","AI_BUILD_PLAN_EMBED_HOST_NAMES","youtube","vimeo","AI_BUILD_PLAN_LIMITS","reuse","create","screens","sections","uses","fields","embeds","text","seoDescription","items","string","description","type","nullableString","anyOf","strings","atMost","limit","AI_BUILD_PLAN_TOOL","name","strict","inputSchema","additionalProperties","required","properties","kind","enum","id","purpose","why","duplicateOf","title","slug","nav","seoTitle","AI_PLAN_ASKS_FOR_VIDEO","EMBEDS_SCHEMA","host","asked","url","AI_BUILD_PLAN_EMBEDS_TOOL","RECORD_SCHEMA","base","withRecords","tool","record","AI_BUILD_PLAN_RECORDS_TOOL","AI_BUILD_PLAN_EMBEDS_RECORDS_TOOL","aiBuildPlanToolFor","brief","options","video","test","records","isRecord","value","Array","isArray","PlanShapeError","Error","parseAiBuildPlan","input","repairs","path","trimmed","trim","length","push","cut","slice","brk","search","trimEnd","nullable","undefined","list","oneOf","allowed","includes","join","planRecord","entry","aiPlanRecordBase","root","map","raw","index","names","Set","key","toLowerCase","has","add","field","fieldIndex","filter","Boolean","rawSection","sectionIndex","section","Number","isInteger","ref","refIndex","ok","plan","error","message","segments","split","every","segment","isAiPlanNewRef","startsWith","aiPlanCreateFor","find","aiPlanSlugKey","replace","aiPlanEmbedsFor","target","embed","aiEmbedHostOf","parsed","URL","protocol","hostname","aiEmbedVideoKey","parts","pathname","part","endsWith","searchParams","get","aiPlanUndeclaredRefs","found","undeclared","nameOf","forEach","screen","screenIndex","useIndex"],"mappings":"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"}
|
|
@@ -95,6 +95,11 @@ export interface AiInventoryDataset {
|
|
|
95
95
|
id: string;
|
|
96
96
|
name: string;
|
|
97
97
|
fields: string[];
|
|
98
|
+
/**
|
|
99
|
+
* The same fields' ids, in the same order: what a record template binds as
|
|
100
|
+
* `{{item.<id>}}` (AGL-3475). Absent where the reader did not see them.
|
|
101
|
+
*/
|
|
102
|
+
fieldIds?: string[];
|
|
98
103
|
}
|
|
99
104
|
export interface AiInventoryCollection {
|
|
100
105
|
id: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-site-inventory.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * The site's inventory as a model sees it (AGL-2935): what the site already\n * has, so a plan reuses before it creates (doctrine rule 7).\n *\n * COMPACT by construction (AGL-2937): ids, names, prop shapes and slugs —\n * never a node tree, never a record's content. A component is its name and\n * the props an instance fills in; a form is its field names; a dataset its\n * field names. That is everything a planner needs to decide \"reuse this\"\n * and a generator needs to place a reference, and it is a few hundred\n * tokens where the trees would be tens of thousands.\n *\n * CAPPED three times, and the three are different numbers on purpose\n * (AGL-2937). The reader holds each kind to `AI_SITE_INVENTORY_MAX_PER_KIND`;\n * the prompt block LISTS only `AI_SITE_INVENTORY_LISTED_PER_KIND` of them and\n * is held to `AI_SITE_INVENTORY_MAX_CHARS` besides; and the rest are answered\n * on request, through the lookup tool the generation loop offers beside every\n * door's own (`tools/ai-inventory-lookup-tool.ts`).\n *\n * The listing cap is what a prompt can afford: the block is volatile, so it\n * is billed at full input rate on every request and every re-ask, and a site\n * with two hundred components would spend most of a plan's prompt on rows it\n * will not use. The read cap is what a LOOKUP can answer from, which is a\n * different question — a search for \"pricing card\" is worth nothing if the\n * card was never read. A kind cut by either is named in `truncated`, so the\n * model is told more exist rather than concluding they do not.\n *\n * The shapes live here, apart from the reader (`runtime/site-inventory.ts`),\n * so the prompt block, the validators and their specs can hold an inventory\n * without loading the Admin SDK the reader needs.\n */\n\n/**\n * Rows per kind the reader holds. Above the listing cap on purpose: the rows\n * past it are what the lookup tool searches, and a row never read cannot be\n * found. One projection of a few fields each, so the read is small even at\n * this cap.\n */\nexport const AI_SITE_INVENTORY_MAX_PER_KIND = 200\n\n/** Rows per kind the prompt block lists; the rest are found by lookup. */\nexport const AI_SITE_INVENTORY_LISTED_PER_KIND = 40\n\n/** The rendered block's ceiling: 2,000 tokens at four characters a token. */\nexport const AI_SITE_INVENTORY_MAX_CHARS = 8_000\n\nexport type AiInventoryKind =\n | 'components'\n | 'layouts'\n | 'templates'\n | 'forms'\n | 'datasets'\n | 'collections'\n | 'screens'\n\nexport const AI_INVENTORY_KINDS: readonly AiInventoryKind[] = [\n 'components',\n 'layouts',\n 'templates',\n 'forms',\n 'datasets',\n 'collections',\n 'screens',\n]\n\n/**\n * A reusable component's declared props as an instance may fill them: prop\n * name → its declared type (`text`, `image`, `href`, `number`, `boolean`, …).\n */\nexport type AiComponentPropTypes = Record<string, string>\n\nexport interface AiInventoryComponent {\n id: string\n name: string\n /** The props an instance fills in: name → declared type. */\n props: AiComponentPropTypes\n /**\n * The facts in square brackets each prop's default shows until a page sets\n * its own, by prop name (AGL-3056): what a page placing the component still\n * asks the member to fill. Read for the member's note, never listed in a\n * prompt; absent when no default holds one.\n */\n bracketedDefaults?: Record<string, string[]>\n}\n\nexport interface AiInventoryLayout {\n id: string\n name: string\n /** The layout this one nests inside, when it nests. */\n parentId: string | null\n}\n\nexport interface AiInventoryTemplate {\n id: string\n name: string\n kind: string\n}\n\nexport interface AiInventoryForm {\n id: string\n name: string\n fields: string[]\n}\n\nexport interface AiInventoryDataset {\n id: string\n name: string\n fields: string[]\n}\n\nexport interface AiInventoryCollection {\n id: string\n name: string\n slug: string\n}\n\nexport interface AiInventoryScreen {\n id: string\n name: string\n slug: string\n layoutId: string | null\n /** A collection's entry template rather than a page of its own. */\n template: boolean\n}\n\n/**\n * The site's one theme, as a planner and the email rule read it: a short\n * description, the light scheme's colors by palette path, and the families\n * the theme loads.\n */\nexport interface AiInventoryTheme {\n summary: string[]\n colors: Record<string, string>\n fonts: string[]\n}\n\nexport interface AiSiteInventory {\n hostId: string\n components: AiInventoryComponent[]\n layouts: AiInventoryLayout[]\n templates: AiInventoryTemplate[]\n forms: AiInventoryForm[]\n datasets: AiInventoryDataset[]\n collections: AiInventoryCollection[]\n screens: AiInventoryScreen[]\n theme: AiInventoryTheme | null\n /** Kinds with more records than are listed. */\n truncated: AiInventoryKind[]\n}\n\n/** One inventory record, whatever kind it is. */\nexport type AiInventoryRow =\n | AiInventoryComponent\n | AiInventoryLayout\n | AiInventoryTemplate\n | AiInventoryForm\n | AiInventoryDataset\n | AiInventoryCollection\n | AiInventoryScreen\n\n/**\n * The rows of one kind. The inventory's fields are named after their kinds,\n * so this is a lookup rather than a switch, and it is the one place that\n * relies on the correspondence: the prompt block, the lookup tool and their\n * specs all read a kind through it.\n */\nexport function aiInventoryRows(\n inventory: AiSiteInventory,\n kind: AiInventoryKind,\n): readonly AiInventoryRow[] {\n return inventory[kind]\n}\n\n/** The heading one kind's lines are listed under, naming its columns in order. */\nexport const AI_INVENTORY_KIND_HEADINGS: Readonly<Record<AiInventoryKind, string>> = {\n components: 'Reusable components (id \\u00b7 name \\u00b7 props an instance fills)',\n layouts: 'Layouts (id \\u00b7 name)',\n templates: 'Templates (id \\u00b7 name \\u00b7 kind)',\n forms: 'Forms (id \\u00b7 name \\u00b7 fields)',\n datasets: 'Datasets (id \\u00b7 name \\u00b7 fields)',\n collections: 'Content collections (id \\u00b7 name \\u00b7 slug)',\n screens: 'Pages (id \\u00b7 name \\u00b7 slug)',\n}\n\n/**\n * One record as a line, in the order its heading names the columns. The one\n * renderer: the prompt block and the lookup tool's answer read alike, so a\n * row found by lookup is the same shape as a row that was listed.\n */\nexport function aiInventoryLine(kind: AiInventoryKind, row: AiInventoryRow): string {\n const head = `${row.id} \\u00b7 ${row.name}`\n switch (kind) {\n case 'components': {\n const props = Object.entries((row as AiInventoryComponent).props)\n .map(([name, type]) => `${name}:${type}`)\n .join(', ')\n return `${head} \\u00b7 ${props || 'no props'}`\n }\n case 'layouts':\n return head\n case 'templates':\n return `${head} \\u00b7 ${(row as AiInventoryTemplate).kind}`\n case 'forms':\n return `${head} \\u00b7 ${(row as AiInventoryForm).fields.join(', ')}`\n case 'datasets':\n return `${head} \\u00b7 ${(row as AiInventoryDataset).fields.join(', ')}`\n case 'collections':\n return `${head} \\u00b7 ${(row as AiInventoryCollection).slug}`\n case 'screens': {\n const screen = row as AiInventoryScreen\n return `${head} \\u00b7 ${screen.slug}${screen.template ? ' \\u00b7 entry template' : ''}`\n }\n }\n}\n\n/** Every word of a record a lookup searches: its line, which holds them all. */\nexport function aiInventoryHaystack(kind: AiInventoryKind, row: AiInventoryRow): string {\n return aiInventoryLine(kind, row).toLowerCase()\n}\n\n/** The addresses a site's home screen is stored under. */\nconst HOME_SLUGS = new Set(['', 'home', 'index'])\n\n/**\n * The site's home screens, by id (AGL-3056): a screen at the root or at\n * `home`, as a seeded site stores its first screen, or one named Home. A link\n * with nowhere to go is sent there, which is why the doctrine names them.\n */\nexport function aiHomeScreenIds(inventory: AiSiteInventory | null): string[] {\n return (inventory?.screens ?? [])\n .filter(\n (screen) =>\n !screen.template &&\n (HOME_SLUGS.has(screen.slug.trim().replace(/^\\/+|\\/+$/g, '').toLowerCase()) ||\n /^home(?:\\s?page)?$/i.test(screen.name.trim())),\n )\n .map((screen) => screen.id)\n}\n\n/** A site the org does not own; the inventory is never read for it. */\nexport class AiInventoryScopeError extends Error {\n constructor(hostId: string) {\n super(`host ${hostId} is not a site of the workspace that asked`)\n this.name = 'AiInventoryScopeError'\n }\n}\n\nexport function emptyAiSiteInventory(hostId: string): AiSiteInventory {\n return {\n hostId,\n components: [],\n layouts: [],\n templates: [],\n forms: [],\n datasets: [],\n collections: [],\n screens: [],\n theme: null,\n truncated: [],\n }\n}\n"],"names":["AI_SITE_INVENTORY_MAX_PER_KIND","AI_SITE_INVENTORY_LISTED_PER_KIND","AI_SITE_INVENTORY_MAX_CHARS","AI_INVENTORY_KINDS","aiInventoryRows","inventory","kind","AI_INVENTORY_KIND_HEADINGS","components","layouts","templates","forms","datasets","collections","screens","aiInventoryLine","row","head","id","name","props","Object","entries","map","type","join","fields","slug","screen","template","aiInventoryHaystack","toLowerCase","HOME_SLUGS","Set","aiHomeScreenIds","filter","has","trim","replace","test","AiInventoryScopeError","Error","hostId","emptyAiSiteInventory","theme","truncated"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-site-inventory.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * The site's inventory as a model sees it (AGL-2935): what the site already\n * has, so a plan reuses before it creates (doctrine rule 7).\n *\n * COMPACT by construction (AGL-2937): ids, names, prop shapes and slugs —\n * never a node tree, never a record's content. A component is its name and\n * the props an instance fills in; a form is its field names; a dataset its\n * field names. That is everything a planner needs to decide \"reuse this\"\n * and a generator needs to place a reference, and it is a few hundred\n * tokens where the trees would be tens of thousands.\n *\n * CAPPED three times, and the three are different numbers on purpose\n * (AGL-2937). The reader holds each kind to `AI_SITE_INVENTORY_MAX_PER_KIND`;\n * the prompt block LISTS only `AI_SITE_INVENTORY_LISTED_PER_KIND` of them and\n * is held to `AI_SITE_INVENTORY_MAX_CHARS` besides; and the rest are answered\n * on request, through the lookup tool the generation loop offers beside every\n * door's own (`tools/ai-inventory-lookup-tool.ts`).\n *\n * The listing cap is what a prompt can afford: the block is volatile, so it\n * is billed at full input rate on every request and every re-ask, and a site\n * with two hundred components would spend most of a plan's prompt on rows it\n * will not use. The read cap is what a LOOKUP can answer from, which is a\n * different question — a search for \"pricing card\" is worth nothing if the\n * card was never read. A kind cut by either is named in `truncated`, so the\n * model is told more exist rather than concluding they do not.\n *\n * The shapes live here, apart from the reader (`runtime/site-inventory.ts`),\n * so the prompt block, the validators and their specs can hold an inventory\n * without loading the Admin SDK the reader needs.\n */\n\n/**\n * Rows per kind the reader holds. Above the listing cap on purpose: the rows\n * past it are what the lookup tool searches, and a row never read cannot be\n * found. One projection of a few fields each, so the read is small even at\n * this cap.\n */\nexport const AI_SITE_INVENTORY_MAX_PER_KIND = 200\n\n/** Rows per kind the prompt block lists; the rest are found by lookup. */\nexport const AI_SITE_INVENTORY_LISTED_PER_KIND = 40\n\n/** The rendered block's ceiling: 2,000 tokens at four characters a token. */\nexport const AI_SITE_INVENTORY_MAX_CHARS = 8_000\n\nexport type AiInventoryKind =\n | 'components'\n | 'layouts'\n | 'templates'\n | 'forms'\n | 'datasets'\n | 'collections'\n | 'screens'\n\nexport const AI_INVENTORY_KINDS: readonly AiInventoryKind[] = [\n 'components',\n 'layouts',\n 'templates',\n 'forms',\n 'datasets',\n 'collections',\n 'screens',\n]\n\n/**\n * A reusable component's declared props as an instance may fill them: prop\n * name → its declared type (`text`, `image`, `href`, `number`, `boolean`, …).\n */\nexport type AiComponentPropTypes = Record<string, string>\n\nexport interface AiInventoryComponent {\n id: string\n name: string\n /** The props an instance fills in: name → declared type. */\n props: AiComponentPropTypes\n /**\n * The facts in square brackets each prop's default shows until a page sets\n * its own, by prop name (AGL-3056): what a page placing the component still\n * asks the member to fill. Read for the member's note, never listed in a\n * prompt; absent when no default holds one.\n */\n bracketedDefaults?: Record<string, string[]>\n}\n\nexport interface AiInventoryLayout {\n id: string\n name: string\n /** The layout this one nests inside, when it nests. */\n parentId: string | null\n}\n\nexport interface AiInventoryTemplate {\n id: string\n name: string\n kind: string\n}\n\nexport interface AiInventoryForm {\n id: string\n name: string\n fields: string[]\n}\n\nexport interface AiInventoryDataset {\n id: string\n name: string\n fields: string[]\n /**\n * The same fields' ids, in the same order: what a record template binds as\n * `{{item.<id>}}` (AGL-3475). Absent where the reader did not see them.\n */\n fieldIds?: string[]\n}\n\nexport interface AiInventoryCollection {\n id: string\n name: string\n slug: string\n}\n\nexport interface AiInventoryScreen {\n id: string\n name: string\n slug: string\n layoutId: string | null\n /** A collection's entry template rather than a page of its own. */\n template: boolean\n}\n\n/**\n * The site's one theme, as a planner and the email rule read it: a short\n * description, the light scheme's colors by palette path, and the families\n * the theme loads.\n */\nexport interface AiInventoryTheme {\n summary: string[]\n colors: Record<string, string>\n fonts: string[]\n}\n\nexport interface AiSiteInventory {\n hostId: string\n components: AiInventoryComponent[]\n layouts: AiInventoryLayout[]\n templates: AiInventoryTemplate[]\n forms: AiInventoryForm[]\n datasets: AiInventoryDataset[]\n collections: AiInventoryCollection[]\n screens: AiInventoryScreen[]\n theme: AiInventoryTheme | null\n /** Kinds with more records than are listed. */\n truncated: AiInventoryKind[]\n}\n\n/** One inventory record, whatever kind it is. */\nexport type AiInventoryRow =\n | AiInventoryComponent\n | AiInventoryLayout\n | AiInventoryTemplate\n | AiInventoryForm\n | AiInventoryDataset\n | AiInventoryCollection\n | AiInventoryScreen\n\n/**\n * The rows of one kind. The inventory's fields are named after their kinds,\n * so this is a lookup rather than a switch, and it is the one place that\n * relies on the correspondence: the prompt block, the lookup tool and their\n * specs all read a kind through it.\n */\nexport function aiInventoryRows(\n inventory: AiSiteInventory,\n kind: AiInventoryKind,\n): readonly AiInventoryRow[] {\n return inventory[kind]\n}\n\n/** The heading one kind's lines are listed under, naming its columns in order. */\nexport const AI_INVENTORY_KIND_HEADINGS: Readonly<Record<AiInventoryKind, string>> = {\n components: 'Reusable components (id \\u00b7 name \\u00b7 props an instance fills)',\n layouts: 'Layouts (id \\u00b7 name)',\n templates: 'Templates (id \\u00b7 name \\u00b7 kind)',\n forms: 'Forms (id \\u00b7 name \\u00b7 fields)',\n datasets: 'Datasets (id \\u00b7 name \\u00b7 fields)',\n collections: 'Content collections (id \\u00b7 name \\u00b7 slug)',\n screens: 'Pages (id \\u00b7 name \\u00b7 slug)',\n}\n\n/**\n * One record as a line, in the order its heading names the columns. The one\n * renderer: the prompt block and the lookup tool's answer read alike, so a\n * row found by lookup is the same shape as a row that was listed.\n */\nexport function aiInventoryLine(kind: AiInventoryKind, row: AiInventoryRow): string {\n const head = `${row.id} \\u00b7 ${row.name}`\n switch (kind) {\n case 'components': {\n const props = Object.entries((row as AiInventoryComponent).props)\n .map(([name, type]) => `${name}:${type}`)\n .join(', ')\n return `${head} \\u00b7 ${props || 'no props'}`\n }\n case 'layouts':\n return head\n case 'templates':\n return `${head} \\u00b7 ${(row as AiInventoryTemplate).kind}`\n case 'forms':\n return `${head} \\u00b7 ${(row as AiInventoryForm).fields.join(', ')}`\n case 'datasets':\n return `${head} \\u00b7 ${(row as AiInventoryDataset).fields.join(', ')}`\n case 'collections':\n return `${head} \\u00b7 ${(row as AiInventoryCollection).slug}`\n case 'screens': {\n const screen = row as AiInventoryScreen\n return `${head} \\u00b7 ${screen.slug}${screen.template ? ' \\u00b7 entry template' : ''}`\n }\n }\n}\n\n/** Every word of a record a lookup searches: its line, which holds them all. */\nexport function aiInventoryHaystack(kind: AiInventoryKind, row: AiInventoryRow): string {\n return aiInventoryLine(kind, row).toLowerCase()\n}\n\n/** The addresses a site's home screen is stored under. */\nconst HOME_SLUGS = new Set(['', 'home', 'index'])\n\n/**\n * The site's home screens, by id (AGL-3056): a screen at the root or at\n * `home`, as a seeded site stores its first screen, or one named Home. A link\n * with nowhere to go is sent there, which is why the doctrine names them.\n */\nexport function aiHomeScreenIds(inventory: AiSiteInventory | null): string[] {\n return (inventory?.screens ?? [])\n .filter(\n (screen) =>\n !screen.template &&\n (HOME_SLUGS.has(screen.slug.trim().replace(/^\\/+|\\/+$/g, '').toLowerCase()) ||\n /^home(?:\\s?page)?$/i.test(screen.name.trim())),\n )\n .map((screen) => screen.id)\n}\n\n/** A site the org does not own; the inventory is never read for it. */\nexport class AiInventoryScopeError extends Error {\n constructor(hostId: string) {\n super(`host ${hostId} is not a site of the workspace that asked`)\n this.name = 'AiInventoryScopeError'\n }\n}\n\nexport function emptyAiSiteInventory(hostId: string): AiSiteInventory {\n return {\n hostId,\n components: [],\n layouts: [],\n templates: [],\n forms: [],\n datasets: [],\n collections: [],\n screens: [],\n theme: null,\n truncated: [],\n }\n}\n"],"names":["AI_SITE_INVENTORY_MAX_PER_KIND","AI_SITE_INVENTORY_LISTED_PER_KIND","AI_SITE_INVENTORY_MAX_CHARS","AI_INVENTORY_KINDS","aiInventoryRows","inventory","kind","AI_INVENTORY_KIND_HEADINGS","components","layouts","templates","forms","datasets","collections","screens","aiInventoryLine","row","head","id","name","props","Object","entries","map","type","join","fields","slug","screen","template","aiInventoryHaystack","toLowerCase","HOME_SLUGS","Set","aiHomeScreenIds","filter","has","trim","replace","test","AiInventoryScopeError","Error","hostId","emptyAiSiteInventory","theme","truncated"],"mappings":"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"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
1
2
|
/**
|
|
2
3
|
* @license
|
|
3
4
|
* Copyright 2026 Aglyn LLC
|
|
@@ -13,8 +14,7 @@
|
|
|
13
14
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
15
|
* See the License for the specific language governing permissions and
|
|
15
16
|
* limitations under the License.
|
|
16
|
-
*/ import {
|
|
17
|
-
import { checkEntitlement } from "@aglyn/aglyn/app-utils/plan-entitlements";
|
|
17
|
+
*/ import { checkEntitlement } from "@aglyn/aglyn/app-utils/plan-entitlements";
|
|
18
18
|
import { HOST_EVENT_LABELS, HOST_EVENT_TYPES } from "@aglyn/aglyn/app-utils/host-events";
|
|
19
19
|
/**
|
|
20
20
|
* A `workflow` job (AGL-2919): an automation drafted from a description, or an
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-workflow-job.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 { HostActionStepType } from '@aglyn/aglyn/app-utils/actions'\nimport { checkEntitlement } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport {\n HOST_EVENT_LABELS,\n HOST_EVENT_TYPES,\n type HostEventType,\n} from '@aglyn/aglyn/app-utils/host-events'\nimport type { AglynOrgBilling } from '@aglyn/aglyn/foundation/definitions/org-billing.types'\n\n/**\n * A `workflow` job (AGL-2919): an automation drafted from a description, or an\n * existing automation explained — what it does, or why one of its runs failed.\n *\n * One kind, three modes, because the three share everything a job carries:\n * the site, the gates, the meter and the automation vocabulary. `inputs.mode`\n * picks the mode, and a job created without one drafts.\n *\n * - `draft` writes a new automation to the site's Actions through the\n * workflows plugin's draft writer, OFF, built only from the triggers and\n * steps below that this workspace can run. What the description names that\n * the site does not have is left as a placeholder, never guessed.\n * - `explain` reads a saved automation — an action or a workflow — and\n * answers in plain words what it does.\n * - `diagnose` reads a saved automation and one of its FAILED runs, as the\n * run history recorded it, and answers why it failed and what to change.\n *\n * Nothing in any mode switches an automation on, runs one, or changes one.\n */\n\nexport const AI_WORKFLOW_JOB_MODES = ['draft', 'explain', 'diagnose'] as const\nexport type AiWorkflowJobMode = (typeof AI_WORKFLOW_JOB_MODES)[number]\n\n/** What an explained automation is: an action, or a workflow of function calls. */\nexport const AI_WORKFLOW_TARGET_TYPES = ['action', 'workflow'] as const\nexport type AiWorkflowTargetType = (typeof AI_WORKFLOW_TARGET_TYPES)[number]\n\nexport type AiWorkflowJobInputs =\n | { mode: 'draft' }\n | { mode: 'explain'; targetType: AiWorkflowTargetType; targetId: string }\n | { mode: 'diagnose'; targetType: AiWorkflowTargetType; targetId: string; runId: string }\n\nconst DOCUMENT_ID = /^[A-Za-z0-9_-]{1,128}$/\n\n/** A job's inputs read into its mode, or the sentence that says what is wrong with them. */\nexport function parseAiWorkflowJobInputs(\n inputs: Readonly<Record<string, unknown>> | null | undefined,\n): AiWorkflowJobInputs | string {\n const mode = inputs?.['mode'] ?? 'draft'\n if (!(AI_WORKFLOW_JOB_MODES as readonly unknown[]).includes(mode)) {\n return 'inputs.mode must be draft, explain or diagnose'\n }\n if (mode === 'draft') return { mode }\n const targetType = inputs?.['targetType']\n const targetId = inputs?.['targetId']\n if (\n !(AI_WORKFLOW_TARGET_TYPES as readonly unknown[]).includes(targetType) ||\n typeof targetId !== 'string' ||\n !DOCUMENT_ID.test(targetId)\n ) {\n return 'Pick the automation to explain'\n }\n const target = { targetType: targetType as AiWorkflowTargetType, targetId }\n if (mode === 'explain') return { mode, ...target }\n const runId = inputs?.['runId']\n if (typeof runId !== 'string' || !DOCUMENT_ID.test(runId)) return 'Pick the run to explain'\n return { mode: 'diagnose', ...target, runId }\n}\n\n// ── What a workspace can run ──────────────────────────────────────────────\n\n/** A part of the platform some triggers and steps need beyond the actions builder itself. */\nexport type AiAutomationNeed = 'crm' | 'webhooks' | 'bookings'\n\nexport type AiAutomationCapabilities = Readonly<Record<AiAutomationNeed, boolean>>\n\n/** Each need as a sentence names it. */\nexport const AI_AUTOMATION_NEED_LABELS: Readonly<Record<AiAutomationNeed, string>> = {\n crm: 'the CRM',\n webhooks: 'webhooks',\n bookings: 'bookings',\n}\n\n/**\n * What this workspace can run, from the entitlements the executor itself\n * reads before it runs a step: `crm` for the CRM steps, `webhooks` for a\n * webhook post, and `bookings` for the booking trigger's door.\n */\nexport function aiAutomationCapabilities(\n org: Partial<AglynOrgBilling> | Readonly<Record<string, unknown>> | null | undefined,\n): AiAutomationCapabilities {\n const billing = org as Partial<AglynOrgBilling> | null | undefined\n return {\n crm: checkEntitlement(billing, 'crm'),\n webhooks: checkEntitlement(billing, 'webhooks'),\n bookings: checkEntitlement(billing, 'bookings'),\n }\n}\n\n// ── The vocabulary ────────────────────────────────────────────────────────\n\n/**\n * The triggers a drafted automation may start on: every host event, which is\n * every event a server door emits. The on-page events (a click, a scroll) are\n * left out: they watch one element of one page, which a description cannot\n * name, and they belong to the page's own interactions.\n */\nexport const AI_AUTOMATION_TRIGGERS: readonly HostEventType[] = HOST_EVENT_TYPES\n\n/** The triggers only a door of a part of the platform emits. */\nexport const AI_AUTOMATION_TRIGGER_NEEDS: Readonly<Partial<Record<HostEventType, AiAutomationNeed>>> = {\n booking: 'bookings',\n taskCompleted: 'crm',\n contactStageChanged: 'crm',\n dealStageChanged: 'crm',\n dealWon: 'crm',\n dealLost: 'crm',\n}\n\n/**\n * The steps a drafted automation may take: the server steps and the flow\n * steps, which act on the person an event names. The on-page steps — show an\n * element, open a menu, run a script — are left out for the reason the\n * on-page triggers are: they need a selector on a page a description cannot\n * name.\n */\nexport const AI_AUTOMATION_STEP_TYPES = [\n 'sendEmail',\n 'notifyAdmins',\n 'enrollList',\n 'assignCampaign',\n 'runWorkflow',\n 'datasetAppend',\n 'updateDataset',\n 'webhookPost',\n 'siteAlert',\n 'wait',\n 'waitForEvent',\n 'exitFlow',\n 'setContactStage',\n 'addContactTag',\n 'assignContactOwner',\n 'createCrmTask',\n 'logCrmActivity',\n] as const satisfies readonly HostActionStepType[]\n\nexport type AiAutomationStepType = (typeof AI_AUTOMATION_STEP_TYPES)[number]\n\n/** The steps the executor refuses without a part of the platform. */\nexport const AI_AUTOMATION_STEP_NEEDS: Readonly<Partial<Record<AiAutomationStepType, AiAutomationNeed>>> = {\n webhookPost: 'webhooks',\n setContactStage: 'crm',\n addContactTag: 'crm',\n assignContactOwner: 'crm',\n createCrmTask: 'crm',\n logCrmActivity: 'crm',\n}\n\n/** The trigger's name as the Actions editor labels it. */\nexport function aiAutomationTriggerLabel(event: string): string {\n return HOST_EVENT_LABELS[event as HostEventType] ?? event\n}\n\n// ── A description the vocabulary cannot build ─────────────────────────────\n\n/**\n * Why a description was answered with no automation. Each is a fixed reason\n * with a fixed sentence, so what a person reads is never the model's own\n * account of itself.\n */\nexport const AI_AUTOMATION_UNSUPPORTED = [\n 'not-an-automation',\n 'no-trigger',\n 'no-step',\n 'needs-crm',\n 'needs-webhooks',\n 'needs-bookings',\n] as const\n\nexport type AiAutomationUnsupported = (typeof AI_AUTOMATION_UNSUPPORTED)[number]\n\n/** The need an unsupported reason names, for the reasons that name one. */\nexport const AI_AUTOMATION_UNSUPPORTED_NEED: Readonly<\n Partial<Record<AiAutomationUnsupported, AiAutomationNeed>>\n> = {\n 'needs-crm': 'crm',\n 'needs-webhooks': 'webhooks',\n 'needs-bookings': 'bookings',\n}\n\nexport const AI_AUTOMATION_UNSUPPORTED_COPY: Readonly<Record<AiAutomationUnsupported, string>> = {\n 'not-an-automation':\n 'This description is not an automation — something that happens, and what to do then — so nothing was drafted.',\n 'no-trigger':\n 'Nothing on the platform can start an automation like this one, so nothing was drafted. Describe it as something that happens on your site, such as a form being submitted.',\n 'no-step': 'An automation cannot do what this description asks yet, so nothing was drafted.',\n 'needs-crm':\n 'This automation needs the CRM, which this workspace’s plan does not include, so nothing was drafted.',\n 'needs-webhooks':\n 'This automation needs webhooks, which this workspace’s plan does not include, so nothing was drafted.',\n 'needs-bookings':\n 'This automation needs bookings, which this workspace’s plan does not include, so nothing was drafted.',\n}\n\n// ── What a job tells a person ─────────────────────────────────────────────\n\n/** The resource the workflows plugin writes a drafted automation under. */\nexport const AI_AUTOMATION_RESOURCE = 'automation'\n\nexport const AI_WORKFLOW_NO_SITE_COPY = 'This automation job names a site this workspace does not have.'\nexport const AI_WORKFLOW_UNAVAILABLE_COPY = 'Automations are not available on this site.'\nexport const AI_WORKFLOW_GONE_COPY = 'That automation no longer exists.'\nexport const AI_WORKFLOW_RUN_GONE_COPY = 'That run is no longer in the automation’s history.'\nexport const AI_WORKFLOW_RUN_NOT_FAILED_COPY = 'That run did not fail, so there is nothing to explain.'\nexport const AI_WORKFLOW_NO_DRAFT_COPY =\n 'The AI could not draft an automation from this description. Try describing what happens, and what should happen then.'\nexport const AI_WORKFLOW_NO_EXPLANATION_COPY =\n 'The AI could not explain this automation. Try again.'\nexport const AI_WORKFLOW_SAVE_FAILURE_COPY =\n 'The automation was drafted but could not be saved. Try the job again.'\n"],"names":["checkEntitlement","HOST_EVENT_LABELS","HOST_EVENT_TYPES","AI_WORKFLOW_JOB_MODES","AI_WORKFLOW_TARGET_TYPES","DOCUMENT_ID","parseAiWorkflowJobInputs","inputs","mode","includes","targetType","targetId","test","target","runId","AI_AUTOMATION_NEED_LABELS","crm","webhooks","bookings","aiAutomationCapabilities","org","billing","AI_AUTOMATION_TRIGGERS","AI_AUTOMATION_TRIGGER_NEEDS","booking","taskCompleted","contactStageChanged","dealStageChanged","dealWon","dealLost","AI_AUTOMATION_STEP_TYPES","AI_AUTOMATION_STEP_NEEDS","webhookPost","setContactStage","addContactTag","assignContactOwner","createCrmTask","logCrmActivity","aiAutomationTriggerLabel","event","AI_AUTOMATION_UNSUPPORTED","AI_AUTOMATION_UNSUPPORTED_NEED","AI_AUTOMATION_UNSUPPORTED_COPY","AI_AUTOMATION_RESOURCE","AI_WORKFLOW_NO_SITE_COPY","AI_WORKFLOW_UNAVAILABLE_COPY","AI_WORKFLOW_GONE_COPY","AI_WORKFLOW_RUN_GONE_COPY","AI_WORKFLOW_RUN_NOT_FAILED_COPY","AI_WORKFLOW_NO_DRAFT_COPY","AI_WORKFLOW_NO_EXPLANATION_COPY","AI_WORKFLOW_SAVE_FAILURE_COPY"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-workflow-job.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 { checkEntitlement } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport {\n HOST_EVENT_LABELS,\n HOST_EVENT_TYPES,\n type HostEventType,\n} from '@aglyn/aglyn/app-utils/host-events'\nimport type { AglynOrgBilling } from '@aglyn/aglyn/foundation/definitions/org-billing.types'\nimport type { AiAutomationFormatStepType } from './ai-automation-format'\n\n/**\n * A `workflow` job (AGL-2919): an automation drafted from a description, or an\n * existing automation explained — what it does, or why one of its runs failed.\n *\n * One kind, three modes, because the three share everything a job carries:\n * the site, the gates, the meter and the automation vocabulary. `inputs.mode`\n * picks the mode, and a job created without one drafts.\n *\n * - `draft` writes a new automation to the site's Actions through the\n * workflows plugin's draft writer, OFF, built only from the triggers and\n * steps below that this workspace can run. What the description names that\n * the site does not have is left as a placeholder, never guessed.\n * - `explain` reads a saved automation — an action or a workflow — and\n * answers in plain words what it does.\n * - `diagnose` reads a saved automation and one of its FAILED runs, as the\n * run history recorded it, and answers why it failed and what to change.\n *\n * Nothing in any mode switches an automation on, runs one, or changes one.\n */\n\nexport const AI_WORKFLOW_JOB_MODES = ['draft', 'explain', 'diagnose'] as const\nexport type AiWorkflowJobMode = (typeof AI_WORKFLOW_JOB_MODES)[number]\n\n/** What an explained automation is: an action, or a workflow of function calls. */\nexport const AI_WORKFLOW_TARGET_TYPES = ['action', 'workflow'] as const\nexport type AiWorkflowTargetType = (typeof AI_WORKFLOW_TARGET_TYPES)[number]\n\nexport type AiWorkflowJobInputs =\n | { mode: 'draft' }\n | { mode: 'explain'; targetType: AiWorkflowTargetType; targetId: string }\n | { mode: 'diagnose'; targetType: AiWorkflowTargetType; targetId: string; runId: string }\n\nconst DOCUMENT_ID = /^[A-Za-z0-9_-]{1,128}$/\n\n/** A job's inputs read into its mode, or the sentence that says what is wrong with them. */\nexport function parseAiWorkflowJobInputs(\n inputs: Readonly<Record<string, unknown>> | null | undefined,\n): AiWorkflowJobInputs | string {\n const mode = inputs?.['mode'] ?? 'draft'\n if (!(AI_WORKFLOW_JOB_MODES as readonly unknown[]).includes(mode)) {\n return 'inputs.mode must be draft, explain or diagnose'\n }\n if (mode === 'draft') return { mode }\n const targetType = inputs?.['targetType']\n const targetId = inputs?.['targetId']\n if (\n !(AI_WORKFLOW_TARGET_TYPES as readonly unknown[]).includes(targetType) ||\n typeof targetId !== 'string' ||\n !DOCUMENT_ID.test(targetId)\n ) {\n return 'Pick the automation to explain'\n }\n const target = { targetType: targetType as AiWorkflowTargetType, targetId }\n if (mode === 'explain') return { mode, ...target }\n const runId = inputs?.['runId']\n if (typeof runId !== 'string' || !DOCUMENT_ID.test(runId)) return 'Pick the run to explain'\n return { mode: 'diagnose', ...target, runId }\n}\n\n// ── What a workspace can run ──────────────────────────────────────────────\n\n/** A part of the platform some triggers and steps need beyond the actions builder itself. */\nexport type AiAutomationNeed = 'crm' | 'webhooks' | 'bookings'\n\nexport type AiAutomationCapabilities = Readonly<Record<AiAutomationNeed, boolean>>\n\n/** Each need as a sentence names it. */\nexport const AI_AUTOMATION_NEED_LABELS: Readonly<Record<AiAutomationNeed, string>> = {\n crm: 'the CRM',\n webhooks: 'webhooks',\n bookings: 'bookings',\n}\n\n/**\n * What this workspace can run, from the entitlements the executor itself\n * reads before it runs a step: `crm` for the CRM steps, `webhooks` for a\n * webhook post, and `bookings` for the booking trigger's door.\n */\nexport function aiAutomationCapabilities(\n org: Partial<AglynOrgBilling> | Readonly<Record<string, unknown>> | null | undefined,\n): AiAutomationCapabilities {\n const billing = org as Partial<AglynOrgBilling> | null | undefined\n return {\n crm: checkEntitlement(billing, 'crm'),\n webhooks: checkEntitlement(billing, 'webhooks'),\n bookings: checkEntitlement(billing, 'bookings'),\n }\n}\n\n// ── The vocabulary ────────────────────────────────────────────────────────\n\n/**\n * The triggers a drafted automation may start on: every host event, which is\n * every event a server door emits. The on-page events (a click, a scroll) are\n * left out: they watch one element of one page, which a description cannot\n * name, and they belong to the page's own interactions.\n */\nexport const AI_AUTOMATION_TRIGGERS: readonly HostEventType[] = HOST_EVENT_TYPES\n\n/** The triggers only a door of a part of the platform emits. */\nexport const AI_AUTOMATION_TRIGGER_NEEDS: Readonly<Partial<Record<HostEventType, AiAutomationNeed>>> = {\n booking: 'bookings',\n taskCompleted: 'crm',\n contactStageChanged: 'crm',\n dealStageChanged: 'crm',\n dealWon: 'crm',\n dealLost: 'crm',\n}\n\n/**\n * The steps a drafted automation may take: the server steps and the flow\n * steps, which act on the person an event names. The on-page steps — show an\n * element, open a menu, run a script — are left out for the reason the\n * on-page triggers are: they need a selector on a page a description cannot\n * name.\n */\nexport const AI_AUTOMATION_STEP_TYPES = [\n 'sendEmail',\n 'notifyAdmins',\n 'enrollList',\n 'assignCampaign',\n 'runWorkflow',\n 'datasetAppend',\n 'updateDataset',\n 'webhookPost',\n 'siteAlert',\n 'wait',\n 'waitForEvent',\n 'exitFlow',\n 'setContactStage',\n 'addContactTag',\n 'assignContactOwner',\n 'createCrmTask',\n 'logCrmActivity',\n] as const satisfies readonly AiAutomationFormatStepType[]\n\nexport type AiAutomationStepType = (typeof AI_AUTOMATION_STEP_TYPES)[number]\n\n/** The steps the executor refuses without a part of the platform. */\nexport const AI_AUTOMATION_STEP_NEEDS: Readonly<Partial<Record<AiAutomationStepType, AiAutomationNeed>>> = {\n webhookPost: 'webhooks',\n setContactStage: 'crm',\n addContactTag: 'crm',\n assignContactOwner: 'crm',\n createCrmTask: 'crm',\n logCrmActivity: 'crm',\n}\n\n/** The trigger's name as the Actions editor labels it. */\nexport function aiAutomationTriggerLabel(event: string): string {\n return HOST_EVENT_LABELS[event as HostEventType] ?? event\n}\n\n// ── A description the vocabulary cannot build ─────────────────────────────\n\n/**\n * Why a description was answered with no automation. Each is a fixed reason\n * with a fixed sentence, so what a person reads is never the model's own\n * account of itself.\n */\nexport const AI_AUTOMATION_UNSUPPORTED = [\n 'not-an-automation',\n 'no-trigger',\n 'no-step',\n 'needs-crm',\n 'needs-webhooks',\n 'needs-bookings',\n] as const\n\nexport type AiAutomationUnsupported = (typeof AI_AUTOMATION_UNSUPPORTED)[number]\n\n/** The need an unsupported reason names, for the reasons that name one. */\nexport const AI_AUTOMATION_UNSUPPORTED_NEED: Readonly<\n Partial<Record<AiAutomationUnsupported, AiAutomationNeed>>\n> = {\n 'needs-crm': 'crm',\n 'needs-webhooks': 'webhooks',\n 'needs-bookings': 'bookings',\n}\n\nexport const AI_AUTOMATION_UNSUPPORTED_COPY: Readonly<Record<AiAutomationUnsupported, string>> = {\n 'not-an-automation':\n 'This description is not an automation — something that happens, and what to do then — so nothing was drafted.',\n 'no-trigger':\n 'Nothing on the platform can start an automation like this one, so nothing was drafted. Describe it as something that happens on your site, such as a form being submitted.',\n 'no-step': 'An automation cannot do what this description asks yet, so nothing was drafted.',\n 'needs-crm':\n 'This automation needs the CRM, which this workspace’s plan does not include, so nothing was drafted.',\n 'needs-webhooks':\n 'This automation needs webhooks, which this workspace’s plan does not include, so nothing was drafted.',\n 'needs-bookings':\n 'This automation needs bookings, which this workspace’s plan does not include, so nothing was drafted.',\n}\n\n// ── What a job tells a person ─────────────────────────────────────────────\n\n/** The resource the workflows plugin writes a drafted automation under. */\nexport const AI_AUTOMATION_RESOURCE = 'automation'\n\nexport const AI_WORKFLOW_NO_SITE_COPY = 'This automation job names a site this workspace does not have.'\nexport const AI_WORKFLOW_UNAVAILABLE_COPY = 'Automations are not available on this site.'\nexport const AI_WORKFLOW_GONE_COPY = 'That automation no longer exists.'\nexport const AI_WORKFLOW_RUN_GONE_COPY = 'That run is no longer in the automation’s history.'\nexport const AI_WORKFLOW_RUN_NOT_FAILED_COPY = 'That run did not fail, so there is nothing to explain.'\nexport const AI_WORKFLOW_NO_DRAFT_COPY =\n 'The AI could not draft an automation from this description. Try describing what happens, and what should happen then.'\nexport const AI_WORKFLOW_NO_EXPLANATION_COPY =\n 'The AI could not explain this automation. Try again.'\nexport const AI_WORKFLOW_SAVE_FAILURE_COPY =\n 'The automation was drafted but could not be saved. Try the job again.'\n"],"names":["checkEntitlement","HOST_EVENT_LABELS","HOST_EVENT_TYPES","AI_WORKFLOW_JOB_MODES","AI_WORKFLOW_TARGET_TYPES","DOCUMENT_ID","parseAiWorkflowJobInputs","inputs","mode","includes","targetType","targetId","test","target","runId","AI_AUTOMATION_NEED_LABELS","crm","webhooks","bookings","aiAutomationCapabilities","org","billing","AI_AUTOMATION_TRIGGERS","AI_AUTOMATION_TRIGGER_NEEDS","booking","taskCompleted","contactStageChanged","dealStageChanged","dealWon","dealLost","AI_AUTOMATION_STEP_TYPES","AI_AUTOMATION_STEP_NEEDS","webhookPost","setContactStage","addContactTag","assignContactOwner","createCrmTask","logCrmActivity","aiAutomationTriggerLabel","event","AI_AUTOMATION_UNSUPPORTED","AI_AUTOMATION_UNSUPPORTED_NEED","AI_AUTOMATION_UNSUPPORTED_COPY","AI_AUTOMATION_RESOURCE","AI_WORKFLOW_NO_SITE_COPY","AI_WORKFLOW_UNAVAILABLE_COPY","AI_WORKFLOW_GONE_COPY","AI_WORKFLOW_RUN_GONE_COPY","AI_WORKFLOW_RUN_NOT_FAILED_COPY","AI_WORKFLOW_NO_DRAFT_COPY","AI_WORKFLOW_NO_EXPLANATION_COPY","AI_WORKFLOW_SAVE_FAILURE_COPY"],"mappings":"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"}
|
|
@@ -131,6 +131,29 @@ export interface AiCatalogProvider {
|
|
|
131
131
|
*/
|
|
132
132
|
subprocessor?: AiProviderSubprocessorWording;
|
|
133
133
|
}
|
|
134
|
+
/** How the data cell words CRM assistance: what a record sends, and what never leaves. */
|
|
135
|
+
export interface AnthropicCrmAssistanceWording {
|
|
136
|
+
record: string;
|
|
137
|
+
never: string;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* CRM assistance as `/legal/subprocessors` publishes it today: the disclosed
|
|
141
|
+
* fields of a record, with addresses and numbers in its text replaced. This
|
|
142
|
+
* is what production sends while `release_crm_assist_whole_record` is off.
|
|
143
|
+
*/
|
|
144
|
+
export declare const ANTHROPIC_CRM_ASSISTANCE_DISCLOSED: AnthropicCrmAssistanceWording;
|
|
145
|
+
/**
|
|
146
|
+
* CRM assistance once the whole record is sent (AGL-3520): the open-ended
|
|
147
|
+
* wording the Subprocessors row and Privacy Policy section 2 must be
|
|
148
|
+
* republished with BEFORE `release_crm_assist_whole_record` is turned on.
|
|
149
|
+
* Not published: the catalog row below carries
|
|
150
|
+
* {@link ANTHROPIC_CRM_ASSISTANCE_DISCLOSED} until that republication, when
|
|
151
|
+
* this takes its place and `publishedOn` moves to the change-log date
|
|
152
|
+
* (`docs/drafts/agl-3520-crm-assistance-whole-record-disclosure.md`).
|
|
153
|
+
*/
|
|
154
|
+
export declare const ANTHROPIC_CRM_ASSISTANCE_WHOLE_RECORD: AnthropicCrmAssistanceWording;
|
|
155
|
+
/** The Anthropic row's data cell, with CRM assistance worded as `crm` words it. */
|
|
156
|
+
export declare function anthropicDataReceived(crm: AnthropicCrmAssistanceWording): string;
|
|
134
157
|
/**
|
|
135
158
|
* The providers the catalog carries models for, with the name a settings
|
|
136
159
|
* form shows. A form offers providers from here for the reason it offers
|
|
@@ -43,6 +43,34 @@
|
|
|
43
43
|
};
|
|
44
44
|
const ANTHROPIC = 'anthropic';
|
|
45
45
|
const OPENAI_COMPATIBLE = 'openai-compatible';
|
|
46
|
+
/**
|
|
47
|
+
* The Anthropic row's data cell up to its CRM sentence: every door but CRM
|
|
48
|
+
* assistance, worded once for both of the CRM sentences below.
|
|
49
|
+
*/ const ANTHROPIC_DATA_RECEIVED_LEAD = "What the user submits — a question, instruction or brief, with the earlier messages of the same Assist conversation — and the content of the element, post, section or page being worked on, with the generated response. On Pro and above, the organization's name and the console route and host travel with an Assist question. For an edit the assistant proposes in the besigner, an outline of the open page, component or layout: element and component ids, layer names, shortened setting values and the selected element's styles. For a generation job, the site inventory: the names and addresses of its pages and collections, the names of its components, layouts, templates, forms and datasets with their prop and field names, and the theme's summary, colors and fonts. For a theme change, the site's current theme settings and brand colors as hex values, from the organization's brand settings, the site logo in the media library or a public page the brief links to. For features that review or write search information, the text and structure of the pages concerned. For product copy, the store's name, the product's name, type, description, tags, options and search listing, the store's category names, and the product's first media-library photo as a copy at most 768 px on its longer edge with its metadata stripped; never another media file, a price, stock, an order or a customer. For products, categories and discounts proposed from a brief, the store's name and its existing category names. For an automation drafted from a brief, which of CRM, webhooks and bookings the plan includes; for an explanation, the automation's outline (trigger, conditions, each step's text with the names of the lists, campaigns, workflows, webhooks and datasets it uses and whether each exists, and a workflow's function names and expressions) and, for a failed run, its time, steps and recorded errors, with email addresses removed and never the triggering event's data. For an insight, the figure reports available and aggregate tables: traffic with top page paths, referrers and campaign tags; form views and submissions; revenue, orders and best-selling product names; bookings by service; campaign subjects with delivery, open and click rates; A/B test and variant conversions; and, for a dataset the member can see, field names and types, record and fill counts, number ranges and totals grouped by a value at least three records share. Email addresses and phone numbers are removed, and no individual record is sent. ";
|
|
50
|
+
/** What every CRM door adds after its record sentence: drafts and imports. */ const ANTHROPIC_DATA_RECEIVED_CRM_DRAFTS_AND_IMPORTS = "An email draft adds the request and the record's merge field names; an import sends the field names and types and each column's header and value kind, never a row.";
|
|
51
|
+
/**
|
|
52
|
+
* CRM assistance as `/legal/subprocessors` publishes it today: the disclosed
|
|
53
|
+
* fields of a record, with addresses and numbers in its text replaced. This
|
|
54
|
+
* is what production sends while `release_crm_assist_whole_record` is off.
|
|
55
|
+
*/ export const ANTHROPIC_CRM_ASSISTANCE_DISCLOSED = {
|
|
56
|
+
record: "For CRM assistance, the opened contact, company, deal or lead as the CRM shows it: its name and, by kind, job title, company, lifecycle stage, tags, capture history and counts, domain, industry, headcount, pipeline stages, status, amount, dates, lost reason, parties and lead status, with its notes, newest timeline entries and open tasks and deals. Email addresses and phone numbers in that text are replaced, and no email, phone or postal field, marketing consent, custom field value, team member or record id is sent.",
|
|
57
|
+
never: 'No account identifiers, email addresses or authentication tokens.'
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* CRM assistance once the whole record is sent (AGL-3520): the open-ended
|
|
61
|
+
* wording the Subprocessors row and Privacy Policy section 2 must be
|
|
62
|
+
* republished with BEFORE `release_crm_assist_whole_record` is turned on.
|
|
63
|
+
* Not published: the catalog row below carries
|
|
64
|
+
* {@link ANTHROPIC_CRM_ASSISTANCE_DISCLOSED} until that republication, when
|
|
65
|
+
* this takes its place and `publishedOn` moves to the change-log date
|
|
66
|
+
* (`docs/drafts/agl-3520-crm-assistance-whole-record-disclosure.md`).
|
|
67
|
+
*/ export const ANTHROPIC_CRM_ASSISTANCE_WHOLE_RECORD = {
|
|
68
|
+
record: 'For CRM assistance, the opened record as the CRM shows it, with all of its standard and custom fields, including contact details, notes, timeline and related records.',
|
|
69
|
+
never: 'No account identifiers or authentication tokens, and no email address outside an opened CRM record.'
|
|
70
|
+
};
|
|
71
|
+
/** The Anthropic row's data cell, with CRM assistance worded as `crm` words it. */ export function anthropicDataReceived(crm) {
|
|
72
|
+
return `${ANTHROPIC_DATA_RECEIVED_LEAD}${crm.record} ${ANTHROPIC_DATA_RECEIVED_CRM_DRAFTS_AND_IMPORTS} ${crm.never}`;
|
|
73
|
+
}
|
|
46
74
|
/**
|
|
47
75
|
* The providers the catalog carries models for, with the name a settings
|
|
48
76
|
* form shows. A form offers providers from here for the reason it offers
|
|
@@ -58,7 +86,7 @@ const OPENAI_COMPATIBLE = 'openai-compatible';
|
|
|
58
86
|
purpose: "AI-assisted features in the console and the site editor: the Aglyn Assist helper, including changes it proposes to a page, component, or layout open in the editor; editor assistance (rewriting element copy, drafting blog bodies, generating a section layout); and AI generation, which creates drafts and proposals for a customer's site from a brief or from what the site already holds, such as copy, layouts, templates, automations, search titles and descriptions, theme changes, product descriptions and tags, draft products, and store categories and discounts; explanations of a site's automations, including why an automation's run failed; AI insights, which answer a customer's questions about its own figures, such as site traffic, sales, bookings, forms, campaigns, A/B tests and datasets, and write a weekly summary of them for members who ask for one; and AI assistance in the CRM, which summarizes a contact, company, deal or lead for a user and suggests a next step, drafts a one-to-one email for the user to review and send, and suggests how the columns of a spreadsheet the user imports match CRM fields",
|
|
59
87
|
publishedOn: '2026-09-17',
|
|
60
88
|
reason: "Reached through the AI plugin's Anthropic adapter (`libs/plugins/ai/src/lib/providers/anthropic.ts`) by the doors that call the AI runtime. `libs/plugins/ai/src/lib/server/assist-chat.ts` is gated by `release_assist` AND the key, and a generation job's text step by `release_ai_generative`; `libs/plugins/ai/src/lib/server/ai-assist.ts` carries NO release flag, so setting `{apiKeyEnv}` in production is by itself what starts this flow. `assist-anthropic-subprocessor-gate.spec.ts` holds the per-door detail and is the deeper guard for this one vendor.",
|
|
61
|
-
dataReceived:
|
|
89
|
+
dataReceived: anthropicDataReceived(ANTHROPIC_CRM_ASSISTANCE_DISCLOSED)
|
|
62
90
|
}
|
|
63
91
|
},
|
|
64
92
|
{
|