@aglyn/plugins-ai 1.0.0-beta.232 → 1.0.0-beta.233

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.
@@ -85,6 +85,16 @@ export interface AiPlanCapabilities {
85
85
  * SITE plan's worst case leaves room for. Absent for every other job.
86
86
  */
87
87
  freeSitePages?: number;
88
+ /**
89
+ * Whether this job's pages are written in the compact layout language
90
+ * (AGL-3660), whose compiler draws a section's repeated items itself: as
91
+ * instances of a component the plan places there, or, with none, in their
92
+ * compact form, a title over its text, which is smaller than a block rule 1
93
+ * calls a repeat. A plan is then not asked for a component for a section's
94
+ * repeated items (AGL-3616); a section two pages share still is one. The
95
+ * plan step sets it for a `build` job. Absent is `false`.
96
+ */
97
+ repeatsCompiled?: boolean;
88
98
  }
89
99
  /** What a job of one kind builds from its own plan, where it builds only some creations. */
90
100
  export interface AiPlanJobScope {
@@ -115,6 +125,12 @@ export declare function aiCreationNoun(kind: AiBuildPlanCreateKind): string;
115
125
  * planned as four sections of one item.
116
126
  */
117
127
  export declare const AI_PLAN_INLINE_SENTENCE = "This workspace keeps no reusable components: draw a list's repeated items in one section.";
128
+ /**
129
+ * The sentence a build's request states (AGL-3616): its pages are written in
130
+ * the layout language, which draws a section's repeated items itself, so the
131
+ * plan counts them in the section's `items` and needs no component for them.
132
+ */
133
+ export declare const AI_PLAN_COMPILED_REPEATS_SENTENCE = "A section's repeated items need no component here: count them in its items and the page draws them. A section two pages share is still one component.";
118
134
  /**
119
135
  * The sentence a request states when the site has no saved form and this job
120
136
  * may create none (AGL-3596): the plan places no form, rather than one drawn
@@ -63,6 +63,11 @@ const ALLOWED = {
63
63
  * each item, which is how a live About page's four practice areas came to be
64
64
  * planned as four sections of one item.
65
65
  */ export const AI_PLAN_INLINE_SENTENCE = "This workspace keeps no reusable components: draw a list's repeated items in one section.";
66
+ /**
67
+ * The sentence a build's request states (AGL-3616): its pages are written in
68
+ * the layout language, which draws a section's repeated items itself, so the
69
+ * plan counts them in the section's `items` and needs no component for them.
70
+ */ export const AI_PLAN_COMPILED_REPEATS_SENTENCE = "A section's repeated items need no component here: count them in its items and the page draws them. A section two pages share is still one component.";
66
71
  /**
67
72
  * The sentence a request states when the site has no saved form and this job
68
73
  * may create none (AGL-3596): the plan places no form, rather than one drawn
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-plan-capabilities.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 {\n AI_BUILD_PLAN_CREATE_KINDS,\n AI_BUILD_PLAN_CREATION_NOUNS,\n type AiBuildPlan,\n type AiBuildPlanCreateKind,\n} from './ai-build-plan'\n\n/**\n * What a plan may create on one site (AGL-3030): the workspace's plan and the\n * site's counts against its limits, narrowed to what the job itself builds.\n *\n * A plan that names a creation nothing can make is a dead end the member pays\n * for twice: once for the plan, and again for the plan they describe after\n * being told to make the creation by hand — which, on a workspace whose plan\n * does not include it, they cannot. So the plan step is TOLD what may be\n * created before it answers, and the plan rules refuse a creation outside it\n * with the one re-ask every other rule gets.\n *\n * ── Where the doctrine builds inline ─────────────────────────────────────\n *\n * The building doctrine makes a repeat a reusable component and a form a\n * saved form placed by id. A workspace whose plan keeps no reusable\n * components draws a list's repeated items in one section instead\n * (AGL-3071); `reusableComponents` is the one flag every validator reads for\n * that.\n *\n * A form is never drawn inline (AGL-3596): it is a saved form, counted\n * against the site's form allowance like one a member makes. Where the site\n * has none to place and may create none, the page places no form at all.\n *\n * ── Kept out of the cached prefix ────────────────────────────────────────\n *\n * The capabilities are per workspace and per site, so they ride the plan's\n * USER turn, never a system block: the doctrine's cached prefix stays one\n * entry for the platform. The doctrine states the rule once — create only\n * what the request says may be created, and build inline where it says the\n * workspace keeps no reusable components — and the request states the facts.\n *\n * This module imports nothing at runtime beyond the plan's own model, so the\n * validators, the plan step, the doors and their specs read one vocabulary.\n * What a workspace's plan and counts come to is read by\n * `readAiPlanCapabilities` in `jobs/ai-job-drafts.ts`, beside the band\n * arithmetic the create route and the draft writer share.\n */\n\n/** One creation kind, as a job on this site may make it or not. */\nexport interface AiPlanCreation {\n allowed: boolean\n /** How many more the site may hold; `null` when the plan counts none. */\n left: number | null\n /**\n * Why not, as a clause a sentence completes: \"this workspace's plan does\n * not include reusable components\". `null` when allowed.\n */\n reason: string | null\n}\n\nexport interface AiPlanCapabilities {\n /**\n * Whether the workspace keeps reusable components. Where it does not, a\n * list's repeated items are drawn in one section. Forms are not read off\n * it: whether one may be created is `create.form` (AGL-3596).\n */\n reusableComponents: boolean\n /** Every creation kind a plan may name, and whether this job may make one here. */\n create: Readonly<Record<AiBuildPlanCreateKind, AiPlanCreation>>\n /**\n * Whether the workspace spends the Free taste, whose monthly credits are a\n * wall (AGL-2925): a plan is then held to the sections the wall's worst\n * case pays for (AGL-3070). Absent is `false`.\n */\n freeTaste?: boolean\n /**\n * A Free workspace's site scaffold (AGL-3594): the most pages its plan may\n * build, which the plan step sets for a `site` job on the Free taste. The\n * Free wall then holds the plan to that many pages and to the sections a\n * SITE plan's worst case leaves room for. Absent for every other job.\n */\n freeSitePages?: number\n}\n\n/** What a job of one kind builds from its own plan, where it builds only some creations. */\nexport interface AiPlanJobScope {\n /** The job as a refusal names it: \"a page job\". */\n noun: string\n /** The creation kinds the job builds itself; any other is refused before the plan is kept. */\n creates: readonly AiBuildPlanCreateKind[]\n}\n\nconst ALLOWED: AiPlanCreation = { allowed: true, left: null, reason: null }\n\n/**\n * Capabilities that restrict nothing: what a plan is held to when no\n * workspace was read — an org-level job, an eval, a spec about something\n * else. The doctrine applies whole.\n */\nexport function aiUnrestrictedPlanCapabilities(): AiPlanCapabilities {\n return {\n reusableComponents: true,\n create: Object.fromEntries(\n AI_BUILD_PLAN_CREATE_KINDS.map((kind) => [kind, ALLOWED]),\n ) as Record<AiBuildPlanCreateKind, AiPlanCreation>,\n }\n}\n\n/**\n * The workspace's capabilities narrowed to what a job of one kind builds.\n * A creation the workspace itself cannot make keeps the workspace's reason,\n * which is the one a member can act on; one the job does not build says so.\n */\nexport function aiPlanCapabilitiesForJob(\n capabilities: AiPlanCapabilities,\n scope: AiPlanJobScope | null | undefined,\n): AiPlanCapabilities {\n if (!scope) return capabilities\n const create = { ...capabilities.create }\n for (const kind of AI_BUILD_PLAN_CREATE_KINDS) {\n if (!create[kind].allowed || scope.creates.includes(kind)) continue\n create[kind] = { allowed: false, left: null, reason: `${scope.noun} does not build one` }\n }\n return { ...capabilities, create }\n}\n\n/** \"a layout\", \"an email design\": a creation kind with its article. */\nexport function aiCreationNoun(kind: AiBuildPlanCreateKind): string {\n const noun = AI_BUILD_PLAN_CREATION_NOUNS[kind].noun\n return `${/^[aeiou]/.test(noun) ? 'an' : 'a'} ${noun}`\n}\n\n/**\n * The sentence a request states when the workspace keeps no reusable\n * components. It says a list's repeated items go in one section (AGL-3071):\n * \"a repeated item in its own section\" reads just as well as a section for\n * each item, which is how a live About page's four practice areas came to be\n * planned as four sections of one item.\n */\nexport const AI_PLAN_INLINE_SENTENCE =\n \"This workspace keeps no reusable components: draw a list's repeated items in one section.\"\n\n/**\n * The sentence a request states when the site has no saved form and this job\n * may create none (AGL-3596): the plan places no form, rather than one drawn\n * on the page with no saved form behind it.\n */\nexport const AI_PLAN_NO_FORM_SENTENCE =\n 'This site has no saved form and may create none: plan no form, and no section that collects answers.'\n\n/**\n * What the plan step's user turn says the job may create here: one line a\n * kind, and the inline sentence where the workspace keeps no reusable\n * components. Per workspace and per site, so it is never a system block.\n */\nexport function aiPlanCapabilityLines(capabilities: AiPlanCapabilities): string[] {\n const lines = ['What this job may create on this site:']\n for (const kind of AI_BUILD_PLAN_CREATE_KINDS) {\n const entry = capabilities.create[kind]\n const noun = AI_BUILD_PLAN_CREATION_NOUNS[kind].noun\n if (entry.allowed) {\n lines.push(`- ${noun}: yes${entry.left === null ? '' : `, ${entry.left} more`}`)\n } else {\n lines.push(`- ${noun}: no, because ${entry.reason ?? 'it cannot be made here'}`)\n }\n }\n if (!capabilities.reusableComponents) lines.push(AI_PLAN_INLINE_SENTENCE)\n return lines\n}\n\n/**\n * Whether a plan on this site can place a saved form at all (AGL-3596): the\n * site has one, or the job may create one. Absent capabilities restrict\n * nothing, as everywhere else.\n */\nexport function aiPlanCanPlaceForm(\n inventory: { forms: readonly unknown[] } | null,\n capabilities: Pick<AiPlanCapabilities, 'create'> | null,\n): boolean {\n if (inventory?.forms.length) return true\n return capabilities?.create.form.allowed !== false\n}\n\n/** What to do instead of a creation the job may not make, for the re-ask and the review. */\nfunction insteadOf(kind: AiBuildPlanCreateKind, capabilities: AiPlanCapabilities): string {\n switch (kind) {\n case 'component':\n return capabilities.reusableComponents\n ? 'Place a component the site already has.'\n : \"Draw a list's repeated items in one section instead.\"\n case 'form':\n return 'Place a form the site already has, or leave the form off the page.'\n case 'layout':\n return 'Put the page in a layout the site already has.'\n case 'template':\n return 'Build the page without a template.'\n case 'theme-change':\n return \"Use the colors the theme already has.\"\n case 'dataset':\n return 'Bind a dataset or collection the site already has, or keep the list short enough to write out.'\n case 'email':\n return 'Leave the email out of this job.'\n }\n}\n\n/** One creation a plan names that this job may not make here. */\nexport interface AiPlanUncreatable {\n kind: AiBuildPlanCreateKind\n name: string\n /** The plan path of the creation: `create[2]`. */\n path: string\n /** Why not, as a clause: \"this site has room for 1 more\". */\n reason: string\n /** One customer-safe sentence: what was refused, why, and what to do instead. */\n message: string\n}\n\n/**\n * Why the nth creation of a kind may not be made here, as a clause; `null`\n * when it may. A kind the site may make only so many more of refuses the ones\n * past that count, not the first of them.\n */\nfunction refusedReason(capability: AiPlanCreation, nth: number): string | null {\n const overCount = capability.allowed && capability.left !== null && nth > capability.left\n if (capability.allowed && !overCount) return null\n return overCount\n ? `this site has room for ${capability.left} more`\n : (capability.reason ?? 'it cannot be made here')\n}\n\n/** The creations a plan names that the capabilities refuse, in plan order. */\nexport function aiPlanUncreatable(\n plan: Pick<AiBuildPlan, 'create'>,\n capabilities: AiPlanCapabilities,\n): AiPlanUncreatable[] {\n const refused: AiPlanUncreatable[] = []\n const counted = new Map<AiBuildPlanCreateKind, number>()\n plan.create.forEach((entry, index) => {\n const nth = (counted.get(entry.kind) ?? 0) + 1\n counted.set(entry.kind, nth)\n const reason = refusedReason(capabilities.create[entry.kind], nth)\n if (reason === null) return\n refused.push({\n kind: entry.kind,\n name: entry.name,\n path: `create[${index}]`,\n reason,\n message: `The plan creates ${aiCreationNoun(entry.kind)} named \"${entry.name}\", and ${reason}. ${insteadOf(entry.kind, capabilities)}`,\n })\n })\n return refused\n}\n\n/**\n * Why the plan could not add one more creation of a kind here, and what to\n * do instead; `null` when it could (AGL-3040). Counted past the creations of\n * that kind the plan already names, as `aiPlanUncreatable` counts them, so a\n * plan told to declare a creation is never told to declare one the next\n * answer is refused for.\n */\nexport function aiPlanUncreatableKind(\n plan: Pick<AiBuildPlan, 'create'>,\n kind: AiBuildPlanCreateKind,\n capabilities: AiPlanCapabilities,\n): { reason: string; instead: string } | null {\n const planned = plan.create.filter((entry) => entry.kind === kind).length\n const reason = refusedReason(capabilities.create[kind], planned + 1)\n return reason === null ? null : { reason, instead: insteadOf(kind, capabilities) }\n}\n"],"names":["AI_BUILD_PLAN_CREATE_KINDS","AI_BUILD_PLAN_CREATION_NOUNS","ALLOWED","allowed","left","reason","aiUnrestrictedPlanCapabilities","reusableComponents","create","Object","fromEntries","map","kind","aiPlanCapabilitiesForJob","capabilities","scope","creates","includes","noun","aiCreationNoun","test","AI_PLAN_INLINE_SENTENCE","AI_PLAN_NO_FORM_SENTENCE","aiPlanCapabilityLines","lines","entry","push","aiPlanCanPlaceForm","inventory","forms","length","form","insteadOf","refusedReason","capability","nth","overCount","aiPlanUncreatable","plan","refused","counted","Map","forEach","index","get","set","name","path","message","aiPlanUncreatableKind","planned","filter","instead"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-plan-capabilities.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 {\n AI_BUILD_PLAN_CREATE_KINDS,\n AI_BUILD_PLAN_CREATION_NOUNS,\n type AiBuildPlan,\n type AiBuildPlanCreateKind,\n} from './ai-build-plan'\n\n/**\n * What a plan may create on one site (AGL-3030): the workspace's plan and the\n * site's counts against its limits, narrowed to what the job itself builds.\n *\n * A plan that names a creation nothing can make is a dead end the member pays\n * for twice: once for the plan, and again for the plan they describe after\n * being told to make the creation by hand — which, on a workspace whose plan\n * does not include it, they cannot. So the plan step is TOLD what may be\n * created before it answers, and the plan rules refuse a creation outside it\n * with the one re-ask every other rule gets.\n *\n * ── Where the doctrine builds inline ─────────────────────────────────────\n *\n * The building doctrine makes a repeat a reusable component and a form a\n * saved form placed by id. A workspace whose plan keeps no reusable\n * components draws a list's repeated items in one section instead\n * (AGL-3071); `reusableComponents` is the one flag every validator reads for\n * that.\n *\n * A form is never drawn inline (AGL-3596): it is a saved form, counted\n * against the site's form allowance like one a member makes. Where the site\n * has none to place and may create none, the page places no form at all.\n *\n * ── Kept out of the cached prefix ────────────────────────────────────────\n *\n * The capabilities are per workspace and per site, so they ride the plan's\n * USER turn, never a system block: the doctrine's cached prefix stays one\n * entry for the platform. The doctrine states the rule once — create only\n * what the request says may be created, and build inline where it says the\n * workspace keeps no reusable components — and the request states the facts.\n *\n * This module imports nothing at runtime beyond the plan's own model, so the\n * validators, the plan step, the doors and their specs read one vocabulary.\n * What a workspace's plan and counts come to is read by\n * `readAiPlanCapabilities` in `jobs/ai-job-drafts.ts`, beside the band\n * arithmetic the create route and the draft writer share.\n */\n\n/** One creation kind, as a job on this site may make it or not. */\nexport interface AiPlanCreation {\n allowed: boolean\n /** How many more the site may hold; `null` when the plan counts none. */\n left: number | null\n /**\n * Why not, as a clause a sentence completes: \"this workspace's plan does\n * not include reusable components\". `null` when allowed.\n */\n reason: string | null\n}\n\nexport interface AiPlanCapabilities {\n /**\n * Whether the workspace keeps reusable components. Where it does not, a\n * list's repeated items are drawn in one section. Forms are not read off\n * it: whether one may be created is `create.form` (AGL-3596).\n */\n reusableComponents: boolean\n /** Every creation kind a plan may name, and whether this job may make one here. */\n create: Readonly<Record<AiBuildPlanCreateKind, AiPlanCreation>>\n /**\n * Whether the workspace spends the Free taste, whose monthly credits are a\n * wall (AGL-2925): a plan is then held to the sections the wall's worst\n * case pays for (AGL-3070). Absent is `false`.\n */\n freeTaste?: boolean\n /**\n * A Free workspace's site scaffold (AGL-3594): the most pages its plan may\n * build, which the plan step sets for a `site` job on the Free taste. The\n * Free wall then holds the plan to that many pages and to the sections a\n * SITE plan's worst case leaves room for. Absent for every other job.\n */\n freeSitePages?: number\n /**\n * Whether this job's pages are written in the compact layout language\n * (AGL-3660), whose compiler draws a section's repeated items itself: as\n * instances of a component the plan places there, or, with none, in their\n * compact form, a title over its text, which is smaller than a block rule 1\n * calls a repeat. A plan is then not asked for a component for a section's\n * repeated items (AGL-3616); a section two pages share still is one. The\n * plan step sets it for a `build` job. Absent is `false`.\n */\n repeatsCompiled?: boolean\n}\n\n/** What a job of one kind builds from its own plan, where it builds only some creations. */\nexport interface AiPlanJobScope {\n /** The job as a refusal names it: \"a page job\". */\n noun: string\n /** The creation kinds the job builds itself; any other is refused before the plan is kept. */\n creates: readonly AiBuildPlanCreateKind[]\n}\n\nconst ALLOWED: AiPlanCreation = { allowed: true, left: null, reason: null }\n\n/**\n * Capabilities that restrict nothing: what a plan is held to when no\n * workspace was read — an org-level job, an eval, a spec about something\n * else. The doctrine applies whole.\n */\nexport function aiUnrestrictedPlanCapabilities(): AiPlanCapabilities {\n return {\n reusableComponents: true,\n create: Object.fromEntries(\n AI_BUILD_PLAN_CREATE_KINDS.map((kind) => [kind, ALLOWED]),\n ) as Record<AiBuildPlanCreateKind, AiPlanCreation>,\n }\n}\n\n/**\n * The workspace's capabilities narrowed to what a job of one kind builds.\n * A creation the workspace itself cannot make keeps the workspace's reason,\n * which is the one a member can act on; one the job does not build says so.\n */\nexport function aiPlanCapabilitiesForJob(\n capabilities: AiPlanCapabilities,\n scope: AiPlanJobScope | null | undefined,\n): AiPlanCapabilities {\n if (!scope) return capabilities\n const create = { ...capabilities.create }\n for (const kind of AI_BUILD_PLAN_CREATE_KINDS) {\n if (!create[kind].allowed || scope.creates.includes(kind)) continue\n create[kind] = { allowed: false, left: null, reason: `${scope.noun} does not build one` }\n }\n return { ...capabilities, create }\n}\n\n/** \"a layout\", \"an email design\": a creation kind with its article. */\nexport function aiCreationNoun(kind: AiBuildPlanCreateKind): string {\n const noun = AI_BUILD_PLAN_CREATION_NOUNS[kind].noun\n return `${/^[aeiou]/.test(noun) ? 'an' : 'a'} ${noun}`\n}\n\n/**\n * The sentence a request states when the workspace keeps no reusable\n * components. It says a list's repeated items go in one section (AGL-3071):\n * \"a repeated item in its own section\" reads just as well as a section for\n * each item, which is how a live About page's four practice areas came to be\n * planned as four sections of one item.\n */\nexport const AI_PLAN_INLINE_SENTENCE =\n \"This workspace keeps no reusable components: draw a list's repeated items in one section.\"\n\n/**\n * The sentence a build's request states (AGL-3616): its pages are written in\n * the layout language, which draws a section's repeated items itself, so the\n * plan counts them in the section's `items` and needs no component for them.\n */\nexport const AI_PLAN_COMPILED_REPEATS_SENTENCE =\n \"A section's repeated items need no component here: count them in its items and the page draws them. A section two pages share is still one component.\"\n\n/**\n * The sentence a request states when the site has no saved form and this job\n * may create none (AGL-3596): the plan places no form, rather than one drawn\n * on the page with no saved form behind it.\n */\nexport const AI_PLAN_NO_FORM_SENTENCE =\n 'This site has no saved form and may create none: plan no form, and no section that collects answers.'\n\n/**\n * What the plan step's user turn says the job may create here: one line a\n * kind, and the inline sentence where the workspace keeps no reusable\n * components. Per workspace and per site, so it is never a system block.\n */\nexport function aiPlanCapabilityLines(capabilities: AiPlanCapabilities): string[] {\n const lines = ['What this job may create on this site:']\n for (const kind of AI_BUILD_PLAN_CREATE_KINDS) {\n const entry = capabilities.create[kind]\n const noun = AI_BUILD_PLAN_CREATION_NOUNS[kind].noun\n if (entry.allowed) {\n lines.push(`- ${noun}: yes${entry.left === null ? '' : `, ${entry.left} more`}`)\n } else {\n lines.push(`- ${noun}: no, because ${entry.reason ?? 'it cannot be made here'}`)\n }\n }\n if (!capabilities.reusableComponents) lines.push(AI_PLAN_INLINE_SENTENCE)\n return lines\n}\n\n/**\n * Whether a plan on this site can place a saved form at all (AGL-3596): the\n * site has one, or the job may create one. Absent capabilities restrict\n * nothing, as everywhere else.\n */\nexport function aiPlanCanPlaceForm(\n inventory: { forms: readonly unknown[] } | null,\n capabilities: Pick<AiPlanCapabilities, 'create'> | null,\n): boolean {\n if (inventory?.forms.length) return true\n return capabilities?.create.form.allowed !== false\n}\n\n/** What to do instead of a creation the job may not make, for the re-ask and the review. */\nfunction insteadOf(kind: AiBuildPlanCreateKind, capabilities: AiPlanCapabilities): string {\n switch (kind) {\n case 'component':\n return capabilities.reusableComponents\n ? 'Place a component the site already has.'\n : \"Draw a list's repeated items in one section instead.\"\n case 'form':\n return 'Place a form the site already has, or leave the form off the page.'\n case 'layout':\n return 'Put the page in a layout the site already has.'\n case 'template':\n return 'Build the page without a template.'\n case 'theme-change':\n return \"Use the colors the theme already has.\"\n case 'dataset':\n return 'Bind a dataset or collection the site already has, or keep the list short enough to write out.'\n case 'email':\n return 'Leave the email out of this job.'\n }\n}\n\n/** One creation a plan names that this job may not make here. */\nexport interface AiPlanUncreatable {\n kind: AiBuildPlanCreateKind\n name: string\n /** The plan path of the creation: `create[2]`. */\n path: string\n /** Why not, as a clause: \"this site has room for 1 more\". */\n reason: string\n /** One customer-safe sentence: what was refused, why, and what to do instead. */\n message: string\n}\n\n/**\n * Why the nth creation of a kind may not be made here, as a clause; `null`\n * when it may. A kind the site may make only so many more of refuses the ones\n * past that count, not the first of them.\n */\nfunction refusedReason(capability: AiPlanCreation, nth: number): string | null {\n const overCount = capability.allowed && capability.left !== null && nth > capability.left\n if (capability.allowed && !overCount) return null\n return overCount\n ? `this site has room for ${capability.left} more`\n : (capability.reason ?? 'it cannot be made here')\n}\n\n/** The creations a plan names that the capabilities refuse, in plan order. */\nexport function aiPlanUncreatable(\n plan: Pick<AiBuildPlan, 'create'>,\n capabilities: AiPlanCapabilities,\n): AiPlanUncreatable[] {\n const refused: AiPlanUncreatable[] = []\n const counted = new Map<AiBuildPlanCreateKind, number>()\n plan.create.forEach((entry, index) => {\n const nth = (counted.get(entry.kind) ?? 0) + 1\n counted.set(entry.kind, nth)\n const reason = refusedReason(capabilities.create[entry.kind], nth)\n if (reason === null) return\n refused.push({\n kind: entry.kind,\n name: entry.name,\n path: `create[${index}]`,\n reason,\n message: `The plan creates ${aiCreationNoun(entry.kind)} named \"${entry.name}\", and ${reason}. ${insteadOf(entry.kind, capabilities)}`,\n })\n })\n return refused\n}\n\n/**\n * Why the plan could not add one more creation of a kind here, and what to\n * do instead; `null` when it could (AGL-3040). Counted past the creations of\n * that kind the plan already names, as `aiPlanUncreatable` counts them, so a\n * plan told to declare a creation is never told to declare one the next\n * answer is refused for.\n */\nexport function aiPlanUncreatableKind(\n plan: Pick<AiBuildPlan, 'create'>,\n kind: AiBuildPlanCreateKind,\n capabilities: AiPlanCapabilities,\n): { reason: string; instead: string } | null {\n const planned = plan.create.filter((entry) => entry.kind === kind).length\n const reason = refusedReason(capabilities.create[kind], planned + 1)\n return reason === null ? null : { reason, instead: insteadOf(kind, capabilities) }\n}\n"],"names":["AI_BUILD_PLAN_CREATE_KINDS","AI_BUILD_PLAN_CREATION_NOUNS","ALLOWED","allowed","left","reason","aiUnrestrictedPlanCapabilities","reusableComponents","create","Object","fromEntries","map","kind","aiPlanCapabilitiesForJob","capabilities","scope","creates","includes","noun","aiCreationNoun","test","AI_PLAN_INLINE_SENTENCE","AI_PLAN_COMPILED_REPEATS_SENTENCE","AI_PLAN_NO_FORM_SENTENCE","aiPlanCapabilityLines","lines","entry","push","aiPlanCanPlaceForm","inventory","forms","length","form","insteadOf","refusedReason","capability","nth","overCount","aiPlanUncreatable","plan","refused","counted","Map","forEach","index","get","set","name","path","message","aiPlanUncreatableKind","planned","filter","instead"],"mappings":"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"}
@@ -23,3 +23,21 @@ import type { OperatorAlertDefinition } from '@aglyn/aglyn/app-utils/operator-al
23
23
  * label — so a self-hoster on any provider hears the same thing.
24
24
  */
25
25
  export declare const AI_PROVIDER_UNAVAILABLE: OperatorAlertDefinition;
26
+ /**
27
+ * An AI job failed on our side: a provider error, a step that produced
28
+ * nothing usable, a build that delivered nothing. Not a model declining the
29
+ * brief, and not a site with AI switched off — those are the customer's, and
30
+ * the customer is told. Deduped per job kind, so an outage is one alert an
31
+ * hour for each kind it breaks rather than one per job; the runner's own
32
+ * words are in `{{error}}`, and every failure is in the log as `ai job failed`.
33
+ */
34
+ export declare const AI_JOB_FAILED: OperatorAlertDefinition;
35
+ /**
36
+ * A build finished with part of it missing: it delivered something, so the
37
+ * job is `done`, but one or more of its items failed on our side (AGL-3683).
38
+ * The customer has a site or build with holes in it and no failed job to
39
+ * show for it, which is why it is its own alert. Items the model declined
40
+ * are the customer's and do not count. Deduped per job kind, like
41
+ * `ai.jobFailed`.
42
+ */
43
+ export declare const AI_BUILD_PARTLY_FAILED: OperatorAlertDefinition;
@@ -33,5 +33,47 @@
33
33
  dedupeWindowMinutes: 6 * 60,
34
34
  defaultEnabled: true
35
35
  };
36
+ /**
37
+ * An AI job failed on our side: a provider error, a step that produced
38
+ * nothing usable, a build that delivered nothing. Not a model declining the
39
+ * brief, and not a site with AI switched off — those are the customer's, and
40
+ * the customer is told. Deduped per job kind, so an outage is one alert an
41
+ * hour for each kind it breaks rather than one per job; the runner's own
42
+ * words are in `{{error}}`, and every failure is in the log as `ai job failed`.
43
+ */ export const AI_JOB_FAILED = {
44
+ type: 'ai.jobFailed',
45
+ pluginId: AI_PLUGIN_ID,
46
+ label: 'An AI job failed',
47
+ description: 'An AI job (a site, page, build, theme…) failed on the platform’s side, so a customer got nothing for their request. Read the error, then search the logs for “ai job failed” to see whether it is one job or every job of that kind.',
48
+ tier: 'should',
49
+ category: 'ops',
50
+ title: 'An AI {{kind}} job failed',
51
+ body: 'AI job {{jobId}} ({{kind}}) in workspace {{orgId}} failed at step {{step}}: {{error}}. The customer was told and given back the credits. Further {{kind}} failures within the hour are counted into the next alert.',
52
+ link: '/admin/orgs/{{orgId}}',
53
+ delivery: 'immediate',
54
+ dedupeWindowMinutes: 60,
55
+ defaultEnabled: true
56
+ };
57
+ /**
58
+ * A build finished with part of it missing: it delivered something, so the
59
+ * job is `done`, but one or more of its items failed on our side (AGL-3683).
60
+ * The customer has a site or build with holes in it and no failed job to
61
+ * show for it, which is why it is its own alert. Items the model declined
62
+ * are the customer's and do not count. Deduped per job kind, like
63
+ * `ai.jobFailed`.
64
+ */ export const AI_BUILD_PARTLY_FAILED = {
65
+ type: 'ai.buildPartlyFailed',
66
+ pluginId: AI_PLUGIN_ID,
67
+ label: 'An AI build finished with parts missing',
68
+ description: 'An AI build (a site, or a multi-part build) finished, but some of its parts failed on the platform’s side, so the customer got less than they asked for. Read which parts and why, then search the logs for the job id.',
69
+ tier: 'should',
70
+ category: 'ops',
71
+ title: 'An AI {{kind}} build finished with {{failed}} of {{total}} parts failed',
72
+ body: 'AI job {{jobId}} ({{kind}}) in workspace {{orgId}} finished, but {{failed}} of its {{total}} parts failed on our side: {{items}}. The customer was given back those parts’ credits. Further partial {{kind}} builds within the hour are counted into the next alert.',
73
+ link: '/admin/orgs/{{orgId}}',
74
+ delivery: 'immediate',
75
+ dedupeWindowMinutes: 60,
76
+ defaultEnabled: true
77
+ };
36
78
 
37
79
  //# sourceMappingURL=operator-alerts.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/plugins/ai/src/lib/operator-alerts.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 { OperatorAlertDefinition } from '@aglyn/aglyn/app-utils/operator-alerts'\nimport { AI_PLUGIN_ID } from './constants'\n\n/**\n * The AI plugin's operator alert (AGL-3377): the platform's provider account\n * refused its key or ran out of credit, so every AI feature on the install\n * fails. Provider-generic — each adapter classifies its own vendor's answer\n * as an `accountProblem`, and the runtime raises this with the provider's\n * label — so a self-hoster on any provider hears the same thing.\n */\nexport const AI_PROVIDER_UNAVAILABLE: OperatorAlertDefinition = {\n type: 'ai.providerUnavailable',\n pluginId: AI_PLUGIN_ID,\n label: 'Platform AI provider unavailable',\n description:\n 'The platform’s AI provider refused its key or ran out of credit, so every AI feature on the install is failing until the key or the balance is fixed.',\n tier: 'should',\n category: 'ops',\n title: '{{provider}} is refusing requests',\n body:\n 'The platform’s {{provider}} account refused a request because {{reason}}. AI features fail until it is fixed ({{keyEnv}}).',\n delivery: 'immediate',\n dedupeWindowMinutes: 6 * 60,\n defaultEnabled: true,\n}\n"],"names":["AI_PLUGIN_ID","AI_PROVIDER_UNAVAILABLE","type","pluginId","label","description","tier","category","title","body","delivery","dedupeWindowMinutes","defaultEnabled"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAGD,SAASA,YAAY,QAAQ,iBAAa;AAE1C;;;;;;CAMC,GACD,OAAO,MAAMC,0BAAmD;IAC9DC,MAAM;IACNC,UAAUH;IACVI,OAAO;IACPC,aACE;IACFC,MAAM;IACNC,UAAU;IACVC,OAAO;IACPC,MACE;IACFC,UAAU;IACVC,qBAAqB,IAAI;IACzBC,gBAAgB;AAClB,EAAC"}
1
+ {"version":3,"sources":["../../../../../../libs/plugins/ai/src/lib/operator-alerts.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 { OperatorAlertDefinition } from '@aglyn/aglyn/app-utils/operator-alerts'\nimport { AI_PLUGIN_ID } from './constants'\n\n/**\n * The AI plugin's operator alert (AGL-3377): the platform's provider account\n * refused its key or ran out of credit, so every AI feature on the install\n * fails. Provider-generic — each adapter classifies its own vendor's answer\n * as an `accountProblem`, and the runtime raises this with the provider's\n * label — so a self-hoster on any provider hears the same thing.\n */\nexport const AI_PROVIDER_UNAVAILABLE: OperatorAlertDefinition = {\n type: 'ai.providerUnavailable',\n pluginId: AI_PLUGIN_ID,\n label: 'Platform AI provider unavailable',\n description:\n 'The platform’s AI provider refused its key or ran out of credit, so every AI feature on the install is failing until the key or the balance is fixed.',\n tier: 'should',\n category: 'ops',\n title: '{{provider}} is refusing requests',\n body:\n 'The platform’s {{provider}} account refused a request because {{reason}}. AI features fail until it is fixed ({{keyEnv}}).',\n delivery: 'immediate',\n dedupeWindowMinutes: 6 * 60,\n defaultEnabled: true,\n}\n\n/**\n * An AI job failed on our side: a provider error, a step that produced\n * nothing usable, a build that delivered nothing. Not a model declining the\n * brief, and not a site with AI switched off — those are the customer's, and\n * the customer is told. Deduped per job kind, so an outage is one alert an\n * hour for each kind it breaks rather than one per job; the runner's own\n * words are in `{{error}}`, and every failure is in the log as `ai job failed`.\n */\nexport const AI_JOB_FAILED: OperatorAlertDefinition = {\n type: 'ai.jobFailed',\n pluginId: AI_PLUGIN_ID,\n label: 'An AI job failed',\n description:\n 'An AI job (a site, page, build, theme…) failed on the platform’s side, so a customer got nothing for their request. Read the error, then search the logs for “ai job failed” to see whether it is one job or every job of that kind.',\n tier: 'should',\n category: 'ops',\n title: 'An AI {{kind}} job failed',\n body:\n 'AI job {{jobId}} ({{kind}}) in workspace {{orgId}} failed at step {{step}}: {{error}}. The customer was told and given back the credits. Further {{kind}} failures within the hour are counted into the next alert.',\n link: '/admin/orgs/{{orgId}}',\n delivery: 'immediate',\n dedupeWindowMinutes: 60,\n defaultEnabled: true,\n}\n\n/**\n * A build finished with part of it missing: it delivered something, so the\n * job is `done`, but one or more of its items failed on our side (AGL-3683).\n * The customer has a site or build with holes in it and no failed job to\n * show for it, which is why it is its own alert. Items the model declined\n * are the customer's and do not count. Deduped per job kind, like\n * `ai.jobFailed`.\n */\nexport const AI_BUILD_PARTLY_FAILED: OperatorAlertDefinition = {\n type: 'ai.buildPartlyFailed',\n pluginId: AI_PLUGIN_ID,\n label: 'An AI build finished with parts missing',\n description:\n 'An AI build (a site, or a multi-part build) finished, but some of its parts failed on the platform’s side, so the customer got less than they asked for. Read which parts and why, then search the logs for the job id.',\n tier: 'should',\n category: 'ops',\n title: 'An AI {{kind}} build finished with {{failed}} of {{total}} parts failed',\n body:\n 'AI job {{jobId}} ({{kind}}) in workspace {{orgId}} finished, but {{failed}} of its {{total}} parts failed on our side: {{items}}. The customer was given back those parts’ credits. Further partial {{kind}} builds within the hour are counted into the next alert.',\n link: '/admin/orgs/{{orgId}}',\n delivery: 'immediate',\n dedupeWindowMinutes: 60,\n defaultEnabled: true,\n}\n"],"names":["AI_PLUGIN_ID","AI_PROVIDER_UNAVAILABLE","type","pluginId","label","description","tier","category","title","body","delivery","dedupeWindowMinutes","defaultEnabled","AI_JOB_FAILED","link","AI_BUILD_PARTLY_FAILED"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAGD,SAASA,YAAY,QAAQ,iBAAa;AAE1C;;;;;;CAMC,GACD,OAAO,MAAMC,0BAAmD;IAC9DC,MAAM;IACNC,UAAUH;IACVI,OAAO;IACPC,aACE;IACFC,MAAM;IACNC,UAAU;IACVC,OAAO;IACPC,MACE;IACFC,UAAU;IACVC,qBAAqB,IAAI;IACzBC,gBAAgB;AAClB,EAAC;AAED;;;;;;;CAOC,GACD,OAAO,MAAMC,gBAAyC;IACpDX,MAAM;IACNC,UAAUH;IACVI,OAAO;IACPC,aACE;IACFC,MAAM;IACNC,UAAU;IACVC,OAAO;IACPC,MACE;IACFK,MAAM;IACNJ,UAAU;IACVC,qBAAqB;IACrBC,gBAAgB;AAClB,EAAC;AAED;;;;;;;CAOC,GACD,OAAO,MAAMG,yBAAkD;IAC7Db,MAAM;IACNC,UAAUH;IACVI,OAAO;IACPC,aACE;IACFC,MAAM;IACNC,UAAU;IACVC,OAAO;IACPC,MACE;IACFK,MAAM;IACNJ,UAAU;IACVC,qBAAqB;IACrBC,gBAAgB;AAClB,EAAC"}
@@ -683,6 +683,13 @@ export declare function aiNamesMatch(left: string, right: string): boolean;
683
683
  * Rule 1 (plan): repeated items place a component, and a section two screens
684
684
  * share is one. A workspace that keeps no reusable components draws them in
685
685
  * their sections instead (AGL-3030), and nothing is refused.
686
+ *
687
+ * A job whose pages the layout language compiles (`repeatsCompiled`, a
688
+ * build) is not asked for a component for a section's repeated items: the
689
+ * compiler draws them in the section, compact where the plan places none, so
690
+ * the page answers rule 1 itself (AGL-3660). Held to the first half anyway,
691
+ * the canonical build request was refused twice for an About page's three
692
+ * values (AGL-3616). A section two pages share is still one component.
686
693
  */
687
694
  export declare function detectPlanRepeats(plan: AiBuildPlan, inventory: AiSiteInventory | null, capabilities?: AiPlanCapabilities | null): AiDoctrineViolation[];
688
695
  /** The fewest sections of one item, side by side and of one kind, that are one list split apart. */
@@ -2912,6 +2912,13 @@ const FORM_SECTION_NAME = /\bform\b|\bsign[- ]?up\b|\bsubscribe\b|\bnewsletter\b
2912
2912
  * Rule 1 (plan): repeated items place a component, and a section two screens
2913
2913
  * share is one. A workspace that keeps no reusable components draws them in
2914
2914
  * their sections instead (AGL-3030), and nothing is refused.
2915
+ *
2916
+ * A job whose pages the layout language compiles (`repeatsCompiled`, a
2917
+ * build) is not asked for a component for a section's repeated items: the
2918
+ * compiler draws them in the section, compact where the plan places none, so
2919
+ * the page answers rule 1 itself (AGL-3660). Held to the first half anyway,
2920
+ * the canonical build request was refused twice for an About page's three
2921
+ * values (AGL-3616). A section two pages share is still one component.
2915
2922
  */ export function detectPlanRepeats(plan, inventory, capabilities = null) {
2916
2923
  if ((capabilities == null ? void 0 : capabilities.reusableComponents) === false) return [];
2917
2924
  const kinds = inventoryKinds(inventory);
@@ -2921,7 +2928,7 @@ const FORM_SECTION_NAME = /\bform\b|\bsign[- ]?up\b|\bsubscribe\b|\bnewsletter\b
2921
2928
  'dataset',
2922
2929
  'collection'
2923
2930
  ].includes(String(refKind(ref, plan, kinds))));
2924
- const repeated = sectionPaths(plan).filter(({ section })=>section.items >= AI_REPEAT_MIN_COUNT && !placesComponent(section.uses) && !bound(section.uses));
2931
+ const repeated = (capabilities == null ? void 0 : capabilities.repeatsCompiled) ? [] : sectionPaths(plan).filter(({ section })=>section.items >= AI_REPEAT_MIN_COUNT && !placesComponent(section.uses) && !bound(section.uses));
2925
2932
  if (repeated.length) {
2926
2933
  violations.push({
2927
2934
  rule: 1,
@@ -3017,7 +3024,7 @@ const FORM_SECTION_NAME = /\bform\b|\bsign[- ]?up\b|\bsubscribe\b|\bnewsletter\b
3017
3024
  const listed = `${names.slice(0, -1).join(', ')} and ${names[names.length - 1]}`;
3018
3025
  const [{ kind }] = run;
3019
3026
  // Rule 1 asks a list this long on a workspace that keeps components to place one for its item.
3020
- const component = !kind.components.length && (capabilities == null ? void 0 : capabilities.reusableComponents) !== false && run.length >= AI_REPEAT_MIN_COUNT;
3027
+ const component = !kind.components.length && (capabilities == null ? void 0 : capabilities.reusableComponents) !== false && !(capabilities == null ? void 0 : capabilities.repeatsCompiled) && run.length >= AI_REPEAT_MIN_COUNT;
3021
3028
  const section = {
3022
3029
  name: kind.name,
3023
3030
  uses: component ? [