@aglyn/plugins-ai 1.0.0-beta.151 → 1.0.0-beta.152
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/jobs/ai-job-component-step.js +6 -1
- package/src/lib/jobs/ai-job-component-step.js.map +1 -1
- package/src/lib/jobs/ai-job-form-step.js +6 -1
- package/src/lib/jobs/ai-job-form-step.js.map +1 -1
- package/src/lib/jobs/ai-job-layout-step.js +6 -1
- package/src/lib/jobs/ai-job-layout-step.js.map +1 -1
- package/src/lib/jobs/ai-job-template-step.js +6 -1
- package/src/lib/jobs/ai-job-template-step.js.map +1 -1
- package/src/lib/runtime/ai-palette.generated.js +2 -2
- package/src/lib/runtime/ai-palette.generated.js.map +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aglyn/plugins-ai",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.152",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"homepage": "https://aglyn.com",
|
|
6
6
|
"repository": {
|
|
@@ -25,22 +25,22 @@
|
|
|
25
25
|
"./package.json": "./package.json"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@aglyn/aglyn": "1.0.0-beta.
|
|
29
|
-
"@aglyn/shared-data-enums": "1.0.0-beta.
|
|
30
|
-
"@aglyn/shared-data-mdi": "1.0.0-beta.
|
|
31
|
-
"@aglyn/shared-data-types": "1.0.0-beta.
|
|
32
|
-
"@aglyn/shared-ui-email-campaigns": "1.0.0-beta.
|
|
33
|
-
"@aglyn/shared-ui-jsx": "1.0.0-beta.
|
|
34
|
-
"@aglyn/shared-ui-snackstack": "1.0.0-beta.
|
|
35
|
-
"@aglyn/shared-ui-theme": "1.0.0-beta.
|
|
36
|
-
"@aglyn/shared-util-email": "1.0.0-beta.
|
|
37
|
-
"@aglyn/shared-util-errors": "1.0.0-beta.
|
|
38
|
-
"@aglyn/shared-util-http": "1.0.0-beta.
|
|
39
|
-
"@aglyn/shared-util-timestamp": "1.0.0-beta.
|
|
40
|
-
"@aglyn/shared-util-tools": "1.0.0-beta.
|
|
41
|
-
"@aglyn/tenant-data-admin": "1.0.0-beta.
|
|
42
|
-
"@aglyn/tenant-feature-instance": "1.0.0-beta.
|
|
43
|
-
"@aglyn/tenant-runtime": "1.0.0-beta.
|
|
28
|
+
"@aglyn/aglyn": "1.0.0-beta.152",
|
|
29
|
+
"@aglyn/shared-data-enums": "1.0.0-beta.152",
|
|
30
|
+
"@aglyn/shared-data-mdi": "1.0.0-beta.152",
|
|
31
|
+
"@aglyn/shared-data-types": "1.0.0-beta.152",
|
|
32
|
+
"@aglyn/shared-ui-email-campaigns": "1.0.0-beta.152",
|
|
33
|
+
"@aglyn/shared-ui-jsx": "1.0.0-beta.152",
|
|
34
|
+
"@aglyn/shared-ui-snackstack": "1.0.0-beta.152",
|
|
35
|
+
"@aglyn/shared-ui-theme": "1.0.0-beta.152",
|
|
36
|
+
"@aglyn/shared-util-email": "1.0.0-beta.152",
|
|
37
|
+
"@aglyn/shared-util-errors": "1.0.0-beta.152",
|
|
38
|
+
"@aglyn/shared-util-http": "1.0.0-beta.152",
|
|
39
|
+
"@aglyn/shared-util-timestamp": "1.0.0-beta.152",
|
|
40
|
+
"@aglyn/shared-util-tools": "1.0.0-beta.152",
|
|
41
|
+
"@aglyn/tenant-data-admin": "1.0.0-beta.152",
|
|
42
|
+
"@aglyn/tenant-feature-instance": "1.0.0-beta.152",
|
|
43
|
+
"@aglyn/tenant-runtime": "1.0.0-beta.152",
|
|
44
44
|
"@swc/helpers": "0.5.23"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
@@ -148,7 +148,12 @@ export function createAiJobComponentStep(deps = {}) {
|
|
|
148
148
|
sourceId: creation.duplicateOf,
|
|
149
149
|
name,
|
|
150
150
|
uid: job.createdBy,
|
|
151
|
-
org: org
|
|
151
|
+
org: org,
|
|
152
|
+
// The same claim the page step makes (AGL-3024): a copy core mints is
|
|
153
|
+
// written under core's own id, never `draftId`, so a step that runs
|
|
154
|
+
// again — a pass resumed, a run cut off after the copy — would mint a
|
|
155
|
+
// second. The job's recorded id makes core replay the one it made.
|
|
156
|
+
attemptKey: draftId
|
|
152
157
|
});
|
|
153
158
|
if (copy.ok) {
|
|
154
159
|
var _copy_versionId;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-component-step.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { AglynOrgBilling } from '@aglyn/aglyn/foundation/definitions/org-billing.types'\nimport type { ReusableComponentProp } from '@aglyn/aglyn/foundation/definitions/platform.types'\nimport { duplicateResource } from '@aglyn/tenant-data-admin/server/duplicate-resource'\nimport type { AiJob, AiJobOutput, AiJobPlan } from '../model/ai-jobs.types'\nimport { AI_STEP_TIERS } from '../providers/catalog'\nimport { AI_ROUTING_TABLE, aiModelForStep } from '../providers/routing'\nimport { runValidatedGeneration } from '../runtime/ai-doctrine'\nimport type { AiDoctrineTree } from '../runtime/ai-doctrine-validators'\nimport type { AiLoadEstimate } from '../runtime/ai-palette'\nimport type { AiSystemBlock } from '../runtime/ai-runtime'\nimport { readSiteInventory } from '../runtime/site-inventory'\nimport {\n AI_COMPONENT_TOOL_NAME,\n aiComponentPropKindWords,\n aiComponentTool,\n} from '../tools/ai-component-tool'\nimport { registerAiJobAdmission, type AiJobAdmission } from './ai-job-admission'\nimport { aiJobStepBudget } from './ai-job-budget'\nimport { aiComponentCheck } from './ai-job-component-checks'\nimport { aiJobDraftId } from './ai-job-draft-ids'\nimport {\n aiDraftAdmissionRefusal,\n aiDraftAllowanceRefusal,\n aiSiteSubdomain,\n readAiDraft,\n writeAiDraft,\n type AiDraftRecord,\n} from './ai-job-drafts'\nimport {\n aiBracketedFactsNote,\n aiConfirmedPlan,\n aiDoctrineReview,\n aiGenerationSpent,\n aiJobBriefLine,\n aiLimitReview,\n aiPlanCreation,\n aiPlanReferenceLines,\n aiUnspentOutcome,\n} from './ai-job-generation'\nimport type { AiJobStepRunner } from './ai-job-text-step'\nimport { registerAiJobStep } from './ai-jobs'\n\n/**\n * The component step (AGL-2908): the generation step of a `component` job,\n * run once a member confirmed the job's plan.\n *\n * It builds ONE reusable component from the brief — its tree, and the typed\n * properties each page that places it fills in — through\n * `runValidatedGeneration('component', …)`, so every building rule holds on\n * the tree before it is kept. The tree binds each property with\n * `{{prop.<name>}}`; the palette validator keeps those tokens as written\n * (`definesComponent`), and the step's own check, through `extend`, holds\n * each binding to what the Properties dialog and the Attributes panel allow\n * (`ai-job-component-checks.ts`).\n *\n * The component lands as a new draft (`ai-job-drafts.ts`) that no page\n * places, and gets its first version when a member opens it, as every\n * component created outside the besigner does. A plan that starts from a copy\n * of a component the site has gets that copy, through the platform's\n * duplicate module, and nothing generated.\n *\n * A generation step runs on the job beat, never at an inline door: it\n * registers the least time its lookup rounds, its answer and its re-ask need\n * (AGL-3035), runs without extended thinking, and asks for no more of the\n * ceiling the layout and template steps keep than fits that time on the model\n * it runs. The step's spec measures the request and the answer against it.\n */\n\n/** The longest answer a component may run to; the tree is held to the component budget either way. */\nexport const AI_JOB_COMPONENT_MAX_TOKENS = AI_ROUTING_TABLE['job.component'].maxTokens\n\n/**\n * The component step's time (AGL-3035, AGL-3036): its two inventory-lookup\n * rounds, its answer and its re-ask at the routing table's ceiling on the tier\n * `job.component` is served from, fitted to what a beat can give a step.\n */\nexport const AI_JOB_COMPONENT_STEP_BUDGET = aiJobStepBudget({\n tier: AI_STEP_TIERS['job.component'],\n maxTokens: AI_JOB_COMPONENT_MAX_TOKENS,\n})\n\n/** The least time one component step needs before it starts. */\nexport const AI_JOB_COMPONENT_STEP_MINIMUM_MS = AI_JOB_COMPONENT_STEP_BUDGET.minimumMs\n\n/** A component's name when the plan names none. */\nexport const AI_JOB_COMPONENT_DEFAULT_NAME = 'Component'\n\n/** The component step's own instructions, cached after the doctrine and before the catalog. */\nexport const AI_JOB_COMPONENT_INSTRUCTIONS: readonly AiSystemBlock[] = [\n {\n text: [\n `You build one reusable component for a website: a block the site places on many pages, each placement filling in its own values. Answer with ${AI_COMPONENT_TOOL_NAME}: the component’s tree as one flat node map, and the properties it declares.`,\n 'The root is the document wrapper holding the component. A component is placed inside a page section, which opens with its own h2: it carries no main landmark, its headings are h3 or below, and its colors, spacing and type come from the theme.',\n 'Every value that should differ from one placement to the next is a property, bound in the tree with its token {{prop.<name>}}. Declare every property the tree binds, and bind every property you declare.',\n `Property kinds, as type (the name a page reads): ${aiComponentPropKindWords()}.`,\n 'A property binds only to a field that holds its kind of value:',\n '- text, richText and number: copy, such as a Typography’s text, an Image’s alt or a Button’s label, as the whole value or inside a sentence.',\n '- image: an Image’s src, as the whole value.',\n '- href: a screenId or an href, as the whole value.',\n '- icon: an Icon element’s iconId, as the whole value.',\n '- boolean: a switch setting such as a Button’s fullWidth, or hideIf on the part it hides, as the whole value.',\n '- choice: a setting with fixed options such as a Button’s variant, as the whole value, with every answer’s value one that setting lists.',\n 'An optional part, such as a photo, a badge or a second button, gets a boolean property labeled \"Hide <the part>\" whose default is \"false\", and the part’s own element carries \"hideIf\": \"{{prop.<name>}}\". Never hide the whole component.',\n 'A default is what the component shows until a page sets it: copy in the site’s voice from the brief, with a fact the brief does not give in square brackets; for href a screen id from the site inventory, or \"\"; for image \"\", for an upload; for icon \"\", for the site owner to pick; for choice one of its answers’ values.',\n ].join('\\n'),\n },\n]\n\n/** The component job as the generation's user turn: the name, the brief, the confirmed plan. */\nexport function aiJobComponentPrompt(\n job: Pick<AiJob, 'brief'>,\n plan: AiJobPlan | null,\n name: string,\n): string {\n return [`Component name: ${name}`, aiJobBriefLine(job), ...aiPlanReferenceLines(plan)].join('\\n')\n}\n\n/** A component job is admitted for a site of the job's own org whose plan includes reusable components. */\nexport const aiComponentJobAdmission: AiJobAdmission = (context) =>\n aiDraftAdmissionRefusal(context.firestore, {\n orgId: context.orgId,\n hostId: context.hostId,\n kind: 'component',\n org: context.org,\n })\n\nexport interface AiJobComponentStepDeps {\n /** The inventory reader; specs hand in a fake. */\n readInventory?: typeof readSiteInventory\n /** The platform's duplicate module, for a plan that starts from a copy. */\n duplicate?: typeof duplicateResource\n}\n\nexport function createAiJobComponentStep(deps: AiJobComponentStepDeps = {}): AiJobStepRunner {\n const readInventory = deps.readInventory ?? readSiteInventory\n const duplicate = deps.duplicate ?? duplicateResource\n return async ({ job, now, signal, firestore, modelFor }) => {\n const hostId = job.hostId\n if (!hostId) throw new Error('a component job names no site, and its admission refuses one')\n const output = (draft: AiDraftRecord, load?: AiLoadEstimate | null, note?: string | null): AiJobOutput => ({\n resource: 'reusableComponent',\n id: draft.id,\n versionId: draft.versionId,\n hostId,\n hostSubdomain: draft.hostSubdomain,\n label: draft.name,\n ...(load ? { load } : {}),\n ...(note ? { note } : {}),\n })\n // The switch's answer for this job, else the routing table's (AGL-2942).\n const model = modelFor?.('job.component') ?? aiModelForStep('job.component')\n\n // A run cut off after its draft was written reports that draft, found by the id the job recorded.\n const draftId = aiJobDraftId(job, 'component')\n const written = await readAiDraft(firestore, { kind: 'component', hostId, id: draftId })\n if (written) return aiUnspentOutcome(model, { outputs: [output(written)] })\n\n const [inventory, orgSnapshot] = await Promise.all([\n readInventory(job.orgId, hostId, { firestore }),\n firestore.collection('orgs').doc(job.orgId).get(),\n ])\n const org = (orgSnapshot.data() ?? null) as Partial<AglynOrgBilling> | null\n const plan = aiConfirmedPlan(job)\n const creation = aiPlanCreation(plan, 'component')\n const name = creation?.name || AI_JOB_COMPONENT_DEFAULT_NAME\n\n // The plan starts from a copy of a component the site has (rule 15): the\n // copy is the draft, and nothing is generated. A source gone since the\n // plan was made is built from the brief instead.\n if (\n creation?.duplicateOf &&\n inventory.components.some((component) => component.id === creation.duplicateOf)\n ) {\n const copy = await duplicate('component', {\n orgId: job.orgId,\n hostId,\n sourceId: creation.duplicateOf,\n name,\n uid: job.createdBy,\n org: org as Record<string, unknown> | null,\n })\n if (copy.ok) {\n const hostSubdomain = await aiSiteSubdomain(firestore, hostId)\n return aiUnspentOutcome(model, {\n outputs: [\n output({ id: copy.id, versionId: copy.versionId ?? null, name: copy.name, hostSubdomain }),\n ],\n })\n }\n if (copy.ok === false && copy.status === 403) {\n return aiUnspentOutcome(model, { review: aiLimitReview(copy.error) })\n }\n }\n\n const allowance = await aiDraftAllowanceRefusal(firestore, { kind: 'component', hostId, org })\n if (allowance) return aiUnspentOutcome(model, { review: aiLimitReview(allowance) })\n\n let props: ReusableComponentProp[] = []\n const result = await runValidatedGeneration('component', {\n step: 'job.component',\n model,\n instructions: AI_JOB_COMPONENT_INSTRUCTIONS,\n inventory,\n messages: [{ role: 'user', content: aiJobComponentPrompt(job, plan, name) }],\n tool: aiComponentTool(),\n maxTokens: AI_JOB_COMPONENT_STEP_BUDGET.maxTokens(model),\n ...(AI_ROUTING_TABLE['job.component'].thinking\n ? { thinking: AI_ROUTING_TABLE['job.component'].thinking }\n : {}),\n ...(AI_ROUTING_TABLE['job.component'].effort\n ? { effort: AI_ROUTING_TABLE['job.component'].effort }\n : {}),\n context: { definesComponent: true },\n extend: aiComponentCheck({\n inventory,\n plan,\n onProps: (declared) => {\n props = declared\n },\n }),\n ...(signal ? { signal } : {}),\n })\n const spent = aiGenerationSpent(result)\n if (result.status === 'refused') return { ...spent, refused: true }\n if (result.status === 'needs_input') return { ...spent, review: aiDoctrineReview(result) }\n\n const draft = await writeAiDraft(firestore, {\n kind: 'component',\n hostId,\n id: draftId,\n uid: job.createdBy,\n org,\n name,\n nodes: result.value.nodes,\n rootId: result.value.rootId,\n props,\n now,\n })\n if (draft.ok === false) {\n if (draft.status === 404) throw new Error(`site ${hostId} vanished while its component was generated`)\n return { ...spent, review: aiLimitReview(draft.error) }\n }\n // The facts the brief did not give, in the tree or the defaults a page shows (AGL-3056).\n const note = aiBracketedFactsNote({\n tree: { rootId: result.value.rootId, nodes: result.value.nodes as unknown as AiDoctrineTree['nodes'] },\n inventory,\n copy: props.map((prop) => (typeof prop.defaultValue === 'string' ? prop.defaultValue : '')),\n })\n return { ...spent, outputs: [output(draft, result.value.load, note)] }\n }\n}\n\nexport const runAiJobComponentStep = createAiJobComponentStep()\n\n/** Registers the component step and the check a component job passes before it is created or resumed. */\nexport function registerAiComponentJob(): void {\n registerAiJobStep('component', runAiJobComponentStep, { minimumMs: AI_JOB_COMPONENT_STEP_MINIMUM_MS })\n registerAiJobAdmission('component', aiComponentJobAdmission)\n}\n"],"names":["duplicateResource","AI_STEP_TIERS","AI_ROUTING_TABLE","aiModelForStep","runValidatedGeneration","readSiteInventory","AI_COMPONENT_TOOL_NAME","aiComponentPropKindWords","aiComponentTool","registerAiJobAdmission","aiJobStepBudget","aiComponentCheck","aiJobDraftId","aiDraftAdmissionRefusal","aiDraftAllowanceRefusal","aiSiteSubdomain","readAiDraft","writeAiDraft","aiBracketedFactsNote","aiConfirmedPlan","aiDoctrineReview","aiGenerationSpent","aiJobBriefLine","aiLimitReview","aiPlanCreation","aiPlanReferenceLines","aiUnspentOutcome","registerAiJobStep","AI_JOB_COMPONENT_MAX_TOKENS","maxTokens","AI_JOB_COMPONENT_STEP_BUDGET","tier","AI_JOB_COMPONENT_STEP_MINIMUM_MS","minimumMs","AI_JOB_COMPONENT_DEFAULT_NAME","AI_JOB_COMPONENT_INSTRUCTIONS","text","join","aiJobComponentPrompt","job","plan","name","aiComponentJobAdmission","context","firestore","orgId","hostId","kind","org","createAiJobComponentStep","deps","readInventory","duplicate","now","signal","modelFor","orgSnapshot","Error","output","draft","load","note","resource","id","versionId","hostSubdomain","label","model","draftId","written","outputs","inventory","Promise","all","collection","doc","get","data","creation","duplicateOf","components","some","component","copy","sourceId","uid","createdBy","ok","status","review","error","allowance","props","result","step","instructions","messages","role","content","tool","thinking","effort","definesComponent","extend","onProps","declared","spent","refused","nodes","value","rootId","tree","map","prop","defaultValue","runAiJobComponentStep","registerAiComponentJob"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-component-step.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { AglynOrgBilling } from '@aglyn/aglyn/foundation/definitions/org-billing.types'\nimport type { ReusableComponentProp } from '@aglyn/aglyn/foundation/definitions/platform.types'\nimport { duplicateResource } from '@aglyn/tenant-data-admin/server/duplicate-resource'\nimport type { AiJob, AiJobOutput, AiJobPlan } from '../model/ai-jobs.types'\nimport { AI_STEP_TIERS } from '../providers/catalog'\nimport { AI_ROUTING_TABLE, aiModelForStep } from '../providers/routing'\nimport { runValidatedGeneration } from '../runtime/ai-doctrine'\nimport type { AiDoctrineTree } from '../runtime/ai-doctrine-validators'\nimport type { AiLoadEstimate } from '../runtime/ai-palette'\nimport type { AiSystemBlock } from '../runtime/ai-runtime'\nimport { readSiteInventory } from '../runtime/site-inventory'\nimport {\n AI_COMPONENT_TOOL_NAME,\n aiComponentPropKindWords,\n aiComponentTool,\n} from '../tools/ai-component-tool'\nimport { registerAiJobAdmission, type AiJobAdmission } from './ai-job-admission'\nimport { aiJobStepBudget } from './ai-job-budget'\nimport { aiComponentCheck } from './ai-job-component-checks'\nimport { aiJobDraftId } from './ai-job-draft-ids'\nimport {\n aiDraftAdmissionRefusal,\n aiDraftAllowanceRefusal,\n aiSiteSubdomain,\n readAiDraft,\n writeAiDraft,\n type AiDraftRecord,\n} from './ai-job-drafts'\nimport {\n aiBracketedFactsNote,\n aiConfirmedPlan,\n aiDoctrineReview,\n aiGenerationSpent,\n aiJobBriefLine,\n aiLimitReview,\n aiPlanCreation,\n aiPlanReferenceLines,\n aiUnspentOutcome,\n} from './ai-job-generation'\nimport type { AiJobStepRunner } from './ai-job-text-step'\nimport { registerAiJobStep } from './ai-jobs'\n\n/**\n * The component step (AGL-2908): the generation step of a `component` job,\n * run once a member confirmed the job's plan.\n *\n * It builds ONE reusable component from the brief — its tree, and the typed\n * properties each page that places it fills in — through\n * `runValidatedGeneration('component', …)`, so every building rule holds on\n * the tree before it is kept. The tree binds each property with\n * `{{prop.<name>}}`; the palette validator keeps those tokens as written\n * (`definesComponent`), and the step's own check, through `extend`, holds\n * each binding to what the Properties dialog and the Attributes panel allow\n * (`ai-job-component-checks.ts`).\n *\n * The component lands as a new draft (`ai-job-drafts.ts`) that no page\n * places, and gets its first version when a member opens it, as every\n * component created outside the besigner does. A plan that starts from a copy\n * of a component the site has gets that copy, through the platform's\n * duplicate module, and nothing generated.\n *\n * A generation step runs on the job beat, never at an inline door: it\n * registers the least time its lookup rounds, its answer and its re-ask need\n * (AGL-3035), runs without extended thinking, and asks for no more of the\n * ceiling the layout and template steps keep than fits that time on the model\n * it runs. The step's spec measures the request and the answer against it.\n */\n\n/** The longest answer a component may run to; the tree is held to the component budget either way. */\nexport const AI_JOB_COMPONENT_MAX_TOKENS = AI_ROUTING_TABLE['job.component'].maxTokens\n\n/**\n * The component step's time (AGL-3035, AGL-3036): its two inventory-lookup\n * rounds, its answer and its re-ask at the routing table's ceiling on the tier\n * `job.component` is served from, fitted to what a beat can give a step.\n */\nexport const AI_JOB_COMPONENT_STEP_BUDGET = aiJobStepBudget({\n tier: AI_STEP_TIERS['job.component'],\n maxTokens: AI_JOB_COMPONENT_MAX_TOKENS,\n})\n\n/** The least time one component step needs before it starts. */\nexport const AI_JOB_COMPONENT_STEP_MINIMUM_MS = AI_JOB_COMPONENT_STEP_BUDGET.minimumMs\n\n/** A component's name when the plan names none. */\nexport const AI_JOB_COMPONENT_DEFAULT_NAME = 'Component'\n\n/** The component step's own instructions, cached after the doctrine and before the catalog. */\nexport const AI_JOB_COMPONENT_INSTRUCTIONS: readonly AiSystemBlock[] = [\n {\n text: [\n `You build one reusable component for a website: a block the site places on many pages, each placement filling in its own values. Answer with ${AI_COMPONENT_TOOL_NAME}: the component’s tree as one flat node map, and the properties it declares.`,\n 'The root is the document wrapper holding the component. A component is placed inside a page section, which opens with its own h2: it carries no main landmark, its headings are h3 or below, and its colors, spacing and type come from the theme.',\n 'Every value that should differ from one placement to the next is a property, bound in the tree with its token {{prop.<name>}}. Declare every property the tree binds, and bind every property you declare.',\n `Property kinds, as type (the name a page reads): ${aiComponentPropKindWords()}.`,\n 'A property binds only to a field that holds its kind of value:',\n '- text, richText and number: copy, such as a Typography’s text, an Image’s alt or a Button’s label, as the whole value or inside a sentence.',\n '- image: an Image’s src, as the whole value.',\n '- href: a screenId or an href, as the whole value.',\n '- icon: an Icon element’s iconId, as the whole value.',\n '- boolean: a switch setting such as a Button’s fullWidth, or hideIf on the part it hides, as the whole value.',\n '- choice: a setting with fixed options such as a Button’s variant, as the whole value, with every answer’s value one that setting lists.',\n 'An optional part, such as a photo, a badge or a second button, gets a boolean property labeled \"Hide <the part>\" whose default is \"false\", and the part’s own element carries \"hideIf\": \"{{prop.<name>}}\". Never hide the whole component.',\n 'A default is what the component shows until a page sets it: copy in the site’s voice from the brief, with a fact the brief does not give in square brackets; for href a screen id from the site inventory, or \"\"; for image \"\", for an upload; for icon \"\", for the site owner to pick; for choice one of its answers’ values.',\n ].join('\\n'),\n },\n]\n\n/** The component job as the generation's user turn: the name, the brief, the confirmed plan. */\nexport function aiJobComponentPrompt(\n job: Pick<AiJob, 'brief'>,\n plan: AiJobPlan | null,\n name: string,\n): string {\n return [`Component name: ${name}`, aiJobBriefLine(job), ...aiPlanReferenceLines(plan)].join('\\n')\n}\n\n/** A component job is admitted for a site of the job's own org whose plan includes reusable components. */\nexport const aiComponentJobAdmission: AiJobAdmission = (context) =>\n aiDraftAdmissionRefusal(context.firestore, {\n orgId: context.orgId,\n hostId: context.hostId,\n kind: 'component',\n org: context.org,\n })\n\nexport interface AiJobComponentStepDeps {\n /** The inventory reader; specs hand in a fake. */\n readInventory?: typeof readSiteInventory\n /** The platform's duplicate module, for a plan that starts from a copy. */\n duplicate?: typeof duplicateResource\n}\n\nexport function createAiJobComponentStep(deps: AiJobComponentStepDeps = {}): AiJobStepRunner {\n const readInventory = deps.readInventory ?? readSiteInventory\n const duplicate = deps.duplicate ?? duplicateResource\n return async ({ job, now, signal, firestore, modelFor }) => {\n const hostId = job.hostId\n if (!hostId) throw new Error('a component job names no site, and its admission refuses one')\n const output = (draft: AiDraftRecord, load?: AiLoadEstimate | null, note?: string | null): AiJobOutput => ({\n resource: 'reusableComponent',\n id: draft.id,\n versionId: draft.versionId,\n hostId,\n hostSubdomain: draft.hostSubdomain,\n label: draft.name,\n ...(load ? { load } : {}),\n ...(note ? { note } : {}),\n })\n // The switch's answer for this job, else the routing table's (AGL-2942).\n const model = modelFor?.('job.component') ?? aiModelForStep('job.component')\n\n // A run cut off after its draft was written reports that draft, found by the id the job recorded.\n const draftId = aiJobDraftId(job, 'component')\n const written = await readAiDraft(firestore, { kind: 'component', hostId, id: draftId })\n if (written) return aiUnspentOutcome(model, { outputs: [output(written)] })\n\n const [inventory, orgSnapshot] = await Promise.all([\n readInventory(job.orgId, hostId, { firestore }),\n firestore.collection('orgs').doc(job.orgId).get(),\n ])\n const org = (orgSnapshot.data() ?? null) as Partial<AglynOrgBilling> | null\n const plan = aiConfirmedPlan(job)\n const creation = aiPlanCreation(plan, 'component')\n const name = creation?.name || AI_JOB_COMPONENT_DEFAULT_NAME\n\n // The plan starts from a copy of a component the site has (rule 15): the\n // copy is the draft, and nothing is generated. A source gone since the\n // plan was made is built from the brief instead.\n if (\n creation?.duplicateOf &&\n inventory.components.some((component) => component.id === creation.duplicateOf)\n ) {\n const copy = await duplicate('component', {\n orgId: job.orgId,\n hostId,\n sourceId: creation.duplicateOf,\n name,\n uid: job.createdBy,\n org: org as Record<string, unknown> | null,\n // The same claim the page step makes (AGL-3024): a copy core mints is\n // written under core's own id, never `draftId`, so a step that runs\n // again — a pass resumed, a run cut off after the copy — would mint a\n // second. The job's recorded id makes core replay the one it made.\n attemptKey: draftId,\n })\n if (copy.ok) {\n const hostSubdomain = await aiSiteSubdomain(firestore, hostId)\n return aiUnspentOutcome(model, {\n outputs: [\n output({ id: copy.id, versionId: copy.versionId ?? null, name: copy.name, hostSubdomain }),\n ],\n })\n }\n if (copy.ok === false && copy.status === 403) {\n return aiUnspentOutcome(model, { review: aiLimitReview(copy.error) })\n }\n }\n\n const allowance = await aiDraftAllowanceRefusal(firestore, { kind: 'component', hostId, org })\n if (allowance) return aiUnspentOutcome(model, { review: aiLimitReview(allowance) })\n\n let props: ReusableComponentProp[] = []\n const result = await runValidatedGeneration('component', {\n step: 'job.component',\n model,\n instructions: AI_JOB_COMPONENT_INSTRUCTIONS,\n inventory,\n messages: [{ role: 'user', content: aiJobComponentPrompt(job, plan, name) }],\n tool: aiComponentTool(),\n maxTokens: AI_JOB_COMPONENT_STEP_BUDGET.maxTokens(model),\n ...(AI_ROUTING_TABLE['job.component'].thinking\n ? { thinking: AI_ROUTING_TABLE['job.component'].thinking }\n : {}),\n ...(AI_ROUTING_TABLE['job.component'].effort\n ? { effort: AI_ROUTING_TABLE['job.component'].effort }\n : {}),\n context: { definesComponent: true },\n extend: aiComponentCheck({\n inventory,\n plan,\n onProps: (declared) => {\n props = declared\n },\n }),\n ...(signal ? { signal } : {}),\n })\n const spent = aiGenerationSpent(result)\n if (result.status === 'refused') return { ...spent, refused: true }\n if (result.status === 'needs_input') return { ...spent, review: aiDoctrineReview(result) }\n\n const draft = await writeAiDraft(firestore, {\n kind: 'component',\n hostId,\n id: draftId,\n uid: job.createdBy,\n org,\n name,\n nodes: result.value.nodes,\n rootId: result.value.rootId,\n props,\n now,\n })\n if (draft.ok === false) {\n if (draft.status === 404) throw new Error(`site ${hostId} vanished while its component was generated`)\n return { ...spent, review: aiLimitReview(draft.error) }\n }\n // The facts the brief did not give, in the tree or the defaults a page shows (AGL-3056).\n const note = aiBracketedFactsNote({\n tree: { rootId: result.value.rootId, nodes: result.value.nodes as unknown as AiDoctrineTree['nodes'] },\n inventory,\n copy: props.map((prop) => (typeof prop.defaultValue === 'string' ? prop.defaultValue : '')),\n })\n return { ...spent, outputs: [output(draft, result.value.load, note)] }\n }\n}\n\nexport const runAiJobComponentStep = createAiJobComponentStep()\n\n/** Registers the component step and the check a component job passes before it is created or resumed. */\nexport function registerAiComponentJob(): void {\n registerAiJobStep('component', runAiJobComponentStep, { minimumMs: AI_JOB_COMPONENT_STEP_MINIMUM_MS })\n registerAiJobAdmission('component', aiComponentJobAdmission)\n}\n"],"names":["duplicateResource","AI_STEP_TIERS","AI_ROUTING_TABLE","aiModelForStep","runValidatedGeneration","readSiteInventory","AI_COMPONENT_TOOL_NAME","aiComponentPropKindWords","aiComponentTool","registerAiJobAdmission","aiJobStepBudget","aiComponentCheck","aiJobDraftId","aiDraftAdmissionRefusal","aiDraftAllowanceRefusal","aiSiteSubdomain","readAiDraft","writeAiDraft","aiBracketedFactsNote","aiConfirmedPlan","aiDoctrineReview","aiGenerationSpent","aiJobBriefLine","aiLimitReview","aiPlanCreation","aiPlanReferenceLines","aiUnspentOutcome","registerAiJobStep","AI_JOB_COMPONENT_MAX_TOKENS","maxTokens","AI_JOB_COMPONENT_STEP_BUDGET","tier","AI_JOB_COMPONENT_STEP_MINIMUM_MS","minimumMs","AI_JOB_COMPONENT_DEFAULT_NAME","AI_JOB_COMPONENT_INSTRUCTIONS","text","join","aiJobComponentPrompt","job","plan","name","aiComponentJobAdmission","context","firestore","orgId","hostId","kind","org","createAiJobComponentStep","deps","readInventory","duplicate","now","signal","modelFor","orgSnapshot","Error","output","draft","load","note","resource","id","versionId","hostSubdomain","label","model","draftId","written","outputs","inventory","Promise","all","collection","doc","get","data","creation","duplicateOf","components","some","component","copy","sourceId","uid","createdBy","attemptKey","ok","status","review","error","allowance","props","result","step","instructions","messages","role","content","tool","thinking","effort","definesComponent","extend","onProps","declared","spent","refused","nodes","value","rootId","tree","map","prop","defaultValue","runAiJobComponentStep","registerAiComponentJob"],"mappings":"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"}
|
|
@@ -499,7 +499,12 @@ export function createAiJobFormStep(deps = {}) {
|
|
|
499
499
|
sourceId: creation.duplicateOf,
|
|
500
500
|
name,
|
|
501
501
|
uid: job.createdBy,
|
|
502
|
-
org: org
|
|
502
|
+
org: org,
|
|
503
|
+
// The same claim the page step makes (AGL-3024): a copy core mints is
|
|
504
|
+
// written under core's own id, never `draftId`, so a step that runs
|
|
505
|
+
// again — a pass resumed, a run cut off after the copy — would mint a
|
|
506
|
+
// second. The job's recorded id makes core replay the one it made.
|
|
507
|
+
attemptKey: draftId
|
|
503
508
|
});
|
|
504
509
|
if (copy.ok) {
|
|
505
510
|
// The copy is the whole output, so the contract is asked of it here
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-form-step.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n checkFormContract,\n formFieldsCanYieldAnEmail,\n} from '@aglyn/aglyn/app-utils/form-contract'\nimport {\n collectFormFieldNodeIds,\n FORM_COMPONENT_ID,\n FORM_FIELD_COMPONENT_ID,\n FORM_ID_PROP,\n formFieldDeclsFromNodes,\n formNodeIdIn,\n isMarketingConsentFieldName,\n MARKETING_CONSENT_FORM_FIELD,\n type FormDocument,\n type FormFieldDecl,\n type FormRouting,\n} from '@aglyn/aglyn/app-utils/forms'\nimport { decodeStoredNodes } from '@aglyn/aglyn/app-utils/stored-nodes'\nimport { CANVAS_ROOT_ELEMENT_ID } from '@aglyn/aglyn/foundation/constants/canvas'\nimport type { AglynOrgBilling } from '@aglyn/aglyn/foundation/definitions/org-billing.types'\nimport { NodeType, type NodesMap } from '@aglyn/aglyn/types/nodes'\nimport { duplicateResource } from '@aglyn/tenant-data-admin/server/duplicate-resource'\nimport type { AiJob, AiJobOutput, AiJobPlan } from '../model/ai-jobs.types'\nimport { aiSiteSubmissions, aiSiteWords, type AiSiteSubmissions } from '../model/ai-site-job'\nimport { AI_STEP_TIERS } from '../providers/catalog'\nimport { AI_ROUTING_TABLE, aiModelForStep } from '../providers/routing'\nimport {\n aiDoctrineTreeTool,\n runValidatedGeneration,\n type AiValidatedTree,\n} from '../runtime/ai-doctrine'\nimport type { AiDoctrineTree, AiDoctrineViolation } from '../runtime/ai-doctrine-validators'\nimport type { AiLoadEstimate } from '../runtime/ai-palette'\nimport { AI_PALETTE } from '../runtime/ai-palette.generated'\nimport type { AiSystemBlock, AiTool } from '../runtime/ai-runtime'\nimport { readSiteInventory } from '../runtime/site-inventory'\nimport { registerAiJobAdmission, type AiJobAdmission } from './ai-job-admission'\nimport { aiJobDraftId } from './ai-job-draft-ids'\nimport {\n aiDraftAdmissionRefusal,\n aiDraftAllowanceRefusal,\n aiSiteSubdomain,\n readAiDraft,\n writeAiDraft,\n type AiDraftRecord,\n} from './ai-job-drafts'\nimport {\n aiBracketedFactsNote,\n aiConfirmedPlan,\n aiDoctrineReview,\n aiGenerationSpent,\n aiJobBriefLine,\n aiLimitReview,\n aiModelNodeIds,\n aiPlanCreation,\n aiPlanReferenceLines,\n aiUnspentOutcome,\n} from './ai-job-generation'\nimport type { AiJobStepRunner } from './ai-job-text-step'\nimport { aiJobStepBudget } from './ai-job-budget'\nimport { registerAiJobStep } from './ai-jobs'\n\n/**\n * The form step (AGL-2913): the generation step of a `form` job, run once a\n * member confirmed the job's plan.\n *\n * A form is two halves that must agree for its submissions to arrive: the\n * DESIGN a visitor fills in (`rootId`, `nodes`) and the DECLARATION the submit\n * route reads (`fields`, `consentFieldName`, `routing`). `checkFormContract`\n * is the rule that holds them together when a form is published. So the model\n * writes only the design, through `runValidatedGeneration('form', …)`, and\n * everything else is derived from it with the functions the publish paths\n * use:\n *\n * - `fields` is `formFieldDeclsFromNodes` over the design;\n * - a form that can yield an email address gets the platform's marketing\n * consent field (`MARKETING_CONSENT_FORM_FIELD`, the Forms editor preset's own\n * props) in place of any consent-like field the model drew, named as its\n * `consentFieldName`;\n * - `routing` is `{ lead: true }` when the answer proposes a lead, and\n * nothing otherwise. The stored routing has no place for an email list,\n * so a list proposal leaves the form on the Inbox and says, in the output's\n * `note`, how to enroll the people who tick consent — naming a list only\n * when the brief named one. The step reads no list, contact, CRM record or\n * submission, and sends none.\n *\n * The design is then stamped the way the Forms page's Create stamps one — the\n * form node bound to the new form's id, a canvas root above it — and\n * `checkFormContract` runs on it through `extend`, beside the doctrine's own\n * checks, so a design the contract would refuse at publish is asked for again\n * with the violation named.\n *\n * The form lands as a new draft (`ai-job-drafts.ts`) that no page places and\n * nothing promotes. A plan that starts from a copy of a form the site has gets\n * that copy, through the platform's duplicate module, and nothing generated —\n * and because that path reaches no model, `extend` never runs on it, so the\n * step asks `checkFormContract` of the stored copy itself before it reports\n * one (AGL-3024). A copy still bound to the form it was copied from is a\n * failure, never an output.\n *\n * Like the other generation steps, it runs on the job beat, never at an inline\n * door: it registers the least time its lookup rounds, its answer and its\n * re-ask need (AGL-3035), and runs without extended thinking under the routing\n * table's ceiling for a form, which is the doctrine's own.\n */\n\n/**\n * The longest answer a form may run to: the routing table's `job.form`\n * ceiling, which holds the largest form the output budget admits\n * (`ai-job-form-step.spec.ts` measures it) and is the doctrine's ceiling for\n * the form kind.\n */\nexport const AI_JOB_FORM_MAX_TOKENS = AI_ROUTING_TABLE['job.form'].maxTokens\n\n/**\n * The form step's time (AGL-3035, AGL-3036): its two inventory-lookup rounds,\n * its answer and its re-ask at that ceiling on the tier `job.form` is served\n * from, fitted to what a beat can give a step.\n */\nexport const AI_JOB_FORM_STEP_BUDGET = aiJobStepBudget({\n tier: AI_STEP_TIERS['job.form'],\n maxTokens: AI_JOB_FORM_MAX_TOKENS,\n})\n\n/** The least time one form step needs before it starts. */\nexport const AI_JOB_FORM_STEP_MINIMUM_MS = AI_JOB_FORM_STEP_BUDGET.minimumMs\n\n/** A form's name when the plan names none. */\nexport const AI_JOB_FORM_DEFAULT_NAME = 'New form'\n\n/** Where a proposal routes a form's submissions. */\nexport const AI_FORM_ROUTING_KINDS = ['inbox', 'list', 'lead'] as const\n\nexport type AiFormRoutingKind = (typeof AI_FORM_ROUTING_KINDS)[number]\n\n/** The most things a note says the brief asked for and a form cannot collect. */\nexport const AI_FORM_CANNOT_COLLECT_MAX = 3\n\nconst CANNOT_COLLECT_MAX_CHARS = 60\nconst LIST_NAME_MAX_CHARS = 80\n\n/** The node id the platform's consent field takes in a generated design. */\nexport const AI_FORM_CONSENT_NODE_ID = 'marketingConsentField'\n\n/** The form step's own instructions, cached after the doctrine. */\nexport const AI_JOB_FORM_INSTRUCTIONS: readonly AiSystemBlock[] = [\n {\n text: [\n 'You build one form for a website: the fields a visitor fills in and the button that sends them. Answer with submit_form: the whole form as one flat node map, how its submissions are routed, and what the brief asks for that a form cannot collect.',\n 'The root is one Form (form) holding its Form Fields (formField) in the order a visitor fills them in. Give the form a submitLabel and a successMessage in the site’s voice.',\n 'Every field has a fieldName that is unique in the form and says what it holds in camelCase, such as fullName, email, phone or roofType; a label a visitor reads; and required only when the form cannot do its job without it. Use fieldType email for an email address, textarea for a description, select or radio with options for one choice, checkbox with options for several, and rating for a score. Put each option on its own line and never put a comma inside an option, because a comma starts a new one.',\n 'A form collects text, choices and ratings only. When the brief asks for something else, such as a file or photo upload, a signature or a payment, leave it out and name it in cannotCollect.',\n 'Never draw a marketing consent, newsletter or subscribe checkbox: when the form asks for an email address, the platform adds its own.',\n 'routing.kind is inbox when submissions only need to be read, lead when each one is a sales lead to follow up, and list when the people who send it should join an email list. routing.list is the list’s name only when the brief names one, and null otherwise.',\n ].join('\\n'),\n },\n]\n\nconst TREE_TOOL = aiDoctrineTreeTool('form')\n\n/**\n * The doctrine's form tree tool, with the two things a design cannot say: the\n * routing it proposes and what the brief asked for that no field can collect.\n * Structured fields only, so the step writes every sentence a person reads.\n */\nexport const AI_JOB_FORM_TOOL: AiTool = {\n name: TREE_TOOL.name,\n description:\n 'Submit the form as one flat node map, how its submissions are routed, and what the brief asks for that a form cannot collect.',\n strict: true,\n inputSchema: {\n type: 'object',\n additionalProperties: false,\n required: ['tree', 'routing', 'cannotCollect'],\n properties: {\n tree: (TREE_TOOL.inputSchema['properties'] as Record<string, unknown>)['tree'],\n routing: {\n type: 'object',\n additionalProperties: false,\n required: ['kind', 'list'],\n properties: {\n kind: {\n type: 'string',\n enum: [...AI_FORM_ROUTING_KINDS],\n description:\n 'inbox: submissions are read in the Inbox. lead: each submission with an email address is a sales lead. list: the people who send it should join an email list.',\n },\n list: {\n anyOf: [{ type: 'string' }, { type: 'null' }],\n description: 'The email list the brief names, as it names it; null when it names none.',\n },\n },\n },\n cannotCollect: {\n type: 'array',\n items: { type: 'string' },\n description:\n 'What the brief asks the form to collect that its field types cannot, a few words each, such as \"photo upload\"; empty when there is nothing.',\n },\n },\n },\n}\n\n/** The routing and the gaps an answer proposes, read defensively. */\nexport interface AiFormAnswer {\n routing: { kind: AiFormRoutingKind; list: string | null }\n cannotCollect: string[]\n}\n\n/**\n * What the person already decided about this form, where they were asked\n * (AGL-2918).\n *\n * Only the guided start asks, so only a scaffold's form unit carries one.\n * Whether a submission is a note to read or a lead to chase is a fact about\n * how a business runs, not something readable off a brief, so an answer here\n * BINDS the routing instead of joining the evidence the model weighs: a\n * person who said \"the Inbox\" and got a form filing leads would have been\n * asked a question for nothing.\n */\nexport interface AiFormDecisions {\n /** Where submissions go; `null` where nobody was asked. */\n submissions: AiSiteSubmissions | null\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\nfunction oneLine(value: string, max: number): string {\n return value.replace(/\\s+/g, ' ').trim().slice(0, max).trim()\n}\n\n/**\n * An answer's routing and gaps: an unknown kind is the Inbox, and the rest is\n * trimmed and capped. Where the person was asked where submissions go, their\n * answer replaces the model's proposal — the whole of the difference a\n * question makes.\n */\nexport function parseAiFormAnswer(\n answer: Record<string, unknown>,\n decisions: AiFormDecisions = { submissions: null },\n): AiFormAnswer {\n const routing = isRecord(answer['routing']) ? answer['routing'] : {}\n const proposed = (AI_FORM_ROUTING_KINDS as readonly unknown[]).includes(routing['kind'])\n ? (routing['kind'] as AiFormRoutingKind)\n : 'inbox'\n const kind: AiFormRoutingKind = decisions.submissions ?? proposed\n const list = typeof routing['list'] === 'string' ? oneLine(routing['list'], LIST_NAME_MAX_CHARS) : ''\n const seen = new Set<string>()\n const cannotCollect: string[] = []\n for (const entry of Array.isArray(answer['cannotCollect']) ? answer['cannotCollect'] : []) {\n if (typeof entry !== 'string') continue\n const text = oneLine(entry, CANNOT_COLLECT_MAX_CHARS)\n if (!text || seen.has(text.toLowerCase())) continue\n seen.add(text.toLowerCase())\n cannotCollect.push(text)\n if (cannotCollect.length === AI_FORM_CANNOT_COLLECT_MAX) break\n }\n return { routing: { kind, list: kind === 'list' && list ? list : null }, cannotCollect }\n}\n\n/** A generated form as its draft is written: the stamped design and what it declares. */\nexport interface AiFormDraft {\n /** The design as the Forms page's Create stores one: a canvas root above the bound form. */\n nodes: NodesMap\n rootId: string\n formNodeId: string\n fields: FormFieldDecl[]\n consentFieldName: string | null\n routing: FormRouting | null\n answer: AiFormAnswer\n}\n\n/**\n * The draft a validated design and its answer make: the form node bound to\n * `formId` and captioned `name`, no dataset binding, the platform's consent\n * field when the fields can yield an email address, the canvas root above,\n * and the declaration read off the result. Pure; the input tree is not\n * changed.\n */\nexport function aiFormDraft(\n tree: Pick<AiValidatedTree, 'rootId' | 'nodes'>,\n answer: Record<string, unknown>,\n identity: { formId: string; name: string; decisions?: AiFormDecisions },\n): AiFormDraft {\n const parsed = parseAiFormAnswer(answer, identity.decisions ?? { submissions: null })\n const formNodeId = tree.rootId\n const nodes: NodesMap = {}\n for (const [id, node] of Object.entries(tree.nodes)) {\n nodes[id] = {\n ...node,\n props: { ...(node.props ?? {}) },\n nodes: Array.isArray(node.nodes) ? [...(node.nodes as string[])] : [],\n }\n }\n const form = nodes[formNodeId]\n let consentFieldName: string | null = null\n if (form?.componentId === FORM_COMPONENT_ID) {\n const props = form.props as Record<string, unknown>\n // A dataset binding decides where every submission is also written; it\n // stays the person's choice, made on the Form element.\n delete props['datasetId']\n delete props['datasetName']\n props[FORM_ID_PROP] = identity.formId\n props['formName'] = identity.name\n\n for (const id of collectFormFieldNodeIds(nodes as never, formNodeId)) {\n const fieldName = (nodes[id]?.props as Record<string, unknown> | undefined)?.['fieldName']\n if (!isMarketingConsentFieldName(fieldName)) continue\n const parent = nodes[String(nodes[id]?.parentId ?? '')]\n if (parent && Array.isArray(parent.nodes)) {\n parent.nodes = (parent.nodes as string[]).filter((child) => child !== id)\n }\n delete nodes[id]\n }\n if (formFieldsCanYieldAnEmail(formFieldDeclsFromNodes(nodes as never, formNodeId))) {\n nodes[AI_FORM_CONSENT_NODE_ID] = {\n $id: AI_FORM_CONSENT_NODE_ID,\n type: NodeType.NODE,\n componentId: FORM_FIELD_COMPONENT_ID,\n pluginId: AI_PALETTE[FORM_FIELD_COMPONENT_ID]?.pluginId,\n parentId: formNodeId,\n nodes: [],\n props: { ...MARKETING_CONSENT_FORM_FIELD },\n } as NodesMap[string]\n form.nodes = [...(form.nodes as string[]), AI_FORM_CONSENT_NODE_ID]\n consentFieldName = MARKETING_CONSENT_FORM_FIELD.fieldName\n }\n form.parentId = CANVAS_ROOT_ELEMENT_ID\n }\n nodes[CANVAS_ROOT_ELEMENT_ID] = {\n $id: CANVAS_ROOT_ELEMENT_ID,\n componentId: 'div',\n nodes: [formNodeId],\n } as NodesMap[string]\n return {\n nodes,\n rootId: CANVAS_ROOT_ELEMENT_ID,\n formNodeId,\n fields: formFieldDeclsFromNodes(nodes as never, formNodeId),\n consentFieldName,\n routing: parsed.routing.kind === 'lead' ? { lead: true } : null,\n answer: parsed,\n }\n}\n\n/**\n * What a draft would break once published, as building-rule violations\n * (rule 3: a form is built with its fields, validation, consent and routing\n * together): the form contract's findings, a form with no field, and a list\n * proposal on a form that asks for no email address. Offending nodes are\n * named by the ids the model wrote.\n */\nexport function aiFormDraftViolations(\n draft: AiFormDraft,\n formId: string,\n sourceIds: Readonly<Record<string, string>>,\n): AiDoctrineViolation[] {\n const violations: AiDoctrineViolation[] = checkFormContract({\n form: {\n ...(draft.routing ? { routing: draft.routing } : {}),\n ...(draft.consentFieldName ? { consentFieldName: draft.consentFieldName } : {}),\n },\n formId,\n nodes: draft.nodes as never,\n formNodeId: draft.formNodeId,\n }).map((violation) => ({\n rule: 3 as const,\n code: violation.code,\n message: violation.message,\n ...(violation.nodeId ? { nodeIds: aiModelNodeIds([violation.nodeId], sourceIds) } : {}),\n }))\n if (!draft.fields.length) {\n violations.push({\n rule: 3,\n code: 'form-has-no-fields',\n message:\n 'This form has no named fields, so a visitor has nothing to fill in. Draw the fields the brief asks for.',\n })\n }\n if (draft.answer.routing.kind === 'list' && !formFieldsCanYieldAnEmail(draft.fields)) {\n violations.push({\n rule: 3,\n code: 'list-routing-has-no-email-field',\n message:\n 'This form proposes adding the people who send it to an email list, and it asks for no email address. Add an email field, or route its submissions to the Inbox.',\n paths: ['routing.kind'],\n })\n }\n return violations\n}\n\n/**\n * The door's check for `extend`, and the draft each admitted tree made: the\n * tree a generation returns is the object its last check saw, so the step\n * writes exactly the draft the contract passed.\n */\nexport function aiFormDraftCheck(identity: {\n formId: string\n name: string\n decisions?: AiFormDecisions\n}): {\n extend: (tree: AiValidatedTree, answer: Record<string, unknown>) => AiDoctrineViolation[]\n draftFor: (tree: AiValidatedTree) => AiFormDraft | undefined\n} {\n const drafts = new WeakMap<AiValidatedTree, AiFormDraft>()\n return {\n extend: (tree, answer) => {\n const draft = aiFormDraft(tree, answer, identity)\n drafts.set(tree, draft)\n return aiFormDraftViolations(draft, identity.formId, tree.sourceIds)\n },\n draftFor: (tree) => drafts.get(tree),\n }\n}\n\n/**\n * What a job says when the copy it asked for came back naming the form it\n * was copied FROM (AGL-3024). The contract's own sentence follows it and\n * names the consequence; this one names what to do about it.\n */\nexport const AI_FORM_COPY_UNBOUND_FAILURE =\n 'The copy of that form did not take on an identity of its own, so it is on ' +\n 'the site and not safe to collect with — delete it in Forms and try again.'\n\n/**\n * Whether the copy the platform just wrote is bound to ITSELF, asked of the\n * document rather than of the answer — `null` when it is, and the sentence a\n * person reads when it is not.\n *\n * ## Why a step that generated nothing still has something to check\n *\n * A \"creates X from a copy of Y\" plan reaches no model: the copy IS the\n * output, so there is no answer for the doctrine to refuse and `extend`\n * never runs. That left the one thing a copy can get wrong unasked. A form's\n * design names its own form inside itself, and a copy that kept the source's\n * name files every submission under the source — the console's form page\n * banners exactly this, and the job reported Done beside it.\n *\n * So the check is `checkFormContract`, the SAME function the console's\n * banner and the promotion route ask, run here on the stored copy. Not a\n * second predicate shaped like it: a copy this one passed and that one\n * refused would be a form the job called finished and the console called\n * broken, which is the state this exists to make impossible.\n *\n * Narrowed to `form-id-unbound`, and only that, because it is the one\n * violation duplication CAUSES. Every other code the contract can report —\n * an unnamed field, a consent field the design lost — the copy inherited\n * from a form the site already has and the member chose to copy; refusing\n * those would be refusing to copy a form they can already see.\n */\nexport async function aiFormCopyUnboundFailure(\n firestore: FirebaseFirestore.Firestore,\n copy: { hostId: string; formId: string },\n): Promise<string | null> {\n const snapshot = await firestore\n .collection('hosts')\n .doc(copy.hostId)\n .collection('forms')\n .doc(copy.formId)\n .get()\n const form = (snapshot.data() ?? null) as Partial<FormDocument> | null\n const nodes = decodeStoredNodes(form?.nodes)\n const unbound = checkFormContract({\n form,\n formId: copy.formId,\n nodes: nodes as never,\n formNodeId: formNodeIdIn(nodes as never),\n }).find((violation) => violation.code === 'form-id-unbound')\n return unbound ? `${AI_FORM_COPY_UNBOUND_FAILURE} ${unbound.message}` : null\n}\n\nfunction listOf(items: readonly string[]): string {\n return items.length < 2\n ? items.join('')\n : `${items.slice(0, -1).join(', ')} and ${items[items.length - 1]}`\n}\n\n/**\n * What the person decides next, in words the step writes itself: a lead\n * routing to keep or switch off, the list a proposal could not store, and what\n * the brief asked for that a form cannot collect. `null` when there is none.\n */\nexport function aiFormOutputNote(answer: AiFormAnswer): string | null {\n const sentences: string[] = []\n if (answer.routing.kind === 'lead') {\n sentences.push(\n 'Each submission with an email address also files a lead in CRM → Leads; you can switch that off on the form’s page.',\n )\n }\n if (answer.routing.kind === 'list') {\n const list = answer.routing.list ? `your \"${answer.routing.list}\" list` : 'an email list'\n sentences.push(\n `Submissions arrive in the Inbox. To add the people who tick \"${MARKETING_CONSENT_FORM_FIELD.label}\" to ${list}, add an action on form submissions in Automation that enrolls them.`,\n )\n }\n if (answer.cannotCollect.length) {\n sentences.push(\n `Forms cannot collect ${listOf(answer.cannotCollect)} yet, so this form leaves ${\n answer.cannotCollect.length === 1 ? 'it' : 'them'\n } out.`,\n )\n }\n return sentences.length ? sentences.join(' ') : null\n}\n\n/**\n * The form job as the generation's user turn: the name, who fills the form\n * in, the brief, and the confirmed plan.\n *\n * Who the form is for (AGL-2918) is the answer a contact form's FIELDS turn\n * on — a trades business asks where the work is, a practice asks what the\n * appointment is about — and it is not something a brief about a whole site\n * says in a place the form can find. It rides here, in the turn, because it\n * is one site's own words.\n *\n * Where submissions go rides here too, and it is NOT a hint: the step binds\n * the routing to it whatever the model answers. It is stated so the model\n * does not spend its answer proposing something that will be replaced, and\n * so a form told its submissions are leads asks for the address that makes\n * one.\n */\nexport function aiJobFormPrompt(\n job: Pick<AiJob, 'brief'>,\n plan: AiJobPlan | null,\n name: string,\n context: { audience?: string; submissions?: AiSiteSubmissions | null } = {},\n): string {\n const audience = (context.audience ?? '').trim()\n return [\n `Form name: ${name}`,\n audience ? `The people who fill it in: ${audience}` : '',\n context.submissions ? `Where its submissions go: ${SUBMISSION_WORDS[context.submissions]}` : '',\n aiJobBriefLine(job),\n ...aiPlanReferenceLines(plan),\n ]\n .filter(Boolean)\n .join('\\n')\n}\n\n/** What each decided routing is, said to the model the way the tool says it. */\nconst SUBMISSION_WORDS: Record<AiSiteSubmissions, string> = {\n inbox: 'the Inbox, to be read — routing.kind is inbox',\n lead: 'the Inbox, and each one with an email address is a sales lead — routing.kind is lead',\n}\n\n/** A form job is admitted for a site of the job's own org whose plan has forms, with a form to spare. */\nexport const aiFormJobAdmission: AiJobAdmission = (context) =>\n aiDraftAdmissionRefusal(context.firestore, {\n orgId: context.orgId,\n hostId: context.hostId,\n kind: 'form',\n org: context.org,\n })\n\nexport interface AiJobFormStepDeps {\n /** The inventory reader; specs hand in a fake. */\n readInventory?: typeof readSiteInventory\n /** The platform's duplicate module, for a plan that starts from a copy. */\n duplicate?: typeof duplicateResource\n}\n\nexport function createAiJobFormStep(deps: AiJobFormStepDeps = {}): AiJobStepRunner {\n const readInventory = deps.readInventory ?? readSiteInventory\n const duplicate = deps.duplicate ?? duplicateResource\n return async ({ job, now, signal, firestore, modelFor }) => {\n const hostId = job.hostId\n if (!hostId) throw new Error('a form job names no site, and its admission refuses one')\n const output = (\n draft: AiDraftRecord,\n extra: { load?: AiLoadEstimate | null; note?: string | null } = {},\n ): AiJobOutput => ({\n resource: 'form',\n id: draft.id,\n versionId: draft.versionId,\n hostId,\n hostSubdomain: draft.hostSubdomain,\n label: draft.name,\n ...(extra.load ? { load: extra.load } : {}),\n ...(extra.note ? { note: extra.note } : {}),\n })\n // The switch's answer for this job, else the routing table's (AGL-2942).\n const model = modelFor?.('job.form') ?? aiModelForStep('job.form')\n\n // A run cut off after its draft was written reports that draft, found by the id the job recorded.\n const draftId = aiJobDraftId(job, 'form')\n const written = await readAiDraft(firestore, { kind: 'form', hostId, id: draftId })\n if (written) return aiUnspentOutcome(model, { outputs: [output(written)] })\n\n const [inventory, orgSnapshot] = await Promise.all([\n readInventory(job.orgId, hostId, { firestore }),\n firestore.collection('orgs').doc(job.orgId).get(),\n ])\n const org = (orgSnapshot.data() ?? null) as Partial<AglynOrgBilling> | null\n const plan = aiConfirmedPlan(job)\n const creation = aiPlanCreation(plan, 'form')\n const name = creation?.name || AI_JOB_FORM_DEFAULT_NAME\n\n // The plan starts from a copy of a form the site has (rule 15): the copy\n // is the draft, and nothing is generated. A source gone since the plan\n // was made is built from the brief instead.\n if (creation?.duplicateOf && inventory.forms.some((form) => form.id === creation.duplicateOf)) {\n const copy = await duplicate('form', {\n orgId: job.orgId,\n hostId,\n sourceId: creation.duplicateOf,\n name,\n uid: job.createdBy,\n org: org as Record<string, unknown> | null,\n })\n if (copy.ok) {\n // The copy is the whole output, so the contract is asked of it here\n // or nowhere: a copy still bound to its source is not a form this\n // job may report as built (AGL-3024).\n const unbound = await aiFormCopyUnboundFailure(firestore, { hostId, formId: copy.id })\n if (unbound) return { ...aiUnspentOutcome(model), failure: unbound }\n const hostSubdomain = await aiSiteSubdomain(firestore, hostId)\n return aiUnspentOutcome(model, {\n outputs: [output({ id: copy.id, versionId: copy.versionId, name: copy.name, hostSubdomain })],\n })\n }\n if (copy.ok === false && copy.status === 403) {\n return aiUnspentOutcome(model, { review: aiLimitReview(copy.error) })\n }\n }\n\n const allowance = await aiDraftAllowanceRefusal(firestore, { kind: 'form', hostId, org })\n if (allowance) return aiUnspentOutcome(model, { review: aiLimitReview(allowance) })\n\n // What the person answered about this form, where they were asked\n // (AGL-2918): a scaffold's form unit carries the guided start's inputs.\n const decisions: AiFormDecisions = { submissions: aiSiteSubmissions(job.inputs) }\n const { audience } = aiSiteWords(job.inputs)\n const check = aiFormDraftCheck({ formId: draftId, name, decisions })\n const result = await runValidatedGeneration('form', {\n step: 'job.form',\n model,\n instructions: AI_JOB_FORM_INSTRUCTIONS,\n inventory,\n messages: [\n {\n role: 'user',\n content: aiJobFormPrompt(job, plan, name, {\n audience,\n submissions: decisions.submissions,\n }),\n },\n ],\n tool: AI_JOB_FORM_TOOL,\n maxTokens: AI_JOB_FORM_STEP_BUDGET.maxTokens(model),\n thinking: 'off',\n extend: check.extend,\n ...(signal ? { signal } : {}),\n })\n const spent = aiGenerationSpent(result)\n if (result.status === 'refused') return { ...spent, refused: true }\n if (result.status === 'needs_input') return { ...spent, review: aiDoctrineReview(result) }\n\n const draft = check.draftFor(result.value)\n if (!draft) throw new Error('a form the doctrine admitted has no draft from its check')\n const write = await writeAiDraft(firestore, {\n kind: 'form',\n hostId,\n id: draftId,\n uid: job.createdBy,\n org,\n name,\n nodes: draft.nodes,\n form: {\n rootId: draft.rootId,\n formNodeId: draft.formNodeId,\n fields: draft.fields,\n consentFieldName: draft.consentFieldName,\n routing: draft.routing,\n },\n now,\n })\n if (write.ok === false) {\n if (write.status === 404) throw new Error(`site ${hostId} vanished while its form was generated`)\n return { ...spent, review: aiLimitReview(write.error) }\n }\n // What the person decides next, then the facts the brief did not give (AGL-3056).\n const note = [\n aiFormOutputNote(draft.answer),\n aiBracketedFactsNote({\n tree: { rootId: result.value.rootId, nodes: result.value.nodes as unknown as AiDoctrineTree['nodes'] },\n inventory,\n }),\n ]\n .filter(Boolean)\n .join(' ')\n return {\n ...spent,\n outputs: [output(write, { load: result.value.load, note })],\n }\n }\n}\n\nexport const runAiJobFormStep = createAiJobFormStep()\n\n/** Registers the form step and the check a form job passes before it is created or resumed. */\nexport function registerAiFormJob(): void {\n registerAiJobStep('form', runAiJobFormStep, { minimumMs: AI_JOB_FORM_STEP_MINIMUM_MS })\n registerAiJobAdmission('form', aiFormJobAdmission)\n}\n"],"names":["checkFormContract","formFieldsCanYieldAnEmail","collectFormFieldNodeIds","FORM_COMPONENT_ID","FORM_FIELD_COMPONENT_ID","FORM_ID_PROP","formFieldDeclsFromNodes","formNodeIdIn","isMarketingConsentFieldName","MARKETING_CONSENT_FORM_FIELD","decodeStoredNodes","CANVAS_ROOT_ELEMENT_ID","NodeType","duplicateResource","aiSiteSubmissions","aiSiteWords","AI_STEP_TIERS","AI_ROUTING_TABLE","aiModelForStep","aiDoctrineTreeTool","runValidatedGeneration","AI_PALETTE","readSiteInventory","registerAiJobAdmission","aiJobDraftId","aiDraftAdmissionRefusal","aiDraftAllowanceRefusal","aiSiteSubdomain","readAiDraft","writeAiDraft","aiBracketedFactsNote","aiConfirmedPlan","aiDoctrineReview","aiGenerationSpent","aiJobBriefLine","aiLimitReview","aiModelNodeIds","aiPlanCreation","aiPlanReferenceLines","aiUnspentOutcome","aiJobStepBudget","registerAiJobStep","AI_JOB_FORM_MAX_TOKENS","maxTokens","AI_JOB_FORM_STEP_BUDGET","tier","AI_JOB_FORM_STEP_MINIMUM_MS","minimumMs","AI_JOB_FORM_DEFAULT_NAME","AI_FORM_ROUTING_KINDS","AI_FORM_CANNOT_COLLECT_MAX","CANNOT_COLLECT_MAX_CHARS","LIST_NAME_MAX_CHARS","AI_FORM_CONSENT_NODE_ID","AI_JOB_FORM_INSTRUCTIONS","text","join","TREE_TOOL","AI_JOB_FORM_TOOL","name","description","strict","inputSchema","type","additionalProperties","required","properties","tree","routing","kind","enum","list","anyOf","cannotCollect","items","isRecord","value","Array","isArray","oneLine","max","replace","trim","slice","parseAiFormAnswer","answer","decisions","submissions","proposed","includes","seen","Set","entry","has","toLowerCase","add","push","length","aiFormDraft","identity","parsed","formNodeId","rootId","nodes","id","node","Object","entries","props","form","consentFieldName","componentId","formId","fieldName","parent","String","parentId","filter","child","$id","NODE","pluginId","fields","lead","aiFormDraftViolations","draft","sourceIds","violations","map","violation","rule","code","message","nodeId","nodeIds","paths","aiFormDraftCheck","drafts","WeakMap","extend","set","draftFor","get","AI_FORM_COPY_UNBOUND_FAILURE","aiFormCopyUnboundFailure","firestore","copy","snapshot","collection","doc","hostId","data","unbound","find","listOf","aiFormOutputNote","sentences","label","aiJobFormPrompt","job","plan","context","audience","SUBMISSION_WORDS","Boolean","inbox","aiFormJobAdmission","orgId","org","createAiJobFormStep","deps","readInventory","duplicate","now","signal","modelFor","orgSnapshot","Error","output","extra","resource","versionId","hostSubdomain","load","note","model","draftId","written","outputs","inventory","Promise","all","creation","duplicateOf","forms","some","sourceId","uid","createdBy","ok","failure","status","review","error","allowance","inputs","check","result","step","instructions","messages","role","content","tool","thinking","spent","refused","write","runAiJobFormStep","registerAiFormJob"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-form-step.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n checkFormContract,\n formFieldsCanYieldAnEmail,\n} from '@aglyn/aglyn/app-utils/form-contract'\nimport {\n collectFormFieldNodeIds,\n FORM_COMPONENT_ID,\n FORM_FIELD_COMPONENT_ID,\n FORM_ID_PROP,\n formFieldDeclsFromNodes,\n formNodeIdIn,\n isMarketingConsentFieldName,\n MARKETING_CONSENT_FORM_FIELD,\n type FormDocument,\n type FormFieldDecl,\n type FormRouting,\n} from '@aglyn/aglyn/app-utils/forms'\nimport { decodeStoredNodes } from '@aglyn/aglyn/app-utils/stored-nodes'\nimport { CANVAS_ROOT_ELEMENT_ID } from '@aglyn/aglyn/foundation/constants/canvas'\nimport type { AglynOrgBilling } from '@aglyn/aglyn/foundation/definitions/org-billing.types'\nimport { NodeType, type NodesMap } from '@aglyn/aglyn/types/nodes'\nimport { duplicateResource } from '@aglyn/tenant-data-admin/server/duplicate-resource'\nimport type { AiJob, AiJobOutput, AiJobPlan } from '../model/ai-jobs.types'\nimport { aiSiteSubmissions, aiSiteWords, type AiSiteSubmissions } from '../model/ai-site-job'\nimport { AI_STEP_TIERS } from '../providers/catalog'\nimport { AI_ROUTING_TABLE, aiModelForStep } from '../providers/routing'\nimport {\n aiDoctrineTreeTool,\n runValidatedGeneration,\n type AiValidatedTree,\n} from '../runtime/ai-doctrine'\nimport type { AiDoctrineTree, AiDoctrineViolation } from '../runtime/ai-doctrine-validators'\nimport type { AiLoadEstimate } from '../runtime/ai-palette'\nimport { AI_PALETTE } from '../runtime/ai-palette.generated'\nimport type { AiSystemBlock, AiTool } from '../runtime/ai-runtime'\nimport { readSiteInventory } from '../runtime/site-inventory'\nimport { registerAiJobAdmission, type AiJobAdmission } from './ai-job-admission'\nimport { aiJobDraftId } from './ai-job-draft-ids'\nimport {\n aiDraftAdmissionRefusal,\n aiDraftAllowanceRefusal,\n aiSiteSubdomain,\n readAiDraft,\n writeAiDraft,\n type AiDraftRecord,\n} from './ai-job-drafts'\nimport {\n aiBracketedFactsNote,\n aiConfirmedPlan,\n aiDoctrineReview,\n aiGenerationSpent,\n aiJobBriefLine,\n aiLimitReview,\n aiModelNodeIds,\n aiPlanCreation,\n aiPlanReferenceLines,\n aiUnspentOutcome,\n} from './ai-job-generation'\nimport type { AiJobStepRunner } from './ai-job-text-step'\nimport { aiJobStepBudget } from './ai-job-budget'\nimport { registerAiJobStep } from './ai-jobs'\n\n/**\n * The form step (AGL-2913): the generation step of a `form` job, run once a\n * member confirmed the job's plan.\n *\n * A form is two halves that must agree for its submissions to arrive: the\n * DESIGN a visitor fills in (`rootId`, `nodes`) and the DECLARATION the submit\n * route reads (`fields`, `consentFieldName`, `routing`). `checkFormContract`\n * is the rule that holds them together when a form is published. So the model\n * writes only the design, through `runValidatedGeneration('form', …)`, and\n * everything else is derived from it with the functions the publish paths\n * use:\n *\n * - `fields` is `formFieldDeclsFromNodes` over the design;\n * - a form that can yield an email address gets the platform's marketing\n * consent field (`MARKETING_CONSENT_FORM_FIELD`, the Forms editor preset's own\n * props) in place of any consent-like field the model drew, named as its\n * `consentFieldName`;\n * - `routing` is `{ lead: true }` when the answer proposes a lead, and\n * nothing otherwise. The stored routing has no place for an email list,\n * so a list proposal leaves the form on the Inbox and says, in the output's\n * `note`, how to enroll the people who tick consent — naming a list only\n * when the brief named one. The step reads no list, contact, CRM record or\n * submission, and sends none.\n *\n * The design is then stamped the way the Forms page's Create stamps one — the\n * form node bound to the new form's id, a canvas root above it — and\n * `checkFormContract` runs on it through `extend`, beside the doctrine's own\n * checks, so a design the contract would refuse at publish is asked for again\n * with the violation named.\n *\n * The form lands as a new draft (`ai-job-drafts.ts`) that no page places and\n * nothing promotes. A plan that starts from a copy of a form the site has gets\n * that copy, through the platform's duplicate module, and nothing generated —\n * and because that path reaches no model, `extend` never runs on it, so the\n * step asks `checkFormContract` of the stored copy itself before it reports\n * one (AGL-3024). A copy still bound to the form it was copied from is a\n * failure, never an output.\n *\n * Like the other generation steps, it runs on the job beat, never at an inline\n * door: it registers the least time its lookup rounds, its answer and its\n * re-ask need (AGL-3035), and runs without extended thinking under the routing\n * table's ceiling for a form, which is the doctrine's own.\n */\n\n/**\n * The longest answer a form may run to: the routing table's `job.form`\n * ceiling, which holds the largest form the output budget admits\n * (`ai-job-form-step.spec.ts` measures it) and is the doctrine's ceiling for\n * the form kind.\n */\nexport const AI_JOB_FORM_MAX_TOKENS = AI_ROUTING_TABLE['job.form'].maxTokens\n\n/**\n * The form step's time (AGL-3035, AGL-3036): its two inventory-lookup rounds,\n * its answer and its re-ask at that ceiling on the tier `job.form` is served\n * from, fitted to what a beat can give a step.\n */\nexport const AI_JOB_FORM_STEP_BUDGET = aiJobStepBudget({\n tier: AI_STEP_TIERS['job.form'],\n maxTokens: AI_JOB_FORM_MAX_TOKENS,\n})\n\n/** The least time one form step needs before it starts. */\nexport const AI_JOB_FORM_STEP_MINIMUM_MS = AI_JOB_FORM_STEP_BUDGET.minimumMs\n\n/** A form's name when the plan names none. */\nexport const AI_JOB_FORM_DEFAULT_NAME = 'New form'\n\n/** Where a proposal routes a form's submissions. */\nexport const AI_FORM_ROUTING_KINDS = ['inbox', 'list', 'lead'] as const\n\nexport type AiFormRoutingKind = (typeof AI_FORM_ROUTING_KINDS)[number]\n\n/** The most things a note says the brief asked for and a form cannot collect. */\nexport const AI_FORM_CANNOT_COLLECT_MAX = 3\n\nconst CANNOT_COLLECT_MAX_CHARS = 60\nconst LIST_NAME_MAX_CHARS = 80\n\n/** The node id the platform's consent field takes in a generated design. */\nexport const AI_FORM_CONSENT_NODE_ID = 'marketingConsentField'\n\n/** The form step's own instructions, cached after the doctrine. */\nexport const AI_JOB_FORM_INSTRUCTIONS: readonly AiSystemBlock[] = [\n {\n text: [\n 'You build one form for a website: the fields a visitor fills in and the button that sends them. Answer with submit_form: the whole form as one flat node map, how its submissions are routed, and what the brief asks for that a form cannot collect.',\n 'The root is one Form (form) holding its Form Fields (formField) in the order a visitor fills them in. Give the form a submitLabel and a successMessage in the site’s voice.',\n 'Every field has a fieldName that is unique in the form and says what it holds in camelCase, such as fullName, email, phone or roofType; a label a visitor reads; and required only when the form cannot do its job without it. Use fieldType email for an email address, textarea for a description, select or radio with options for one choice, checkbox with options for several, and rating for a score. Put each option on its own line and never put a comma inside an option, because a comma starts a new one.',\n 'A form collects text, choices and ratings only. When the brief asks for something else, such as a file or photo upload, a signature or a payment, leave it out and name it in cannotCollect.',\n 'Never draw a marketing consent, newsletter or subscribe checkbox: when the form asks for an email address, the platform adds its own.',\n 'routing.kind is inbox when submissions only need to be read, lead when each one is a sales lead to follow up, and list when the people who send it should join an email list. routing.list is the list’s name only when the brief names one, and null otherwise.',\n ].join('\\n'),\n },\n]\n\nconst TREE_TOOL = aiDoctrineTreeTool('form')\n\n/**\n * The doctrine's form tree tool, with the two things a design cannot say: the\n * routing it proposes and what the brief asked for that no field can collect.\n * Structured fields only, so the step writes every sentence a person reads.\n */\nexport const AI_JOB_FORM_TOOL: AiTool = {\n name: TREE_TOOL.name,\n description:\n 'Submit the form as one flat node map, how its submissions are routed, and what the brief asks for that a form cannot collect.',\n strict: true,\n inputSchema: {\n type: 'object',\n additionalProperties: false,\n required: ['tree', 'routing', 'cannotCollect'],\n properties: {\n tree: (TREE_TOOL.inputSchema['properties'] as Record<string, unknown>)['tree'],\n routing: {\n type: 'object',\n additionalProperties: false,\n required: ['kind', 'list'],\n properties: {\n kind: {\n type: 'string',\n enum: [...AI_FORM_ROUTING_KINDS],\n description:\n 'inbox: submissions are read in the Inbox. lead: each submission with an email address is a sales lead. list: the people who send it should join an email list.',\n },\n list: {\n anyOf: [{ type: 'string' }, { type: 'null' }],\n description: 'The email list the brief names, as it names it; null when it names none.',\n },\n },\n },\n cannotCollect: {\n type: 'array',\n items: { type: 'string' },\n description:\n 'What the brief asks the form to collect that its field types cannot, a few words each, such as \"photo upload\"; empty when there is nothing.',\n },\n },\n },\n}\n\n/** The routing and the gaps an answer proposes, read defensively. */\nexport interface AiFormAnswer {\n routing: { kind: AiFormRoutingKind; list: string | null }\n cannotCollect: string[]\n}\n\n/**\n * What the person already decided about this form, where they were asked\n * (AGL-2918).\n *\n * Only the guided start asks, so only a scaffold's form unit carries one.\n * Whether a submission is a note to read or a lead to chase is a fact about\n * how a business runs, not something readable off a brief, so an answer here\n * BINDS the routing instead of joining the evidence the model weighs: a\n * person who said \"the Inbox\" and got a form filing leads would have been\n * asked a question for nothing.\n */\nexport interface AiFormDecisions {\n /** Where submissions go; `null` where nobody was asked. */\n submissions: AiSiteSubmissions | null\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\nfunction oneLine(value: string, max: number): string {\n return value.replace(/\\s+/g, ' ').trim().slice(0, max).trim()\n}\n\n/**\n * An answer's routing and gaps: an unknown kind is the Inbox, and the rest is\n * trimmed and capped. Where the person was asked where submissions go, their\n * answer replaces the model's proposal — the whole of the difference a\n * question makes.\n */\nexport function parseAiFormAnswer(\n answer: Record<string, unknown>,\n decisions: AiFormDecisions = { submissions: null },\n): AiFormAnswer {\n const routing = isRecord(answer['routing']) ? answer['routing'] : {}\n const proposed = (AI_FORM_ROUTING_KINDS as readonly unknown[]).includes(routing['kind'])\n ? (routing['kind'] as AiFormRoutingKind)\n : 'inbox'\n const kind: AiFormRoutingKind = decisions.submissions ?? proposed\n const list = typeof routing['list'] === 'string' ? oneLine(routing['list'], LIST_NAME_MAX_CHARS) : ''\n const seen = new Set<string>()\n const cannotCollect: string[] = []\n for (const entry of Array.isArray(answer['cannotCollect']) ? answer['cannotCollect'] : []) {\n if (typeof entry !== 'string') continue\n const text = oneLine(entry, CANNOT_COLLECT_MAX_CHARS)\n if (!text || seen.has(text.toLowerCase())) continue\n seen.add(text.toLowerCase())\n cannotCollect.push(text)\n if (cannotCollect.length === AI_FORM_CANNOT_COLLECT_MAX) break\n }\n return { routing: { kind, list: kind === 'list' && list ? list : null }, cannotCollect }\n}\n\n/** A generated form as its draft is written: the stamped design and what it declares. */\nexport interface AiFormDraft {\n /** The design as the Forms page's Create stores one: a canvas root above the bound form. */\n nodes: NodesMap\n rootId: string\n formNodeId: string\n fields: FormFieldDecl[]\n consentFieldName: string | null\n routing: FormRouting | null\n answer: AiFormAnswer\n}\n\n/**\n * The draft a validated design and its answer make: the form node bound to\n * `formId` and captioned `name`, no dataset binding, the platform's consent\n * field when the fields can yield an email address, the canvas root above,\n * and the declaration read off the result. Pure; the input tree is not\n * changed.\n */\nexport function aiFormDraft(\n tree: Pick<AiValidatedTree, 'rootId' | 'nodes'>,\n answer: Record<string, unknown>,\n identity: { formId: string; name: string; decisions?: AiFormDecisions },\n): AiFormDraft {\n const parsed = parseAiFormAnswer(answer, identity.decisions ?? { submissions: null })\n const formNodeId = tree.rootId\n const nodes: NodesMap = {}\n for (const [id, node] of Object.entries(tree.nodes)) {\n nodes[id] = {\n ...node,\n props: { ...(node.props ?? {}) },\n nodes: Array.isArray(node.nodes) ? [...(node.nodes as string[])] : [],\n }\n }\n const form = nodes[formNodeId]\n let consentFieldName: string | null = null\n if (form?.componentId === FORM_COMPONENT_ID) {\n const props = form.props as Record<string, unknown>\n // A dataset binding decides where every submission is also written; it\n // stays the person's choice, made on the Form element.\n delete props['datasetId']\n delete props['datasetName']\n props[FORM_ID_PROP] = identity.formId\n props['formName'] = identity.name\n\n for (const id of collectFormFieldNodeIds(nodes as never, formNodeId)) {\n const fieldName = (nodes[id]?.props as Record<string, unknown> | undefined)?.['fieldName']\n if (!isMarketingConsentFieldName(fieldName)) continue\n const parent = nodes[String(nodes[id]?.parentId ?? '')]\n if (parent && Array.isArray(parent.nodes)) {\n parent.nodes = (parent.nodes as string[]).filter((child) => child !== id)\n }\n delete nodes[id]\n }\n if (formFieldsCanYieldAnEmail(formFieldDeclsFromNodes(nodes as never, formNodeId))) {\n nodes[AI_FORM_CONSENT_NODE_ID] = {\n $id: AI_FORM_CONSENT_NODE_ID,\n type: NodeType.NODE,\n componentId: FORM_FIELD_COMPONENT_ID,\n pluginId: AI_PALETTE[FORM_FIELD_COMPONENT_ID]?.pluginId,\n parentId: formNodeId,\n nodes: [],\n props: { ...MARKETING_CONSENT_FORM_FIELD },\n } as NodesMap[string]\n form.nodes = [...(form.nodes as string[]), AI_FORM_CONSENT_NODE_ID]\n consentFieldName = MARKETING_CONSENT_FORM_FIELD.fieldName\n }\n form.parentId = CANVAS_ROOT_ELEMENT_ID\n }\n nodes[CANVAS_ROOT_ELEMENT_ID] = {\n $id: CANVAS_ROOT_ELEMENT_ID,\n componentId: 'div',\n nodes: [formNodeId],\n } as NodesMap[string]\n return {\n nodes,\n rootId: CANVAS_ROOT_ELEMENT_ID,\n formNodeId,\n fields: formFieldDeclsFromNodes(nodes as never, formNodeId),\n consentFieldName,\n routing: parsed.routing.kind === 'lead' ? { lead: true } : null,\n answer: parsed,\n }\n}\n\n/**\n * What a draft would break once published, as building-rule violations\n * (rule 3: a form is built with its fields, validation, consent and routing\n * together): the form contract's findings, a form with no field, and a list\n * proposal on a form that asks for no email address. Offending nodes are\n * named by the ids the model wrote.\n */\nexport function aiFormDraftViolations(\n draft: AiFormDraft,\n formId: string,\n sourceIds: Readonly<Record<string, string>>,\n): AiDoctrineViolation[] {\n const violations: AiDoctrineViolation[] = checkFormContract({\n form: {\n ...(draft.routing ? { routing: draft.routing } : {}),\n ...(draft.consentFieldName ? { consentFieldName: draft.consentFieldName } : {}),\n },\n formId,\n nodes: draft.nodes as never,\n formNodeId: draft.formNodeId,\n }).map((violation) => ({\n rule: 3 as const,\n code: violation.code,\n message: violation.message,\n ...(violation.nodeId ? { nodeIds: aiModelNodeIds([violation.nodeId], sourceIds) } : {}),\n }))\n if (!draft.fields.length) {\n violations.push({\n rule: 3,\n code: 'form-has-no-fields',\n message:\n 'This form has no named fields, so a visitor has nothing to fill in. Draw the fields the brief asks for.',\n })\n }\n if (draft.answer.routing.kind === 'list' && !formFieldsCanYieldAnEmail(draft.fields)) {\n violations.push({\n rule: 3,\n code: 'list-routing-has-no-email-field',\n message:\n 'This form proposes adding the people who send it to an email list, and it asks for no email address. Add an email field, or route its submissions to the Inbox.',\n paths: ['routing.kind'],\n })\n }\n return violations\n}\n\n/**\n * The door's check for `extend`, and the draft each admitted tree made: the\n * tree a generation returns is the object its last check saw, so the step\n * writes exactly the draft the contract passed.\n */\nexport function aiFormDraftCheck(identity: {\n formId: string\n name: string\n decisions?: AiFormDecisions\n}): {\n extend: (tree: AiValidatedTree, answer: Record<string, unknown>) => AiDoctrineViolation[]\n draftFor: (tree: AiValidatedTree) => AiFormDraft | undefined\n} {\n const drafts = new WeakMap<AiValidatedTree, AiFormDraft>()\n return {\n extend: (tree, answer) => {\n const draft = aiFormDraft(tree, answer, identity)\n drafts.set(tree, draft)\n return aiFormDraftViolations(draft, identity.formId, tree.sourceIds)\n },\n draftFor: (tree) => drafts.get(tree),\n }\n}\n\n/**\n * What a job says when the copy it asked for came back naming the form it\n * was copied FROM (AGL-3024). The contract's own sentence follows it and\n * names the consequence; this one names what to do about it.\n */\nexport const AI_FORM_COPY_UNBOUND_FAILURE =\n 'The copy of that form did not take on an identity of its own, so it is on ' +\n 'the site and not safe to collect with — delete it in Forms and try again.'\n\n/**\n * Whether the copy the platform just wrote is bound to ITSELF, asked of the\n * document rather than of the answer — `null` when it is, and the sentence a\n * person reads when it is not.\n *\n * ## Why a step that generated nothing still has something to check\n *\n * A \"creates X from a copy of Y\" plan reaches no model: the copy IS the\n * output, so there is no answer for the doctrine to refuse and `extend`\n * never runs. That left the one thing a copy can get wrong unasked. A form's\n * design names its own form inside itself, and a copy that kept the source's\n * name files every submission under the source — the console's form page\n * banners exactly this, and the job reported Done beside it.\n *\n * So the check is `checkFormContract`, the SAME function the console's\n * banner and the promotion route ask, run here on the stored copy. Not a\n * second predicate shaped like it: a copy this one passed and that one\n * refused would be a form the job called finished and the console called\n * broken, which is the state this exists to make impossible.\n *\n * Narrowed to `form-id-unbound`, and only that, because it is the one\n * violation duplication CAUSES. Every other code the contract can report —\n * an unnamed field, a consent field the design lost — the copy inherited\n * from a form the site already has and the member chose to copy; refusing\n * those would be refusing to copy a form they can already see.\n */\nexport async function aiFormCopyUnboundFailure(\n firestore: FirebaseFirestore.Firestore,\n copy: { hostId: string; formId: string },\n): Promise<string | null> {\n const snapshot = await firestore\n .collection('hosts')\n .doc(copy.hostId)\n .collection('forms')\n .doc(copy.formId)\n .get()\n const form = (snapshot.data() ?? null) as Partial<FormDocument> | null\n const nodes = decodeStoredNodes(form?.nodes)\n const unbound = checkFormContract({\n form,\n formId: copy.formId,\n nodes: nodes as never,\n formNodeId: formNodeIdIn(nodes as never),\n }).find((violation) => violation.code === 'form-id-unbound')\n return unbound ? `${AI_FORM_COPY_UNBOUND_FAILURE} ${unbound.message}` : null\n}\n\nfunction listOf(items: readonly string[]): string {\n return items.length < 2\n ? items.join('')\n : `${items.slice(0, -1).join(', ')} and ${items[items.length - 1]}`\n}\n\n/**\n * What the person decides next, in words the step writes itself: a lead\n * routing to keep or switch off, the list a proposal could not store, and what\n * the brief asked for that a form cannot collect. `null` when there is none.\n */\nexport function aiFormOutputNote(answer: AiFormAnswer): string | null {\n const sentences: string[] = []\n if (answer.routing.kind === 'lead') {\n sentences.push(\n 'Each submission with an email address also files a lead in CRM → Leads; you can switch that off on the form’s page.',\n )\n }\n if (answer.routing.kind === 'list') {\n const list = answer.routing.list ? `your \"${answer.routing.list}\" list` : 'an email list'\n sentences.push(\n `Submissions arrive in the Inbox. To add the people who tick \"${MARKETING_CONSENT_FORM_FIELD.label}\" to ${list}, add an action on form submissions in Automation that enrolls them.`,\n )\n }\n if (answer.cannotCollect.length) {\n sentences.push(\n `Forms cannot collect ${listOf(answer.cannotCollect)} yet, so this form leaves ${\n answer.cannotCollect.length === 1 ? 'it' : 'them'\n } out.`,\n )\n }\n return sentences.length ? sentences.join(' ') : null\n}\n\n/**\n * The form job as the generation's user turn: the name, who fills the form\n * in, the brief, and the confirmed plan.\n *\n * Who the form is for (AGL-2918) is the answer a contact form's FIELDS turn\n * on — a trades business asks where the work is, a practice asks what the\n * appointment is about — and it is not something a brief about a whole site\n * says in a place the form can find. It rides here, in the turn, because it\n * is one site's own words.\n *\n * Where submissions go rides here too, and it is NOT a hint: the step binds\n * the routing to it whatever the model answers. It is stated so the model\n * does not spend its answer proposing something that will be replaced, and\n * so a form told its submissions are leads asks for the address that makes\n * one.\n */\nexport function aiJobFormPrompt(\n job: Pick<AiJob, 'brief'>,\n plan: AiJobPlan | null,\n name: string,\n context: { audience?: string; submissions?: AiSiteSubmissions | null } = {},\n): string {\n const audience = (context.audience ?? '').trim()\n return [\n `Form name: ${name}`,\n audience ? `The people who fill it in: ${audience}` : '',\n context.submissions ? `Where its submissions go: ${SUBMISSION_WORDS[context.submissions]}` : '',\n aiJobBriefLine(job),\n ...aiPlanReferenceLines(plan),\n ]\n .filter(Boolean)\n .join('\\n')\n}\n\n/** What each decided routing is, said to the model the way the tool says it. */\nconst SUBMISSION_WORDS: Record<AiSiteSubmissions, string> = {\n inbox: 'the Inbox, to be read — routing.kind is inbox',\n lead: 'the Inbox, and each one with an email address is a sales lead — routing.kind is lead',\n}\n\n/** A form job is admitted for a site of the job's own org whose plan has forms, with a form to spare. */\nexport const aiFormJobAdmission: AiJobAdmission = (context) =>\n aiDraftAdmissionRefusal(context.firestore, {\n orgId: context.orgId,\n hostId: context.hostId,\n kind: 'form',\n org: context.org,\n })\n\nexport interface AiJobFormStepDeps {\n /** The inventory reader; specs hand in a fake. */\n readInventory?: typeof readSiteInventory\n /** The platform's duplicate module, for a plan that starts from a copy. */\n duplicate?: typeof duplicateResource\n}\n\nexport function createAiJobFormStep(deps: AiJobFormStepDeps = {}): AiJobStepRunner {\n const readInventory = deps.readInventory ?? readSiteInventory\n const duplicate = deps.duplicate ?? duplicateResource\n return async ({ job, now, signal, firestore, modelFor }) => {\n const hostId = job.hostId\n if (!hostId) throw new Error('a form job names no site, and its admission refuses one')\n const output = (\n draft: AiDraftRecord,\n extra: { load?: AiLoadEstimate | null; note?: string | null } = {},\n ): AiJobOutput => ({\n resource: 'form',\n id: draft.id,\n versionId: draft.versionId,\n hostId,\n hostSubdomain: draft.hostSubdomain,\n label: draft.name,\n ...(extra.load ? { load: extra.load } : {}),\n ...(extra.note ? { note: extra.note } : {}),\n })\n // The switch's answer for this job, else the routing table's (AGL-2942).\n const model = modelFor?.('job.form') ?? aiModelForStep('job.form')\n\n // A run cut off after its draft was written reports that draft, found by the id the job recorded.\n const draftId = aiJobDraftId(job, 'form')\n const written = await readAiDraft(firestore, { kind: 'form', hostId, id: draftId })\n if (written) return aiUnspentOutcome(model, { outputs: [output(written)] })\n\n const [inventory, orgSnapshot] = await Promise.all([\n readInventory(job.orgId, hostId, { firestore }),\n firestore.collection('orgs').doc(job.orgId).get(),\n ])\n const org = (orgSnapshot.data() ?? null) as Partial<AglynOrgBilling> | null\n const plan = aiConfirmedPlan(job)\n const creation = aiPlanCreation(plan, 'form')\n const name = creation?.name || AI_JOB_FORM_DEFAULT_NAME\n\n // The plan starts from a copy of a form the site has (rule 15): the copy\n // is the draft, and nothing is generated. A source gone since the plan\n // was made is built from the brief instead.\n if (creation?.duplicateOf && inventory.forms.some((form) => form.id === creation.duplicateOf)) {\n const copy = await duplicate('form', {\n orgId: job.orgId,\n hostId,\n sourceId: creation.duplicateOf,\n name,\n uid: job.createdBy,\n org: org as Record<string, unknown> | null,\n // The same claim the page step makes (AGL-3024): a copy core mints is\n // written under core's own id, never `draftId`, so a step that runs\n // again — a pass resumed, a run cut off after the copy — would mint a\n // second. The job's recorded id makes core replay the one it made.\n attemptKey: draftId,\n })\n if (copy.ok) {\n // The copy is the whole output, so the contract is asked of it here\n // or nowhere: a copy still bound to its source is not a form this\n // job may report as built (AGL-3024).\n const unbound = await aiFormCopyUnboundFailure(firestore, { hostId, formId: copy.id })\n if (unbound) return { ...aiUnspentOutcome(model), failure: unbound }\n const hostSubdomain = await aiSiteSubdomain(firestore, hostId)\n return aiUnspentOutcome(model, {\n outputs: [output({ id: copy.id, versionId: copy.versionId, name: copy.name, hostSubdomain })],\n })\n }\n if (copy.ok === false && copy.status === 403) {\n return aiUnspentOutcome(model, { review: aiLimitReview(copy.error) })\n }\n }\n\n const allowance = await aiDraftAllowanceRefusal(firestore, { kind: 'form', hostId, org })\n if (allowance) return aiUnspentOutcome(model, { review: aiLimitReview(allowance) })\n\n // What the person answered about this form, where they were asked\n // (AGL-2918): a scaffold's form unit carries the guided start's inputs.\n const decisions: AiFormDecisions = { submissions: aiSiteSubmissions(job.inputs) }\n const { audience } = aiSiteWords(job.inputs)\n const check = aiFormDraftCheck({ formId: draftId, name, decisions })\n const result = await runValidatedGeneration('form', {\n step: 'job.form',\n model,\n instructions: AI_JOB_FORM_INSTRUCTIONS,\n inventory,\n messages: [\n {\n role: 'user',\n content: aiJobFormPrompt(job, plan, name, {\n audience,\n submissions: decisions.submissions,\n }),\n },\n ],\n tool: AI_JOB_FORM_TOOL,\n maxTokens: AI_JOB_FORM_STEP_BUDGET.maxTokens(model),\n thinking: 'off',\n extend: check.extend,\n ...(signal ? { signal } : {}),\n })\n const spent = aiGenerationSpent(result)\n if (result.status === 'refused') return { ...spent, refused: true }\n if (result.status === 'needs_input') return { ...spent, review: aiDoctrineReview(result) }\n\n const draft = check.draftFor(result.value)\n if (!draft) throw new Error('a form the doctrine admitted has no draft from its check')\n const write = await writeAiDraft(firestore, {\n kind: 'form',\n hostId,\n id: draftId,\n uid: job.createdBy,\n org,\n name,\n nodes: draft.nodes,\n form: {\n rootId: draft.rootId,\n formNodeId: draft.formNodeId,\n fields: draft.fields,\n consentFieldName: draft.consentFieldName,\n routing: draft.routing,\n },\n now,\n })\n if (write.ok === false) {\n if (write.status === 404) throw new Error(`site ${hostId} vanished while its form was generated`)\n return { ...spent, review: aiLimitReview(write.error) }\n }\n // What the person decides next, then the facts the brief did not give (AGL-3056).\n const note = [\n aiFormOutputNote(draft.answer),\n aiBracketedFactsNote({\n tree: { rootId: result.value.rootId, nodes: result.value.nodes as unknown as AiDoctrineTree['nodes'] },\n inventory,\n }),\n ]\n .filter(Boolean)\n .join(' ')\n return {\n ...spent,\n outputs: [output(write, { load: result.value.load, note })],\n }\n }\n}\n\nexport const runAiJobFormStep = createAiJobFormStep()\n\n/** Registers the form step and the check a form job passes before it is created or resumed. */\nexport function registerAiFormJob(): void {\n registerAiJobStep('form', runAiJobFormStep, { minimumMs: AI_JOB_FORM_STEP_MINIMUM_MS })\n registerAiJobAdmission('form', aiFormJobAdmission)\n}\n"],"names":["checkFormContract","formFieldsCanYieldAnEmail","collectFormFieldNodeIds","FORM_COMPONENT_ID","FORM_FIELD_COMPONENT_ID","FORM_ID_PROP","formFieldDeclsFromNodes","formNodeIdIn","isMarketingConsentFieldName","MARKETING_CONSENT_FORM_FIELD","decodeStoredNodes","CANVAS_ROOT_ELEMENT_ID","NodeType","duplicateResource","aiSiteSubmissions","aiSiteWords","AI_STEP_TIERS","AI_ROUTING_TABLE","aiModelForStep","aiDoctrineTreeTool","runValidatedGeneration","AI_PALETTE","readSiteInventory","registerAiJobAdmission","aiJobDraftId","aiDraftAdmissionRefusal","aiDraftAllowanceRefusal","aiSiteSubdomain","readAiDraft","writeAiDraft","aiBracketedFactsNote","aiConfirmedPlan","aiDoctrineReview","aiGenerationSpent","aiJobBriefLine","aiLimitReview","aiModelNodeIds","aiPlanCreation","aiPlanReferenceLines","aiUnspentOutcome","aiJobStepBudget","registerAiJobStep","AI_JOB_FORM_MAX_TOKENS","maxTokens","AI_JOB_FORM_STEP_BUDGET","tier","AI_JOB_FORM_STEP_MINIMUM_MS","minimumMs","AI_JOB_FORM_DEFAULT_NAME","AI_FORM_ROUTING_KINDS","AI_FORM_CANNOT_COLLECT_MAX","CANNOT_COLLECT_MAX_CHARS","LIST_NAME_MAX_CHARS","AI_FORM_CONSENT_NODE_ID","AI_JOB_FORM_INSTRUCTIONS","text","join","TREE_TOOL","AI_JOB_FORM_TOOL","name","description","strict","inputSchema","type","additionalProperties","required","properties","tree","routing","kind","enum","list","anyOf","cannotCollect","items","isRecord","value","Array","isArray","oneLine","max","replace","trim","slice","parseAiFormAnswer","answer","decisions","submissions","proposed","includes","seen","Set","entry","has","toLowerCase","add","push","length","aiFormDraft","identity","parsed","formNodeId","rootId","nodes","id","node","Object","entries","props","form","consentFieldName","componentId","formId","fieldName","parent","String","parentId","filter","child","$id","NODE","pluginId","fields","lead","aiFormDraftViolations","draft","sourceIds","violations","map","violation","rule","code","message","nodeId","nodeIds","paths","aiFormDraftCheck","drafts","WeakMap","extend","set","draftFor","get","AI_FORM_COPY_UNBOUND_FAILURE","aiFormCopyUnboundFailure","firestore","copy","snapshot","collection","doc","hostId","data","unbound","find","listOf","aiFormOutputNote","sentences","label","aiJobFormPrompt","job","plan","context","audience","SUBMISSION_WORDS","Boolean","inbox","aiFormJobAdmission","orgId","org","createAiJobFormStep","deps","readInventory","duplicate","now","signal","modelFor","orgSnapshot","Error","output","extra","resource","versionId","hostSubdomain","load","note","model","draftId","written","outputs","inventory","Promise","all","creation","duplicateOf","forms","some","sourceId","uid","createdBy","attemptKey","ok","failure","status","review","error","allowance","inputs","check","result","step","instructions","messages","role","content","tool","thinking","spent","refused","write","runAiJobFormStep","registerAiFormJob"],"mappings":"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"}
|
|
@@ -224,7 +224,12 @@ export function createAiJobLayoutStep(deps = {}) {
|
|
|
224
224
|
sourceId: creation.duplicateOf,
|
|
225
225
|
name,
|
|
226
226
|
uid: job.createdBy,
|
|
227
|
-
org: org
|
|
227
|
+
org: org,
|
|
228
|
+
// The same claim the page step makes (AGL-3024): a copy core mints is
|
|
229
|
+
// written under core's own id, never `draftId`, so a step that runs
|
|
230
|
+
// again — a pass resumed, a run cut off after the copy — would mint a
|
|
231
|
+
// second. The job's recorded id makes core replay the one it made.
|
|
232
|
+
attemptKey: draftId
|
|
228
233
|
});
|
|
229
234
|
if (copy.ok) {
|
|
230
235
|
// A copy is not a build (AGL-3024). This branch generates nothing, so
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-layout-step.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { CANVAS_ROOT_ELEMENT_ID } from '@aglyn/aglyn/foundation/constants/canvas'\nimport type { AglynOrgBilling } from '@aglyn/aglyn/foundation/definitions/org-billing.types'\nimport { duplicateResource } from '@aglyn/tenant-data-admin/server/duplicate-resource'\nimport type { AiJob, AiJobOutput, AiJobPlan, AiJobReview } from '../model/ai-jobs.types'\nimport type { AiSiteInventory } from '../model/ai-site-inventory'\nimport { AI_STEP_TIERS } from '../providers/catalog'\nimport { AI_ROUTING_TABLE, aiModelForStep } from '../providers/routing'\nimport {\n aiDoctrineTreeTool,\n runValidatedGeneration,\n type AiValidatedTree,\n} from '../runtime/ai-doctrine'\nimport {\n walkTree,\n type AiDoctrineNode,\n type AiDoctrineTree,\n type AiDoctrineViolation,\n} from '../runtime/ai-doctrine-validators'\nimport type { AiLoadEstimate } from '../runtime/ai-palette'\nimport type { AiSystemBlock } from '../runtime/ai-runtime'\nimport { readSiteInventory } from '../runtime/site-inventory'\nimport { registerAiJobAdmission, type AiJobAdmission } from './ai-job-admission'\nimport { aiJobDraftId } from './ai-job-draft-ids'\nimport {\n aiDraftAdmissionRefusal,\n aiDraftAllowanceRefusal,\n aiSiteSubdomain,\n readAiDraft,\n readAiDraftNodes,\n writeAiDraft,\n type AiDraftRecord,\n} from './ai-job-drafts'\nimport {\n aiBracketedFactsNote,\n aiConfirmedPlan,\n aiDoctrineReview,\n aiGenerationSpent,\n aiJobBriefLine,\n aiLimitReview,\n aiModelNodeIds,\n aiPlacedComponentIds,\n aiPlanCreation,\n aiPlanReferenceLines,\n aiPlanReuseViolations,\n aiUnspentOutcome,\n} from './ai-job-generation'\nimport { aiPlanRegionViolations, aiPlannedLayoutRegions } from './ai-job-plan-conformance'\nimport type { AiJobStepRunner } from './ai-job-text-step'\nimport { aiJobStepBudget } from './ai-job-budget'\nimport { registerAiJobStep } from './ai-jobs'\n\n/**\n * The layout step (AGL-2909): the generation step of a `layout` job, run once\n * a member confirmed the job's plan.\n *\n * It builds ONE shared layout — a header with the site's navigation, the\n * Layout Slot each page renders in, a footer — through\n * `runValidatedGeneration('layout', …)`, so every building rule holds on the\n * tree before it is kept, and adds three checks of its own through `extend`: a\n * component the confirmed plan reuses is placed (rule 7), every region the\n * plan gives the layout is built (AGL-3024), and a navigation or menu\n * component the site already has is placed rather than drawn again (rule 1).\n *\n * The layout lands as a new draft (`ai-job-drafts.ts`): no screen uses it and\n * no layout nests it, so nothing on the live site changes until a member\n * assigns it. A plan that starts from a copy of a layout the site has gets\n * that copy, through the platform's duplicate module, and nothing generated —\n * so the copy is read back and held to the plan's regions the same way\n * (`aiCopiedLayoutReview`), because a branch that generates nothing is the one\n * branch where a promise can go unmet with no model to answer for it.\n *\n * A generation step runs on the job beat, never at an inline door: it\n * registers the least time its lookup rounds, its answer and its re-ask need\n * (AGL-3035), runs without extended thinking — the plan was reasoned out and\n * confirmed before it — and asks for no more of a tighter ceiling than the\n * doctrine's default for a layout than fits that time on the model it runs.\n */\n\n/** The longest answer a layout may run to; the tree is held to the layout budget either way. */\nexport const AI_JOB_LAYOUT_MAX_TOKENS = AI_ROUTING_TABLE['job.layout'].maxTokens\n\n/**\n * The layout step's time (AGL-3035, AGL-3036): its two inventory-lookup\n * rounds, its answer and its re-ask at the routing table's ceiling on the tier\n * `job.layout` is served from, fitted to what a beat can give a step.\n */\nexport const AI_JOB_LAYOUT_STEP_BUDGET = aiJobStepBudget({\n tier: AI_STEP_TIERS['job.layout'],\n maxTokens: AI_JOB_LAYOUT_MAX_TOKENS,\n})\n\n/** The least time one layout step needs before it starts. */\nexport const AI_JOB_LAYOUT_STEP_MINIMUM_MS = AI_JOB_LAYOUT_STEP_BUDGET.minimumMs\n\n/** A layout's name when the plan names none. */\nexport const AI_JOB_LAYOUT_DEFAULT_NAME = 'Site layout'\n\n/** The layout step's own instructions, cached after the doctrine. */\nexport const AI_JOB_LAYOUT_INSTRUCTIONS: readonly AiSystemBlock[] = [\n {\n text: [\n 'You build one shared layout for a website: the frame every page of the site renders inside. Answer with submit_layout: the whole layout as one flat node map.',\n 'A layout is a header, one Layout Slot and a footer, top to bottom. The header is an App Bar (muiAppBar) holding a Toolbar Content (muiToolbar) with the site’s name as text and its navigation. The footer is a Section (section) whose element is footer.',\n 'Navigation links the site’s own screens by id: a Screen Link (muiScreenLink) whose screenId is a screen id from the site inventory, in the order the brief and the confirmed plan give, seven at most in the header. Never invent a screen id, and never link a screen by its address.',\n 'When the site inventory lists a component whose name says it is navigation, a menu or a mega menu, place it in the header as an instance instead of building the navigation again. Place every component the confirmed plan reuses.',\n 'The Layout Slot (layoutSlot) is where each page renders and is the page’s main landmark: leave its component unset or main, and put nothing inside it. A layout carries no h1, because every page brings its own.',\n ].join('\\n'),\n },\n]\n\n/** The layout job as the generation's user turn: the name, the brief, the confirmed plan. */\nexport function aiJobLayoutPrompt(\n job: Pick<AiJob, 'brief'>,\n plan: AiJobPlan | null,\n name: string,\n): string {\n return [`Layout name: ${name}`, aiJobBriefLine(job), ...aiPlanReferenceLines(plan)].join('\\n')\n}\n\n/** A component name that says it is the site's navigation; a footer's links are not. */\nconst NAVIGATION_NAME = /\\b(?:nav|navigation|navbar|menu|mega ?menu)\\b/i\nconst FOOTER_NAME = /\\bfooter\\b/i\n\n/** A node that links somewhere: a Screen Link, or a Button pointed at a screen or an address. */\nfunction isLink(node: AiDoctrineNode): boolean {\n if (node.componentId === 'muiScreenLink') return true\n return (\n node.componentId === 'muiButton' &&\n (typeof node.props?.['screenId'] === 'string' || typeof node.props?.['href'] === 'string')\n )\n}\n\n/**\n * The layout door's checks on a tree the doctrine admitted: every component\n * the confirmed plan reuses is placed, every region the confirmed plan gives\n * the layout is built (AGL-3024), and navigation the site already keeps as a\n * component is placed rather than drawn again in the header.\n */\nexport function aiLayoutReuseCheck(\n inventory: AiSiteInventory | null,\n plan: AiJobPlan | null,\n): (tree: AiValidatedTree) => AiDoctrineViolation[] {\n const components = inventory?.components ?? []\n const navigation = components.filter(\n (component) => NAVIGATION_NAME.test(component.name) && !FOOTER_NAME.test(component.name),\n )\n return (tree) => {\n const placed = aiPlacedComponentIds(tree)\n const violations: AiDoctrineViolation[] = [\n ...aiPlanReuseViolations(inventory, plan, placed, 'layout'),\n ...aiPlanRegionViolations(plan, {\n rootId: tree.rootId,\n nodes: tree.nodes as unknown as Record<string, AiDoctrineNode>,\n }),\n ]\n if (navigation.length && !navigation.some((component) => placed.has(component.id))) {\n const visits = walkTree({\n rootId: tree.rootId,\n nodes: tree.nodes as unknown as Record<string, AiDoctrineNode>,\n })\n const slot = visits.findIndex(({ node }) => node.componentId === 'layoutSlot')\n const header = slot === -1 ? visits : visits.slice(0, slot)\n const links = header.filter(({ node }) => isLink(node)).map(({ id }) => id)\n if (links.length >= 2) {\n violations.push({\n rule: 1,\n code: 'navigation-rebuilt',\n message: `The site already has the \"${navigation[0].name}\" component. Place it in the header instead of building the navigation again.`,\n nodeIds: aiModelNodeIds(links, tree.sourceIds),\n })\n }\n }\n return violations\n }\n}\n\n/**\n * The regions the confirmed plan gives the layout, against the layout a COPY\n * actually produced (AGL-3024); `null` when the copy keeps the plan, or when\n * the plan promised no region to keep.\n *\n * The duplicate branch generates nothing, so it is the one place a plan's\n * promise can go unmet without a model ever answering for it. A copy that\n * cannot be read — deleted between the write and this read, or stored under a\n * root this check does not know — settles nothing, and a promise this step\n * cannot settle is reviewed by a person rather than reported as kept.\n */\nexport async function aiCopiedLayoutReview(\n firestore: FirebaseFirestore.Firestore,\n input: { hostId: string; id: string; name: string; plan: AiJobPlan | null },\n): Promise<AiJobReview | null> {\n const planned = aiPlannedLayoutRegions(input.plan)\n if (!planned.regions.length && !planned.unreadable.length) return null\n const stored = await readAiDraftNodes(firestore, {\n kind: 'layout',\n hostId: input.hostId,\n id: input.id,\n })\n const nodes = (stored?.nodes ?? {}) as unknown as Record<string, AiDoctrineNode>\n if (!nodes[CANVAS_ROOT_ELEMENT_ID]) {\n return {\n reason: 'doctrine',\n message: `\"${input.name}\" was copied from the layout the plan names, and this job could not read the copy back to check it has the regions the plan gives it. Open the layout and check it before you put a page in it.`,\n findings: [],\n }\n }\n const violations = aiPlanRegionViolations(input.plan, { rootId: CANVAS_ROOT_ELEMENT_ID, nodes })\n if (!violations.length) return null\n return aiDoctrineReview({\n message: `\"${input.name}\" is a copy of the layout the plan names, and a copy is all it is: ${violations[0].message}`,\n violations,\n })\n}\n\n/** A layout job is admitted for a site of the job's own org with a shared layout to spare. */\nexport const aiLayoutJobAdmission: AiJobAdmission = (context) =>\n aiDraftAdmissionRefusal(context.firestore, {\n orgId: context.orgId,\n hostId: context.hostId,\n kind: 'layout',\n org: context.org,\n })\n\nexport interface AiJobLayoutStepDeps {\n /** The inventory reader; specs hand in a fake. */\n readInventory?: typeof readSiteInventory\n /** The platform's duplicate module, for a plan that starts from a copy. */\n duplicate?: typeof duplicateResource\n}\n\nexport function createAiJobLayoutStep(deps: AiJobLayoutStepDeps = {}): AiJobStepRunner {\n const readInventory = deps.readInventory ?? readSiteInventory\n const duplicate = deps.duplicate ?? duplicateResource\n return async ({ job, now, signal, firestore, modelFor }) => {\n const hostId = job.hostId\n if (!hostId) throw new Error('a layout job names no site, and its admission refuses one')\n const output = (draft: AiDraftRecord, load?: AiLoadEstimate | null, note?: string | null): AiJobOutput => ({\n resource: 'layout',\n id: draft.id,\n versionId: draft.versionId,\n hostId,\n hostSubdomain: draft.hostSubdomain,\n label: draft.name,\n ...(load ? { load } : {}),\n ...(note ? { note } : {}),\n })\n // The switch's answer for this job, else the routing table's (AGL-2942).\n const model = modelFor?.('job.layout') ?? aiModelForStep('job.layout')\n\n // A run cut off after its draft was written reports that draft, found by the id the job recorded.\n const draftId = aiJobDraftId(job, 'layout')\n const written = await readAiDraft(firestore, { kind: 'layout', hostId, id: draftId })\n if (written) return aiUnspentOutcome(model, { outputs: [output(written)] })\n\n const [inventory, orgSnapshot] = await Promise.all([\n readInventory(job.orgId, hostId, { firestore }),\n firestore.collection('orgs').doc(job.orgId).get(),\n ])\n const org = (orgSnapshot.data() ?? null) as Partial<AglynOrgBilling> | null\n const plan = aiConfirmedPlan(job)\n const creation = aiPlanCreation(plan, 'layout')\n const name = creation?.name || AI_JOB_LAYOUT_DEFAULT_NAME\n\n // The plan starts from a copy of a layout the site has (rule 15): the copy\n // is the draft, and nothing is generated. A source gone since the plan\n // was made is built from the brief instead.\n if (creation?.duplicateOf && inventory.layouts.some((layout) => layout.id === creation.duplicateOf)) {\n const copy = await duplicate('layout', {\n orgId: job.orgId,\n hostId,\n sourceId: creation.duplicateOf,\n name,\n uid: job.createdBy,\n org: org as Record<string, unknown> | null,\n })\n if (copy.ok) {\n // A copy is not a build (AGL-3024). This branch generates nothing, so\n // whatever the plan promised BEYOND the source is not in the draft\n // unless the source already had it: a plan reading \"from a copy of the\n // base layout, adding a sidebar\" gets the base layout, the sidebar\n // nowhere in it, and until now reported Done. The copy's own regions\n // answer the plan's, and a copy that cannot be read answers nothing —\n // which is a stop for a person, never a Done.\n const review = await aiCopiedLayoutReview(firestore, { hostId, id: copy.id, plan, name })\n if (review) return aiUnspentOutcome(model, { review })\n const hostSubdomain = await aiSiteSubdomain(firestore, hostId)\n return aiUnspentOutcome(model, {\n outputs: [output({ id: copy.id, versionId: copy.versionId, name: copy.name, hostSubdomain })],\n })\n }\n if (copy.ok === false && copy.status === 403) {\n return aiUnspentOutcome(model, { review: aiLimitReview(copy.error) })\n }\n }\n\n const allowance = await aiDraftAllowanceRefusal(firestore, { kind: 'layout', hostId, org })\n if (allowance) return aiUnspentOutcome(model, { review: aiLimitReview(allowance) })\n\n const result = await runValidatedGeneration('layout', {\n step: 'job.layout',\n model,\n instructions: AI_JOB_LAYOUT_INSTRUCTIONS,\n inventory,\n messages: [{ role: 'user', content: aiJobLayoutPrompt(job, plan, name) }],\n tool: aiDoctrineTreeTool('layout'),\n maxTokens: AI_JOB_LAYOUT_STEP_BUDGET.maxTokens(model),\n ...(AI_ROUTING_TABLE['job.layout'].thinking ? { thinking: AI_ROUTING_TABLE['job.layout'].thinking } : {}),\n ...(AI_ROUTING_TABLE['job.layout'].effort ? { effort: AI_ROUTING_TABLE['job.layout'].effort } : {}),\n extend: aiLayoutReuseCheck(inventory, plan),\n ...(signal ? { signal } : {}),\n })\n const spent = aiGenerationSpent(result)\n if (result.status === 'refused') return { ...spent, refused: true }\n if (result.status === 'needs_input') return { ...spent, review: aiDoctrineReview(result) }\n\n const draft = await writeAiDraft(firestore, {\n kind: 'layout',\n hostId,\n id: draftId,\n uid: job.createdBy,\n org,\n name,\n nodes: result.value.nodes,\n now,\n })\n if (draft.ok === false) {\n if (draft.status === 404) throw new Error(`site ${hostId} vanished while its layout was generated`)\n return { ...spent, review: aiLimitReview(draft.error) }\n }\n // The facts the brief did not give, which the member fills before publishing (AGL-3056).\n const note = aiBracketedFactsNote({\n tree: { rootId: result.value.rootId, nodes: result.value.nodes as unknown as AiDoctrineTree['nodes'] },\n inventory,\n })\n return { ...spent, outputs: [output(draft, result.value.load, note)] }\n }\n}\n\nexport const runAiJobLayoutStep = createAiJobLayoutStep()\n\n/** Registers the layout step and the check a layout job passes before it is created or resumed. */\nexport function registerAiLayoutJob(): void {\n registerAiJobStep('layout', runAiJobLayoutStep, { minimumMs: AI_JOB_LAYOUT_STEP_MINIMUM_MS })\n registerAiJobAdmission('layout', aiLayoutJobAdmission)\n}\n"],"names":["CANVAS_ROOT_ELEMENT_ID","duplicateResource","AI_STEP_TIERS","AI_ROUTING_TABLE","aiModelForStep","aiDoctrineTreeTool","runValidatedGeneration","walkTree","readSiteInventory","registerAiJobAdmission","aiJobDraftId","aiDraftAdmissionRefusal","aiDraftAllowanceRefusal","aiSiteSubdomain","readAiDraft","readAiDraftNodes","writeAiDraft","aiBracketedFactsNote","aiConfirmedPlan","aiDoctrineReview","aiGenerationSpent","aiJobBriefLine","aiLimitReview","aiModelNodeIds","aiPlacedComponentIds","aiPlanCreation","aiPlanReferenceLines","aiPlanReuseViolations","aiUnspentOutcome","aiPlanRegionViolations","aiPlannedLayoutRegions","aiJobStepBudget","registerAiJobStep","AI_JOB_LAYOUT_MAX_TOKENS","maxTokens","AI_JOB_LAYOUT_STEP_BUDGET","tier","AI_JOB_LAYOUT_STEP_MINIMUM_MS","minimumMs","AI_JOB_LAYOUT_DEFAULT_NAME","AI_JOB_LAYOUT_INSTRUCTIONS","text","join","aiJobLayoutPrompt","job","plan","name","NAVIGATION_NAME","FOOTER_NAME","isLink","node","componentId","props","aiLayoutReuseCheck","inventory","components","navigation","filter","component","test","tree","placed","violations","rootId","nodes","length","some","has","id","visits","slot","findIndex","header","slice","links","map","push","rule","code","message","nodeIds","sourceIds","aiCopiedLayoutReview","firestore","input","planned","regions","unreadable","stored","kind","hostId","reason","findings","aiLayoutJobAdmission","context","orgId","org","createAiJobLayoutStep","deps","readInventory","duplicate","now","signal","modelFor","orgSnapshot","Error","output","draft","load","note","resource","versionId","hostSubdomain","label","model","draftId","written","outputs","Promise","all","collection","doc","get","data","creation","duplicateOf","layouts","layout","copy","sourceId","uid","createdBy","ok","review","status","error","allowance","result","step","instructions","messages","role","content","tool","thinking","effort","extend","spent","refused","value","runAiJobLayoutStep","registerAiLayoutJob"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-layout-step.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { CANVAS_ROOT_ELEMENT_ID } from '@aglyn/aglyn/foundation/constants/canvas'\nimport type { AglynOrgBilling } from '@aglyn/aglyn/foundation/definitions/org-billing.types'\nimport { duplicateResource } from '@aglyn/tenant-data-admin/server/duplicate-resource'\nimport type { AiJob, AiJobOutput, AiJobPlan, AiJobReview } from '../model/ai-jobs.types'\nimport type { AiSiteInventory } from '../model/ai-site-inventory'\nimport { AI_STEP_TIERS } from '../providers/catalog'\nimport { AI_ROUTING_TABLE, aiModelForStep } from '../providers/routing'\nimport {\n aiDoctrineTreeTool,\n runValidatedGeneration,\n type AiValidatedTree,\n} from '../runtime/ai-doctrine'\nimport {\n walkTree,\n type AiDoctrineNode,\n type AiDoctrineTree,\n type AiDoctrineViolation,\n} from '../runtime/ai-doctrine-validators'\nimport type { AiLoadEstimate } from '../runtime/ai-palette'\nimport type { AiSystemBlock } from '../runtime/ai-runtime'\nimport { readSiteInventory } from '../runtime/site-inventory'\nimport { registerAiJobAdmission, type AiJobAdmission } from './ai-job-admission'\nimport { aiJobDraftId } from './ai-job-draft-ids'\nimport {\n aiDraftAdmissionRefusal,\n aiDraftAllowanceRefusal,\n aiSiteSubdomain,\n readAiDraft,\n readAiDraftNodes,\n writeAiDraft,\n type AiDraftRecord,\n} from './ai-job-drafts'\nimport {\n aiBracketedFactsNote,\n aiConfirmedPlan,\n aiDoctrineReview,\n aiGenerationSpent,\n aiJobBriefLine,\n aiLimitReview,\n aiModelNodeIds,\n aiPlacedComponentIds,\n aiPlanCreation,\n aiPlanReferenceLines,\n aiPlanReuseViolations,\n aiUnspentOutcome,\n} from './ai-job-generation'\nimport { aiPlanRegionViolations, aiPlannedLayoutRegions } from './ai-job-plan-conformance'\nimport type { AiJobStepRunner } from './ai-job-text-step'\nimport { aiJobStepBudget } from './ai-job-budget'\nimport { registerAiJobStep } from './ai-jobs'\n\n/**\n * The layout step (AGL-2909): the generation step of a `layout` job, run once\n * a member confirmed the job's plan.\n *\n * It builds ONE shared layout — a header with the site's navigation, the\n * Layout Slot each page renders in, a footer — through\n * `runValidatedGeneration('layout', …)`, so every building rule holds on the\n * tree before it is kept, and adds three checks of its own through `extend`: a\n * component the confirmed plan reuses is placed (rule 7), every region the\n * plan gives the layout is built (AGL-3024), and a navigation or menu\n * component the site already has is placed rather than drawn again (rule 1).\n *\n * The layout lands as a new draft (`ai-job-drafts.ts`): no screen uses it and\n * no layout nests it, so nothing on the live site changes until a member\n * assigns it. A plan that starts from a copy of a layout the site has gets\n * that copy, through the platform's duplicate module, and nothing generated —\n * so the copy is read back and held to the plan's regions the same way\n * (`aiCopiedLayoutReview`), because a branch that generates nothing is the one\n * branch where a promise can go unmet with no model to answer for it.\n *\n * A generation step runs on the job beat, never at an inline door: it\n * registers the least time its lookup rounds, its answer and its re-ask need\n * (AGL-3035), runs without extended thinking — the plan was reasoned out and\n * confirmed before it — and asks for no more of a tighter ceiling than the\n * doctrine's default for a layout than fits that time on the model it runs.\n */\n\n/** The longest answer a layout may run to; the tree is held to the layout budget either way. */\nexport const AI_JOB_LAYOUT_MAX_TOKENS = AI_ROUTING_TABLE['job.layout'].maxTokens\n\n/**\n * The layout step's time (AGL-3035, AGL-3036): its two inventory-lookup\n * rounds, its answer and its re-ask at the routing table's ceiling on the tier\n * `job.layout` is served from, fitted to what a beat can give a step.\n */\nexport const AI_JOB_LAYOUT_STEP_BUDGET = aiJobStepBudget({\n tier: AI_STEP_TIERS['job.layout'],\n maxTokens: AI_JOB_LAYOUT_MAX_TOKENS,\n})\n\n/** The least time one layout step needs before it starts. */\nexport const AI_JOB_LAYOUT_STEP_MINIMUM_MS = AI_JOB_LAYOUT_STEP_BUDGET.minimumMs\n\n/** A layout's name when the plan names none. */\nexport const AI_JOB_LAYOUT_DEFAULT_NAME = 'Site layout'\n\n/** The layout step's own instructions, cached after the doctrine. */\nexport const AI_JOB_LAYOUT_INSTRUCTIONS: readonly AiSystemBlock[] = [\n {\n text: [\n 'You build one shared layout for a website: the frame every page of the site renders inside. Answer with submit_layout: the whole layout as one flat node map.',\n 'A layout is a header, one Layout Slot and a footer, top to bottom. The header is an App Bar (muiAppBar) holding a Toolbar Content (muiToolbar) with the site’s name as text and its navigation. The footer is a Section (section) whose element is footer.',\n 'Navigation links the site’s own screens by id: a Screen Link (muiScreenLink) whose screenId is a screen id from the site inventory, in the order the brief and the confirmed plan give, seven at most in the header. Never invent a screen id, and never link a screen by its address.',\n 'When the site inventory lists a component whose name says it is navigation, a menu or a mega menu, place it in the header as an instance instead of building the navigation again. Place every component the confirmed plan reuses.',\n 'The Layout Slot (layoutSlot) is where each page renders and is the page’s main landmark: leave its component unset or main, and put nothing inside it. A layout carries no h1, because every page brings its own.',\n ].join('\\n'),\n },\n]\n\n/** The layout job as the generation's user turn: the name, the brief, the confirmed plan. */\nexport function aiJobLayoutPrompt(\n job: Pick<AiJob, 'brief'>,\n plan: AiJobPlan | null,\n name: string,\n): string {\n return [`Layout name: ${name}`, aiJobBriefLine(job), ...aiPlanReferenceLines(plan)].join('\\n')\n}\n\n/** A component name that says it is the site's navigation; a footer's links are not. */\nconst NAVIGATION_NAME = /\\b(?:nav|navigation|navbar|menu|mega ?menu)\\b/i\nconst FOOTER_NAME = /\\bfooter\\b/i\n\n/** A node that links somewhere: a Screen Link, or a Button pointed at a screen or an address. */\nfunction isLink(node: AiDoctrineNode): boolean {\n if (node.componentId === 'muiScreenLink') return true\n return (\n node.componentId === 'muiButton' &&\n (typeof node.props?.['screenId'] === 'string' || typeof node.props?.['href'] === 'string')\n )\n}\n\n/**\n * The layout door's checks on a tree the doctrine admitted: every component\n * the confirmed plan reuses is placed, every region the confirmed plan gives\n * the layout is built (AGL-3024), and navigation the site already keeps as a\n * component is placed rather than drawn again in the header.\n */\nexport function aiLayoutReuseCheck(\n inventory: AiSiteInventory | null,\n plan: AiJobPlan | null,\n): (tree: AiValidatedTree) => AiDoctrineViolation[] {\n const components = inventory?.components ?? []\n const navigation = components.filter(\n (component) => NAVIGATION_NAME.test(component.name) && !FOOTER_NAME.test(component.name),\n )\n return (tree) => {\n const placed = aiPlacedComponentIds(tree)\n const violations: AiDoctrineViolation[] = [\n ...aiPlanReuseViolations(inventory, plan, placed, 'layout'),\n ...aiPlanRegionViolations(plan, {\n rootId: tree.rootId,\n nodes: tree.nodes as unknown as Record<string, AiDoctrineNode>,\n }),\n ]\n if (navigation.length && !navigation.some((component) => placed.has(component.id))) {\n const visits = walkTree({\n rootId: tree.rootId,\n nodes: tree.nodes as unknown as Record<string, AiDoctrineNode>,\n })\n const slot = visits.findIndex(({ node }) => node.componentId === 'layoutSlot')\n const header = slot === -1 ? visits : visits.slice(0, slot)\n const links = header.filter(({ node }) => isLink(node)).map(({ id }) => id)\n if (links.length >= 2) {\n violations.push({\n rule: 1,\n code: 'navigation-rebuilt',\n message: `The site already has the \"${navigation[0].name}\" component. Place it in the header instead of building the navigation again.`,\n nodeIds: aiModelNodeIds(links, tree.sourceIds),\n })\n }\n }\n return violations\n }\n}\n\n/**\n * The regions the confirmed plan gives the layout, against the layout a COPY\n * actually produced (AGL-3024); `null` when the copy keeps the plan, or when\n * the plan promised no region to keep.\n *\n * The duplicate branch generates nothing, so it is the one place a plan's\n * promise can go unmet without a model ever answering for it. A copy that\n * cannot be read — deleted between the write and this read, or stored under a\n * root this check does not know — settles nothing, and a promise this step\n * cannot settle is reviewed by a person rather than reported as kept.\n */\nexport async function aiCopiedLayoutReview(\n firestore: FirebaseFirestore.Firestore,\n input: { hostId: string; id: string; name: string; plan: AiJobPlan | null },\n): Promise<AiJobReview | null> {\n const planned = aiPlannedLayoutRegions(input.plan)\n if (!planned.regions.length && !planned.unreadable.length) return null\n const stored = await readAiDraftNodes(firestore, {\n kind: 'layout',\n hostId: input.hostId,\n id: input.id,\n })\n const nodes = (stored?.nodes ?? {}) as unknown as Record<string, AiDoctrineNode>\n if (!nodes[CANVAS_ROOT_ELEMENT_ID]) {\n return {\n reason: 'doctrine',\n message: `\"${input.name}\" was copied from the layout the plan names, and this job could not read the copy back to check it has the regions the plan gives it. Open the layout and check it before you put a page in it.`,\n findings: [],\n }\n }\n const violations = aiPlanRegionViolations(input.plan, { rootId: CANVAS_ROOT_ELEMENT_ID, nodes })\n if (!violations.length) return null\n return aiDoctrineReview({\n message: `\"${input.name}\" is a copy of the layout the plan names, and a copy is all it is: ${violations[0].message}`,\n violations,\n })\n}\n\n/** A layout job is admitted for a site of the job's own org with a shared layout to spare. */\nexport const aiLayoutJobAdmission: AiJobAdmission = (context) =>\n aiDraftAdmissionRefusal(context.firestore, {\n orgId: context.orgId,\n hostId: context.hostId,\n kind: 'layout',\n org: context.org,\n })\n\nexport interface AiJobLayoutStepDeps {\n /** The inventory reader; specs hand in a fake. */\n readInventory?: typeof readSiteInventory\n /** The platform's duplicate module, for a plan that starts from a copy. */\n duplicate?: typeof duplicateResource\n}\n\nexport function createAiJobLayoutStep(deps: AiJobLayoutStepDeps = {}): AiJobStepRunner {\n const readInventory = deps.readInventory ?? readSiteInventory\n const duplicate = deps.duplicate ?? duplicateResource\n return async ({ job, now, signal, firestore, modelFor }) => {\n const hostId = job.hostId\n if (!hostId) throw new Error('a layout job names no site, and its admission refuses one')\n const output = (draft: AiDraftRecord, load?: AiLoadEstimate | null, note?: string | null): AiJobOutput => ({\n resource: 'layout',\n id: draft.id,\n versionId: draft.versionId,\n hostId,\n hostSubdomain: draft.hostSubdomain,\n label: draft.name,\n ...(load ? { load } : {}),\n ...(note ? { note } : {}),\n })\n // The switch's answer for this job, else the routing table's (AGL-2942).\n const model = modelFor?.('job.layout') ?? aiModelForStep('job.layout')\n\n // A run cut off after its draft was written reports that draft, found by the id the job recorded.\n const draftId = aiJobDraftId(job, 'layout')\n const written = await readAiDraft(firestore, { kind: 'layout', hostId, id: draftId })\n if (written) return aiUnspentOutcome(model, { outputs: [output(written)] })\n\n const [inventory, orgSnapshot] = await Promise.all([\n readInventory(job.orgId, hostId, { firestore }),\n firestore.collection('orgs').doc(job.orgId).get(),\n ])\n const org = (orgSnapshot.data() ?? null) as Partial<AglynOrgBilling> | null\n const plan = aiConfirmedPlan(job)\n const creation = aiPlanCreation(plan, 'layout')\n const name = creation?.name || AI_JOB_LAYOUT_DEFAULT_NAME\n\n // The plan starts from a copy of a layout the site has (rule 15): the copy\n // is the draft, and nothing is generated. A source gone since the plan\n // was made is built from the brief instead.\n if (creation?.duplicateOf && inventory.layouts.some((layout) => layout.id === creation.duplicateOf)) {\n const copy = await duplicate('layout', {\n orgId: job.orgId,\n hostId,\n sourceId: creation.duplicateOf,\n name,\n uid: job.createdBy,\n org: org as Record<string, unknown> | null,\n // The same claim the page step makes (AGL-3024): a copy core mints is\n // written under core's own id, never `draftId`, so a step that runs\n // again — a pass resumed, a run cut off after the copy — would mint a\n // second. The job's recorded id makes core replay the one it made.\n attemptKey: draftId,\n })\n if (copy.ok) {\n // A copy is not a build (AGL-3024). This branch generates nothing, so\n // whatever the plan promised BEYOND the source is not in the draft\n // unless the source already had it: a plan reading \"from a copy of the\n // base layout, adding a sidebar\" gets the base layout, the sidebar\n // nowhere in it, and until now reported Done. The copy's own regions\n // answer the plan's, and a copy that cannot be read answers nothing —\n // which is a stop for a person, never a Done.\n const review = await aiCopiedLayoutReview(firestore, { hostId, id: copy.id, plan, name })\n if (review) return aiUnspentOutcome(model, { review })\n const hostSubdomain = await aiSiteSubdomain(firestore, hostId)\n return aiUnspentOutcome(model, {\n outputs: [output({ id: copy.id, versionId: copy.versionId, name: copy.name, hostSubdomain })],\n })\n }\n if (copy.ok === false && copy.status === 403) {\n return aiUnspentOutcome(model, { review: aiLimitReview(copy.error) })\n }\n }\n\n const allowance = await aiDraftAllowanceRefusal(firestore, { kind: 'layout', hostId, org })\n if (allowance) return aiUnspentOutcome(model, { review: aiLimitReview(allowance) })\n\n const result = await runValidatedGeneration('layout', {\n step: 'job.layout',\n model,\n instructions: AI_JOB_LAYOUT_INSTRUCTIONS,\n inventory,\n messages: [{ role: 'user', content: aiJobLayoutPrompt(job, plan, name) }],\n tool: aiDoctrineTreeTool('layout'),\n maxTokens: AI_JOB_LAYOUT_STEP_BUDGET.maxTokens(model),\n ...(AI_ROUTING_TABLE['job.layout'].thinking ? { thinking: AI_ROUTING_TABLE['job.layout'].thinking } : {}),\n ...(AI_ROUTING_TABLE['job.layout'].effort ? { effort: AI_ROUTING_TABLE['job.layout'].effort } : {}),\n extend: aiLayoutReuseCheck(inventory, plan),\n ...(signal ? { signal } : {}),\n })\n const spent = aiGenerationSpent(result)\n if (result.status === 'refused') return { ...spent, refused: true }\n if (result.status === 'needs_input') return { ...spent, review: aiDoctrineReview(result) }\n\n const draft = await writeAiDraft(firestore, {\n kind: 'layout',\n hostId,\n id: draftId,\n uid: job.createdBy,\n org,\n name,\n nodes: result.value.nodes,\n now,\n })\n if (draft.ok === false) {\n if (draft.status === 404) throw new Error(`site ${hostId} vanished while its layout was generated`)\n return { ...spent, review: aiLimitReview(draft.error) }\n }\n // The facts the brief did not give, which the member fills before publishing (AGL-3056).\n const note = aiBracketedFactsNote({\n tree: { rootId: result.value.rootId, nodes: result.value.nodes as unknown as AiDoctrineTree['nodes'] },\n inventory,\n })\n return { ...spent, outputs: [output(draft, result.value.load, note)] }\n }\n}\n\nexport const runAiJobLayoutStep = createAiJobLayoutStep()\n\n/** Registers the layout step and the check a layout job passes before it is created or resumed. */\nexport function registerAiLayoutJob(): void {\n registerAiJobStep('layout', runAiJobLayoutStep, { minimumMs: AI_JOB_LAYOUT_STEP_MINIMUM_MS })\n registerAiJobAdmission('layout', aiLayoutJobAdmission)\n}\n"],"names":["CANVAS_ROOT_ELEMENT_ID","duplicateResource","AI_STEP_TIERS","AI_ROUTING_TABLE","aiModelForStep","aiDoctrineTreeTool","runValidatedGeneration","walkTree","readSiteInventory","registerAiJobAdmission","aiJobDraftId","aiDraftAdmissionRefusal","aiDraftAllowanceRefusal","aiSiteSubdomain","readAiDraft","readAiDraftNodes","writeAiDraft","aiBracketedFactsNote","aiConfirmedPlan","aiDoctrineReview","aiGenerationSpent","aiJobBriefLine","aiLimitReview","aiModelNodeIds","aiPlacedComponentIds","aiPlanCreation","aiPlanReferenceLines","aiPlanReuseViolations","aiUnspentOutcome","aiPlanRegionViolations","aiPlannedLayoutRegions","aiJobStepBudget","registerAiJobStep","AI_JOB_LAYOUT_MAX_TOKENS","maxTokens","AI_JOB_LAYOUT_STEP_BUDGET","tier","AI_JOB_LAYOUT_STEP_MINIMUM_MS","minimumMs","AI_JOB_LAYOUT_DEFAULT_NAME","AI_JOB_LAYOUT_INSTRUCTIONS","text","join","aiJobLayoutPrompt","job","plan","name","NAVIGATION_NAME","FOOTER_NAME","isLink","node","componentId","props","aiLayoutReuseCheck","inventory","components","navigation","filter","component","test","tree","placed","violations","rootId","nodes","length","some","has","id","visits","slot","findIndex","header","slice","links","map","push","rule","code","message","nodeIds","sourceIds","aiCopiedLayoutReview","firestore","input","planned","regions","unreadable","stored","kind","hostId","reason","findings","aiLayoutJobAdmission","context","orgId","org","createAiJobLayoutStep","deps","readInventory","duplicate","now","signal","modelFor","orgSnapshot","Error","output","draft","load","note","resource","versionId","hostSubdomain","label","model","draftId","written","outputs","Promise","all","collection","doc","get","data","creation","duplicateOf","layouts","layout","copy","sourceId","uid","createdBy","attemptKey","ok","review","status","error","allowance","result","step","instructions","messages","role","content","tool","thinking","effort","extend","spent","refused","value","runAiJobLayoutStep","registerAiLayoutJob"],"mappings":"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"}
|
|
@@ -293,7 +293,12 @@ export function createAiJobTemplateStep(deps = {}) {
|
|
|
293
293
|
sourceId: creation.duplicateOf,
|
|
294
294
|
name,
|
|
295
295
|
uid: job.createdBy,
|
|
296
|
-
org: org
|
|
296
|
+
org: org,
|
|
297
|
+
// The same claim the page step makes (AGL-3024): a copy core mints is
|
|
298
|
+
// written under core's own id, never `draftId`, so a step that runs
|
|
299
|
+
// again — a pass resumed, a run cut off after the copy — would mint a
|
|
300
|
+
// second. The job's recorded id makes core replay the one it made.
|
|
301
|
+
attemptKey: draftId
|
|
297
302
|
});
|
|
298
303
|
if (copy.ok) {
|
|
299
304
|
const hostSubdomain = await aiSiteSubdomain(firestore, hostId);
|