@aglyn/plugins-ai 1.0.0-beta.250 → 1.0.0-beta.252
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +17 -17
- package/src/lib/components/ai-brief-dialog.component.js +15 -1
- package/src/lib/components/ai-brief-dialog.component.js.map +1 -1
- package/src/lib/components/ai-job-links.js +1 -1
- package/src/lib/components/ai-job-links.js.map +1 -1
- package/src/lib/components/ai-job-plan.component.js +20 -2
- package/src/lib/components/ai-job-plan.component.js.map +1 -1
- package/src/lib/components/ai-jobs-store.d.ts +11 -0
- package/src/lib/components/ai-jobs-store.js +22 -0
- package/src/lib/components/ai-jobs-store.js.map +1 -1
- package/src/lib/jobs/ai-job-crm-step.js +7 -4
- package/src/lib/jobs/ai-job-crm-step.js.map +1 -1
- package/src/lib/jobs/ai-job-drafts.d.ts +24 -0
- package/src/lib/jobs/ai-job-drafts.js +46 -0
- package/src/lib/jobs/ai-job-drafts.js.map +1 -1
- package/src/lib/jobs/ai-job-experiment-step.js +6 -1
- package/src/lib/jobs/ai-job-experiment-step.js.map +1 -1
- package/src/lib/jobs/ai-job-form-reuse.d.ts +78 -0
- package/src/lib/jobs/ai-job-form-reuse.js +136 -0
- package/src/lib/jobs/ai-job-form-reuse.js.map +1 -0
- package/src/lib/jobs/ai-job-form-step.d.ts +17 -0
- package/src/lib/jobs/ai-job-form-step.js +63 -11
- package/src/lib/jobs/ai-job-form-step.js.map +1 -1
- package/src/lib/jobs/ai-job-insight-step.js +6 -1
- package/src/lib/jobs/ai-job-insight-step.js.map +1 -1
- package/src/lib/jobs/ai-job-plan-step.d.ts +19 -0
- package/src/lib/jobs/ai-job-plan-step.js +39 -1
- package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
- package/src/lib/jobs/ai-job-seo-step.js +5 -3
- package/src/lib/jobs/ai-job-seo-step.js.map +1 -1
- package/src/lib/jobs/ai-job-site-content.d.ts +7 -5
- package/src/lib/jobs/ai-job-site-content.js +7 -5
- package/src/lib/jobs/ai-job-site-content.js.map +1 -1
- package/src/lib/jobs/ai-job-text-step.js +6 -3
- package/src/lib/jobs/ai-job-text-step.js.map +1 -1
- package/src/lib/jobs/ai-layout-stock-photos.d.ts +11 -7
- package/src/lib/jobs/ai-layout-stock-photos.js +25 -10
- package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
- package/src/lib/jobs/ai-stock-business-nouns.d.ts +62 -0
- package/src/lib/jobs/ai-stock-business-nouns.js +264 -0
- package/src/lib/jobs/ai-stock-business-nouns.js.map +1 -0
- package/src/lib/model/ai-build-plan.d.ts +7 -3
- package/src/lib/model/ai-build-plan.js +11 -1
- package/src/lib/model/ai-build-plan.js.map +1 -1
- package/src/lib/model/ai-credit-estimate.d.ts +32 -18
- package/src/lib/model/ai-credit-estimate.js +36 -18
- package/src/lib/model/ai-credit-estimate.js.map +1 -1
- package/src/lib/model/ai-job-activity.d.ts +30 -0
- package/src/lib/model/ai-job-activity.js +35 -0
- package/src/lib/model/ai-job-activity.js.map +1 -1
- package/src/lib/model/ai-media-credits.d.ts +5 -3
- package/src/lib/model/ai-media-credits.js +7 -5
- package/src/lib/model/ai-media-credits.js.map +1 -1
- package/src/lib/model/ai-site-job.js +6 -3
- package/src/lib/model/ai-site-job.js.map +1 -1
- package/src/lib/model/ai-site-store-pages.d.ts +5 -0
- package/src/lib/model/ai-site-store-pages.js +2 -0
- package/src/lib/model/ai-site-store-pages.js.map +1 -1
- package/src/lib/plugin-config.js +2 -2
- package/src/lib/plugin-config.js.map +1 -1
- package/src/lib/providers/anthropic-crm-wording-superseded.d.ts +29 -0
- package/src/lib/providers/anthropic-crm-wording-superseded.js +31 -0
- package/src/lib/providers/anthropic-crm-wording-superseded.js.map +1 -0
- package/src/lib/providers/catalog.d.ts +24 -17
- package/src/lib/providers/catalog.js +63 -30
- package/src/lib/providers/catalog.js.map +1 -1
- package/src/lib/providers/model-choice.d.ts +1 -1
- package/src/lib/providers/model-choice.js +39 -30
- package/src/lib/providers/model-choice.js.map +1 -1
- package/src/lib/providers/routing.d.ts +20 -1
- package/src/lib/providers/routing.js +47 -8
- package/src/lib/providers/routing.js.map +1 -1
- package/src/lib/runtime/ai-dev-env.d.ts +37 -1
- package/src/lib/runtime/ai-dev-env.js +59 -6
- package/src/lib/runtime/ai-dev-env.js.map +1 -1
- package/src/lib/runtime/ai-doctrine-validators.js +3 -1
- package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
- package/src/lib/runtime/ai-doctrine.js +8 -4
- package/src/lib/runtime/ai-doctrine.js.map +1 -1
- package/src/lib/runtime/ai-eval-live.d.ts +8 -0
- package/src/lib/runtime/ai-eval-live.js +34 -8
- package/src/lib/runtime/ai-eval-live.js.map +1 -1
- package/src/lib/runtime/ai-palette.generated.js +4 -0
- package/src/lib/runtime/ai-palette.generated.js.map +1 -1
- package/src/lib/runtime/ai-runtime.js +17 -2
- package/src/lib/runtime/ai-runtime.js.map +1 -1
- package/src/lib/runtime/seo-fields.js +4 -3
- package/src/lib/runtime/seo-fields.js.map +1 -1
- package/src/lib/server/ai-assist.js +3 -2
- package/src/lib/server/ai-assist.js.map +1 -1
- package/src/lib/server/ai-media-svg.js +4 -0
- package/src/lib/server/ai-media-svg.js.map +1 -1
- package/src/lib/usage/ai-allotment-alerts.js +2 -5
- package/src/lib/usage/ai-allotment-alerts.js.map +1 -1
|
@@ -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 { throwIfAiJobCanceled } from './ai-job-cancel'\nimport {\n checkFormContract,\n formFieldsCanYieldAnEmail,\n} from '@aglyn/aglyn/app-utils/form-contract'\nimport {\n collectFormFieldNodeIds,\n FORM_COMPONENT_ID,\n FORM_ID_PROP,\n FORM_INPUT_ELEMENT_IDS,\n formFieldDeclsFromNodes,\n formInputName,\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, AiJobReview } 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 {\n AI_FORM_DATASET_MADE_INPUT,\n aiBindFormToDataset,\n aiFormDatasetNote,\n aiFormDatasetPromptLine,\n aiReadFormDataset,\n type AiFormDataset,\n type AiFormDatasetBinding,\n type AiFormDatasetReadDeps,\n} from './ai-job-form-dataset'\nimport { aiJobPlanCreationsRefusal } from './ai-job-plan-creations'\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 (`aiFormConsentNodes`: a Choice Label holding one Checkbox,\n * worded as the Forms editor preset's option) in place of any consent-like\n * field the model drew, named as its `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/**\n * The node id the platform's consent field takes in a generated design: the\n * Choice Label that holds its Checkbox.\n */\nexport const AI_FORM_CONSENT_NODE_ID = 'marketingConsentField'\n\n/** The node id of the consent field's Checkbox, inside {@link AI_FORM_CONSENT_NODE_ID}. */\nexport const AI_FORM_CONSENT_CHECKBOX_NODE_ID = 'marketingConsentCheckbox'\n\n/** The name the consent Checkbox submits under, and the form's `consentFieldName`. */\nexport const AI_FORM_CONSENT_FIELD_NAME = MARKETING_CONSENT_FORM_FIELD.fieldName\n\n/** What a ticked consent Checkbox posts: an affirmative value `isConsentCheckboxTicked` reads as the tick. */\nexport const AI_FORM_CONSENT_VALUE = 'Yes'\n\n/**\n * The platform's marketing consent field as MUI's separate elements: a Choice\n * Label whose words say what the person agrees to — the Forms editor preset's\n * own option text — holding one unticked, unrequired Checkbox named\n * `marketingConsent`. Ids are fixed, so a re-asked design gets the same nodes.\n */\nexport function aiFormConsentNodes(formNodeId: string): NodesMap[string][] {\n const { choiceLabel, checkbox } = FORM_INPUT_ELEMENT_IDS\n return [\n {\n $id: AI_FORM_CONSENT_NODE_ID,\n type: NodeType.NODE,\n componentId: choiceLabel,\n pluginId: AI_PALETTE[choiceLabel]?.pluginId,\n parentId: formNodeId,\n nodes: [AI_FORM_CONSENT_CHECKBOX_NODE_ID],\n props: { label: MARKETING_CONSENT_FORM_FIELD.options },\n } as NodesMap[string],\n {\n $id: AI_FORM_CONSENT_CHECKBOX_NODE_ID,\n type: NodeType.NODE,\n componentId: checkbox,\n pluginId: AI_PALETTE[checkbox]?.pluginId,\n parentId: AI_FORM_CONSENT_NODE_ID,\n nodes: [],\n props: { name: AI_FORM_CONSENT_FIELD_NAME, value: AI_FORM_CONSENT_VALUE },\n } as NodesMap[string],\n ]\n}\n\n/**\n * The node to remove for a consent input the model drew: the input itself,\n * or the Choice Label and Form Control around it when they frame nothing but\n * consent — so no empty label or orphaned Form Label is left behind. The\n * all-in-one Form Field of an older design is its own frame.\n */\nfunction consentWrapperId(nodes: NodesMap, inputId: string, formNodeId: string): string {\n const framing = new Set<string>([FORM_INPUT_ELEMENT_IDS.choiceLabel, FORM_INPUT_ELEMENT_IDS.formControl])\n let outer = inputId\n let parentId = String(nodes[inputId]?.parentId ?? '')\n while (parentId && parentId !== formNodeId) {\n const parent = nodes[parentId]\n if (!parent || !framing.has(String(parent.componentId))) break\n const answers = collectFormFieldNodeIds(nodes as never, parentId)\n if (!answers.every((id) => isMarketingConsentFieldName(formInputName(nodes[id] as never)))) break\n outer = parentId\n parentId = String(parent.parentId ?? '')\n }\n return outer\n}\n\n/** Removes a node and everything under it, and its id from its parent's children. */\nfunction removeSubtree(nodes: NodesMap, id: string): void {\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 const stack = [id]\n while (stack.length) {\n const next = stack.pop() as string\n const node = nodes[next]\n if (!node) continue\n if (Array.isArray(node.nodes)) stack.push(...(node.nodes as string[]))\n delete nodes[next]\n }\n}\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 fields in the order a visitor fills them in. Each piece of a field is its own element: a field is a Form Control (muiFormControl) holding a Form Label (muiFormLabel) whose text a visitor reads, then the answer, then optionally Helper Text (muiFormHelperText). Give the form a submitLabel and a successMessage in the site’s voice. The Form draws its own button, so a Button (muiButton) with type submit is optional.',\n 'The answer is an Input (muiInput) to type in: type email for an email address, tel for a phone number, multiline true for a description; a Select (muiSelect) holding one Select Option (muiSelectOption) per choice, or a Radio Group (muiRadioGroup) holding one Choice Label (muiFormControlLabel) per choice, each holding a Radio (muiRadio) whose value is the choice, for one choice; a Choice Label holding a Checkbox (muiCheckbox) per choice, the Checkboxes sharing one name, for several; and a Rating (muiRating) for a score. A single yes-or-no tick is a Choice Label holding one Checkbox, outside any Form Control.',\n 'Every Input, Select, Radio Group, Checkbox and Rating has a name that is unique in the form, apart from Checkboxes of one question, and says what it holds in camelCase, such as fullName, email, phone or roofType; and required only when the form cannot do its job without it.',\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 /** The dataset its submissions are also written to, and what was bound (AGL-3616). */\n dataset?: { dataset: AiFormDataset; binding: AiFormDatasetBinding }\n}\n\n/**\n * The draft a validated design and its answer make: the form node bound to\n * `formId` and captioned `name`, the platform's consent field when the\n * fields can yield an email address, the canvas root above, and the\n * declaration read off the result. A dataset binding is made only where the\n * plan said the form writes to one this step may bind (`identity.dataset`,\n * AGL-3616); any the model drew is dropped. 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; dataset?: AiFormDataset | null },\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: the\n // plan's to say (AGL-3616), bound below, never the model's to draw.\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 if (!nodes[id] || !isMarketingConsentFieldName(formInputName(nodes[id] as never))) continue\n removeSubtree(nodes, consentWrapperId(nodes, id, formNodeId))\n }\n if (formFieldsCanYieldAnEmail(formFieldDeclsFromNodes(nodes as never, formNodeId))) {\n for (const node of aiFormConsentNodes(formNodeId)) nodes[node.$id as string] = node\n form.nodes = [...(form.nodes as string[]), AI_FORM_CONSENT_NODE_ID]\n consentFieldName = AI_FORM_CONSENT_FIELD_NAME\n }\n form.parentId = CANVAS_ROOT_ELEMENT_ID\n }\n // The dataset the plan says the form writes to (AGL-3616), bound as the\n // Form element binds one.\n const dataset =\n identity.dataset && form?.componentId === FORM_COMPONENT_ID\n ? { dataset: identity.dataset, binding: aiBindFormToDataset(nodes, formNodeId, identity.dataset) }\n : undefined\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 ...(dataset ? { dataset } : {}),\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 dataset?: AiFormDataset | null\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\n/**\n * The fields the confirmed plan gives the form, against the form a COPY\n * actually produced (AGL-3024); `null` when the copy keeps the plan, or when\n * the plan promised no field to keep.\n *\n * The fourth kind to need this, after the layout, the page and the component.\n * `aiFormCopyUnboundFailure` above already refuses the one violation\n * duplication CAUSES; this refuses the one duplication HIDES — a plan reading\n * \"start from the consultation form and add a case-type question\" getting the\n * consultation form and nothing else, reported as built.\n *\n * A field the plan names is matched on its name alone: a copy that collects\n * the answer under a different label is still collecting it, and this check\n * has no way to tell a renamed field from a missing one without guessing.\n */\nexport async function aiCopiedFormReview(\n firestore: FirebaseFirestore.Firestore,\n input: { hostId: string; formId: string; name: string; plan: AiJobPlan | null },\n): Promise<AiJobReview | null> {\n const promised = (aiPlanCreation(input.plan, 'form')?.fields ?? [])\n .map((field) => field.split(':')[0]?.trim().toLowerCase())\n .filter((field): field is string => Boolean(field))\n if (!promised.length) return null\n const snapshot = await firestore\n .collection('hosts')\n .doc(input.hostId)\n .collection('forms')\n .doc(input.formId)\n .get()\n if (!snapshot.exists) {\n return {\n reason: 'doctrine',\n message: `\"${input.name}\" was copied from the form the plan names, and this job could not read the copy back to check it collects what the plan gives it. Open the form and check it before you put it on a page.`,\n findings: [],\n }\n }\n const fields = (snapshot.get('fields') ?? []) as unknown[]\n const collected = new Set(\n fields\n .map((field) =>\n field && typeof field === 'object'\n ? String((field as Record<string, unknown>)['fieldName'] ?? (field as Record<string, unknown>)['name'] ?? '')\n : '',\n )\n .filter(Boolean)\n .map((name) => name.toLowerCase()),\n )\n const missing = promised.filter((name) => !collected.has(name))\n if (!missing.length) return null\n const one = missing.length === 1\n return aiDoctrineReview({\n message: `\"${input.name}\" is a copy of the form the plan names, and a copy is all it is: the plan gives it ${\n one ? 'a field' : 'fields'\n } the copy does not collect.`,\n violations: [\n {\n rule: null,\n code: 'plan-fields-missing',\n message: `The confirmed plan says this form collects ${missing.join(', ')}, and the copy collects ${\n collected.size ? [...collected].join(', ') : 'nothing'\n }. Add ${one ? 'it' : 'each of them'}, or the copy is the form it was copied from.`,\n },\n ],\n })\n}\n\n/**\n * What the form a short copy would have started from collects, as the build\n * that replaces the copy is told it (AGL-3143 §14): every field it asks for,\n * so the build keeps them beside what the plan adds. Empty when the form\n * cannot be read.\n */\nexport async function aiFormSourceLines(\n firestore: FirebaseFirestore.Firestore,\n input: { hostId: string; id: string; name: string },\n): Promise<string[]> {\n const snapshot = await firestore.collection('hosts').doc(input.hostId).collection('forms').doc(input.id).get()\n const fields = snapshot.exists ? ((snapshot.get('fields') ?? []) as unknown[]) : []\n const asked = fields.flatMap((field) => {\n if (!field || typeof field !== 'object') return []\n const row = field as Record<string, unknown>\n const name = String(row['fieldName'] ?? row['name'] ?? '').trim()\n if (!name) return []\n const fieldType = row['fieldType'] ?? row['type']\n const type = typeof fieldType === 'string' ? ` (${fieldType}${row['required'] ? ', required' : ''})` : ''\n return [`${name}${type}`]\n })\n if (!asked.length) return []\n return [\n `The plan starts from a copy of the form \"${input.name}\", which lacks part of what the plan gives it. Build from it: it collects ${asked.join(', ')}. Keep each of those, and add what the plan adds.`,\n ]\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.options}\" 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/**\n * A form job is admitted for a site of the job's own org whose plan has\n * forms, with a form to spare, and confirmed only for a plan that creates\n * nothing but the form: a form places nothing another job builds\n * (AGL-3143 §15).\n */\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 ownCheck: (hostId) => aiJobPlanCreationsRefusal('form', context, hostId),\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 /** The data plugin's writer lookup the form's dataset is read through (AGL-3616); specs hand in a fake. */\n datasetWriterFor?: AiFormDatasetReadDeps['writerFor']\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, and one that does not collect\n // a field the plan gives the form is built from the plan, told what the\n // source collects (AGL-3143 §14): copying it could only end in the review\n // below. A source this step cannot read still copies.\n const source = creation?.duplicateOf\n ? inventory.forms.find((form) => form.id === creation.duplicateOf)\n : undefined\n const sourceReview = source\n ? await aiCopiedFormReview(firestore, { hostId, formId: source.id, name: source.name, plan })\n : null\n const sourceShort = Boolean(sourceReview?.findings.length)\n const sourceLines =\n source && sourceShort ? await aiFormSourceLines(firestore, { hostId, id: source.id, name: source.name }) : []\n if (creation?.duplicateOf && source && !sourceShort) {\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 // And the one duplication hides: what the plan promised of the copy,\n // read back (AGL-3024). This branch generates nothing, so nothing\n // else can answer for it.\n const review = await aiCopiedFormReview(firestore, {\n hostId,\n formId: copy.id,\n name: copy.name,\n plan,\n })\n if (review) return aiUnspentOutcome(model, { review })\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 // The dataset the plan says the form writes to (AGL-3616), where this\n // workspace may bind one; otherwise the form is the form it always was.\n const dataset = await aiReadFormDataset(\n {\n hostId,\n id: creation?.writesTo,\n org,\n inventory,\n madeId: job.inputs?.[AI_FORM_DATASET_MADE_INPUT],\n },\n deps.datasetWriterFor ? { writerFor: deps.datasetWriterFor } : {},\n )\n const check = aiFormDraftCheck({ formId: draftId, name, decisions, dataset })\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: [\n aiJobFormPrompt(job, plan, name, {\n audience,\n submissions: decisions.submissions,\n }),\n ...sourceLines,\n ...(dataset ? [aiFormDatasetPromptLine(dataset)] : []),\n ].join('\\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 // Never written once the job is canceled (AGL-3616).\n throwIfAiJobCanceled(signal)\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 draft.dataset ? aiFormDatasetNote(draft.dataset.dataset, draft.dataset.binding) : null,\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":["throwIfAiJobCanceled","checkFormContract","formFieldsCanYieldAnEmail","collectFormFieldNodeIds","FORM_COMPONENT_ID","FORM_ID_PROP","FORM_INPUT_ELEMENT_IDS","formFieldDeclsFromNodes","formInputName","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","AI_FORM_DATASET_MADE_INPUT","aiBindFormToDataset","aiFormDatasetNote","aiFormDatasetPromptLine","aiReadFormDataset","aiJobPlanCreationsRefusal","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_FORM_CONSENT_CHECKBOX_NODE_ID","AI_FORM_CONSENT_FIELD_NAME","fieldName","AI_FORM_CONSENT_VALUE","aiFormConsentNodes","formNodeId","choiceLabel","checkbox","$id","type","NODE","componentId","pluginId","parentId","nodes","props","label","options","name","value","consentWrapperId","inputId","framing","Set","formControl","outer","String","parent","has","answers","every","id","removeSubtree","Array","isArray","filter","child","stack","length","next","pop","node","push","AI_JOB_FORM_INSTRUCTIONS","text","join","TREE_TOOL","AI_JOB_FORM_TOOL","description","strict","inputSchema","additionalProperties","required","properties","tree","routing","kind","enum","list","anyOf","cannotCollect","items","isRecord","oneLine","max","replace","trim","slice","parseAiFormAnswer","answer","decisions","submissions","proposed","includes","seen","entry","toLowerCase","add","aiFormDraft","identity","parsed","rootId","Object","entries","form","consentFieldName","formId","dataset","binding","undefined","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","aiCopiedFormReview","input","promised","plan","field","split","Boolean","exists","reason","findings","collected","missing","one","size","aiFormSourceLines","asked","flatMap","row","fieldType","listOf","aiFormOutputNote","sentences","aiJobFormPrompt","job","context","audience","SUBMISSION_WORDS","inbox","aiFormJobAdmission","orgId","org","ownCheck","createAiJobFormStep","deps","readInventory","duplicate","now","signal","modelFor","orgSnapshot","Error","output","extra","resource","versionId","hostSubdomain","load","note","model","draftId","written","outputs","inventory","Promise","all","creation","source","duplicateOf","forms","sourceReview","sourceShort","sourceLines","sourceId","uid","createdBy","attemptKey","ok","failure","review","status","error","allowance","inputs","writesTo","madeId","datasetWriterFor","writerFor","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 { throwIfAiJobCanceled } from './ai-job-cancel'\nimport {\n checkFormContract,\n formFieldsCanYieldAnEmail,\n} from '@aglyn/aglyn/app-utils/form-contract'\nimport {\n collectFormFieldNodeIds,\n FORM_COMPONENT_ID,\n FORM_ID_PROP,\n FORM_INPUT_ELEMENT_IDS,\n formFieldDeclsFromNodes,\n formInputName,\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, AiJobReview } 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 {\n AI_FORM_DATASET_MADE_INPUT,\n aiBindFormToDataset,\n aiFormDatasetNote,\n aiFormDatasetPromptLine,\n aiReadFormDataset,\n type AiFormDataset,\n type AiFormDatasetBinding,\n type AiFormDatasetReadDeps,\n} from './ai-job-form-dataset'\nimport { aiJobPlanCreationsRefusal } from './ai-job-plan-creations'\nimport { aiJobDraftId } from './ai-job-draft-ids'\nimport {\n aiDraftAdmissionRefusal,\n aiDraftAllowanceRefusal,\n aiSiteSubdomain,\n readAiDraft,\n readAiDraftBandTakenSince,\n writeAiDraft,\n type AiDraftRecord,\n} from './ai-job-drafts'\nimport { AI_FORM_REUSED_NOTE, aiSiblingJobForm } from './ai-job-form-reuse'\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 { AiJobStepOutcome, 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 (`aiFormConsentNodes`: a Choice Label holding one Checkbox,\n * worded as the Forms editor preset's option) in place of any consent-like\n * field the model drew, named as its `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 * A site that holds every form its plan includes refuses the draft\n * (AGL-3765). A form the step builds for a page (a unit of a page job, a\n * guided start or a build) then places the form another AI job made on the\n * site moments before (`ai-job-form-reuse.ts`), as two jobs started from one\n * brief race to. Failing that, a site that still had room when the plan was\n * confirmed stops the job on our side, its credits given back; one that was\n * already full is the workspace's limit, for the member to change.\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/**\n * The node id the platform's consent field takes in a generated design: the\n * Choice Label that holds its Checkbox.\n */\nexport const AI_FORM_CONSENT_NODE_ID = 'marketingConsentField'\n\n/** The node id of the consent field's Checkbox, inside {@link AI_FORM_CONSENT_NODE_ID}. */\nexport const AI_FORM_CONSENT_CHECKBOX_NODE_ID = 'marketingConsentCheckbox'\n\n/** The name the consent Checkbox submits under, and the form's `consentFieldName`. */\nexport const AI_FORM_CONSENT_FIELD_NAME = MARKETING_CONSENT_FORM_FIELD.fieldName\n\n/** What a ticked consent Checkbox posts: an affirmative value `isConsentCheckboxTicked` reads as the tick. */\nexport const AI_FORM_CONSENT_VALUE = 'Yes'\n\n/**\n * The platform's marketing consent field as MUI's separate elements: a Choice\n * Label whose words say what the person agrees to — the Forms editor preset's\n * own option text — holding one unticked, unrequired Checkbox named\n * `marketingConsent`. Ids are fixed, so a re-asked design gets the same nodes.\n */\nexport function aiFormConsentNodes(formNodeId: string): NodesMap[string][] {\n const { choiceLabel, checkbox } = FORM_INPUT_ELEMENT_IDS\n return [\n {\n $id: AI_FORM_CONSENT_NODE_ID,\n type: NodeType.NODE,\n componentId: choiceLabel,\n pluginId: AI_PALETTE[choiceLabel]?.pluginId,\n parentId: formNodeId,\n nodes: [AI_FORM_CONSENT_CHECKBOX_NODE_ID],\n props: { label: MARKETING_CONSENT_FORM_FIELD.options },\n } as NodesMap[string],\n {\n $id: AI_FORM_CONSENT_CHECKBOX_NODE_ID,\n type: NodeType.NODE,\n componentId: checkbox,\n pluginId: AI_PALETTE[checkbox]?.pluginId,\n parentId: AI_FORM_CONSENT_NODE_ID,\n nodes: [],\n props: { name: AI_FORM_CONSENT_FIELD_NAME, value: AI_FORM_CONSENT_VALUE },\n } as NodesMap[string],\n ]\n}\n\n/**\n * The node to remove for a consent input the model drew: the input itself,\n * or the Choice Label and Form Control around it when they frame nothing but\n * consent — so no empty label or orphaned Form Label is left behind. The\n * all-in-one Form Field of an older design is its own frame.\n */\nfunction consentWrapperId(nodes: NodesMap, inputId: string, formNodeId: string): string {\n const framing = new Set<string>([FORM_INPUT_ELEMENT_IDS.choiceLabel, FORM_INPUT_ELEMENT_IDS.formControl])\n let outer = inputId\n let parentId = String(nodes[inputId]?.parentId ?? '')\n while (parentId && parentId !== formNodeId) {\n const parent = nodes[parentId]\n if (!parent || !framing.has(String(parent.componentId))) break\n const answers = collectFormFieldNodeIds(nodes as never, parentId)\n if (!answers.every((id) => isMarketingConsentFieldName(formInputName(nodes[id] as never)))) break\n outer = parentId\n parentId = String(parent.parentId ?? '')\n }\n return outer\n}\n\n/** Removes a node and everything under it, and its id from its parent's children. */\nfunction removeSubtree(nodes: NodesMap, id: string): void {\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 const stack = [id]\n while (stack.length) {\n const next = stack.pop() as string\n const node = nodes[next]\n if (!node) continue\n if (Array.isArray(node.nodes)) stack.push(...(node.nodes as string[]))\n delete nodes[next]\n }\n}\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 fields in the order a visitor fills them in. Each piece of a field is its own element: a field is a Form Control (muiFormControl) holding a Form Label (muiFormLabel) whose text a visitor reads, then the answer, then optionally Helper Text (muiFormHelperText). Give the form a submitLabel and a successMessage in the site’s voice. The Form draws its own button, so a Button (muiButton) with type submit is optional.',\n 'The answer is an Input (muiInput) to type in: type email for an email address, tel for a phone number, multiline true for a description; a Select (muiSelect) holding one Select Option (muiSelectOption) per choice, or a Radio Group (muiRadioGroup) holding one Choice Label (muiFormControlLabel) per choice, each holding a Radio (muiRadio) whose value is the choice, for one choice; a Choice Label holding a Checkbox (muiCheckbox) per choice, the Checkboxes sharing one name, for several; and a Rating (muiRating) for a score. A single yes-or-no tick is a Choice Label holding one Checkbox, outside any Form Control.',\n 'Every Input, Select, Radio Group, Checkbox and Rating has a name that is unique in the form, apart from Checkboxes of one question, and says what it holds in camelCase, such as fullName, email, phone or roofType; and required only when the form cannot do its job without it.',\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 /** The dataset its submissions are also written to, and what was bound (AGL-3616). */\n dataset?: { dataset: AiFormDataset; binding: AiFormDatasetBinding }\n}\n\n/**\n * The draft a validated design and its answer make: the form node bound to\n * `formId` and captioned `name`, the platform's consent field when the\n * fields can yield an email address, the canvas root above, and the\n * declaration read off the result. A dataset binding is made only where the\n * plan said the form writes to one this step may bind (`identity.dataset`,\n * AGL-3616); any the model drew is dropped. 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; dataset?: AiFormDataset | null },\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: the\n // plan's to say (AGL-3616), bound below, never the model's to draw.\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 if (!nodes[id] || !isMarketingConsentFieldName(formInputName(nodes[id] as never))) continue\n removeSubtree(nodes, consentWrapperId(nodes, id, formNodeId))\n }\n if (formFieldsCanYieldAnEmail(formFieldDeclsFromNodes(nodes as never, formNodeId))) {\n for (const node of aiFormConsentNodes(formNodeId)) nodes[node.$id as string] = node\n form.nodes = [...(form.nodes as string[]), AI_FORM_CONSENT_NODE_ID]\n consentFieldName = AI_FORM_CONSENT_FIELD_NAME\n }\n form.parentId = CANVAS_ROOT_ELEMENT_ID\n }\n // The dataset the plan says the form writes to (AGL-3616), bound as the\n // Form element binds one.\n const dataset =\n identity.dataset && form?.componentId === FORM_COMPONENT_ID\n ? { dataset: identity.dataset, binding: aiBindFormToDataset(nodes, formNodeId, identity.dataset) }\n : undefined\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 ...(dataset ? { dataset } : {}),\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 dataset?: AiFormDataset | null\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\n/**\n * The fields the confirmed plan gives the form, against the form a COPY\n * actually produced (AGL-3024); `null` when the copy keeps the plan, or when\n * the plan promised no field to keep.\n *\n * The fourth kind to need this, after the layout, the page and the component.\n * `aiFormCopyUnboundFailure` above already refuses the one violation\n * duplication CAUSES; this refuses the one duplication HIDES — a plan reading\n * \"start from the consultation form and add a case-type question\" getting the\n * consultation form and nothing else, reported as built.\n *\n * A field the plan names is matched on its name alone: a copy that collects\n * the answer under a different label is still collecting it, and this check\n * has no way to tell a renamed field from a missing one without guessing.\n */\nexport async function aiCopiedFormReview(\n firestore: FirebaseFirestore.Firestore,\n input: { hostId: string; formId: string; name: string; plan: AiJobPlan | null },\n): Promise<AiJobReview | null> {\n const promised = (aiPlanCreation(input.plan, 'form')?.fields ?? [])\n .map((field) => field.split(':')[0]?.trim().toLowerCase())\n .filter((field): field is string => Boolean(field))\n if (!promised.length) return null\n const snapshot = await firestore\n .collection('hosts')\n .doc(input.hostId)\n .collection('forms')\n .doc(input.formId)\n .get()\n if (!snapshot.exists) {\n return {\n reason: 'doctrine',\n message: `\"${input.name}\" was copied from the form the plan names, and this job could not read the copy back to check it collects what the plan gives it. Open the form and check it before you put it on a page.`,\n findings: [],\n }\n }\n const fields = (snapshot.get('fields') ?? []) as unknown[]\n const collected = new Set(\n fields\n .map((field) =>\n field && typeof field === 'object'\n ? String((field as Record<string, unknown>)['fieldName'] ?? (field as Record<string, unknown>)['name'] ?? '')\n : '',\n )\n .filter(Boolean)\n .map((name) => name.toLowerCase()),\n )\n const missing = promised.filter((name) => !collected.has(name))\n if (!missing.length) return null\n const one = missing.length === 1\n return aiDoctrineReview({\n message: `\"${input.name}\" is a copy of the form the plan names, and a copy is all it is: the plan gives it ${\n one ? 'a field' : 'fields'\n } the copy does not collect.`,\n violations: [\n {\n rule: null,\n code: 'plan-fields-missing',\n message: `The confirmed plan says this form collects ${missing.join(', ')}, and the copy collects ${\n collected.size ? [...collected].join(', ') : 'nothing'\n }. Add ${one ? 'it' : 'each of them'}, or the copy is the form it was copied from.`,\n },\n ],\n })\n}\n\n/**\n * What the form a short copy would have started from collects, as the build\n * that replaces the copy is told it (AGL-3143 §14): every field it asks for,\n * so the build keeps them beside what the plan adds. Empty when the form\n * cannot be read.\n */\nexport async function aiFormSourceLines(\n firestore: FirebaseFirestore.Firestore,\n input: { hostId: string; id: string; name: string },\n): Promise<string[]> {\n const snapshot = await firestore.collection('hosts').doc(input.hostId).collection('forms').doc(input.id).get()\n const fields = snapshot.exists ? ((snapshot.get('fields') ?? []) as unknown[]) : []\n const asked = fields.flatMap((field) => {\n if (!field || typeof field !== 'object') return []\n const row = field as Record<string, unknown>\n const name = String(row['fieldName'] ?? row['name'] ?? '').trim()\n if (!name) return []\n const fieldType = row['fieldType'] ?? row['type']\n const type = typeof fieldType === 'string' ? ` (${fieldType}${row['required'] ? ', required' : ''})` : ''\n return [`${name}${type}`]\n })\n if (!asked.length) return []\n return [\n `The plan starts from a copy of the form \"${input.name}\", which lacks part of what the plan gives it. Build from it: it collects ${asked.join(', ')}. Keep each of those, and add what the plan adds.`,\n ]\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.options}\" 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/**\n * A form job is admitted for a site of the job's own org whose plan has\n * forms, with a form to spare, and confirmed only for a plan that creates\n * nothing but the form: a form places nothing another job builds\n * (AGL-3143 §15).\n */\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 ownCheck: (hostId) => aiJobPlanCreationsRefusal('form', context, hostId),\n })\n\n/**\n * What a form step says when the site's last form was taken after its plan\n * was confirmed (AGL-3765): by another build running beside it, or by a\n * member. The failure is ours, so what the job spent is given back.\n */\nexport const AI_FORM_BAND_TAKEN_COPY =\n 'Another build on this site used the last form your plan includes while this one was running, so this one stopped. Describe it again to build it with the form the site has now.'\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 /** The data plugin's writer lookup the form's dataset is read through (AGL-3616); specs hand in a fake. */\n datasetWriterFor?: AiFormDatasetReadDeps['writerFor']\n /** The form another build just made that a refused unit places (AGL-3765); specs hand in a fake. */\n siblingForm?: typeof aiSiblingJobForm\n}\n\nexport function createAiJobFormStep(deps: AiJobFormStepDeps = {}): AiJobStepRunner {\n const readInventory = deps.readInventory ?? readSiteInventory\n const duplicate = deps.duplicate ?? duplicateResource\n const siblingForm = deps.siblingForm ?? aiSiblingJobForm\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 // The site holds every form its plan includes (AGL-3765). A unit places a\n // form another build on this site just made, as its page would place any\n // form the site has. Otherwise a site that had room when the plan was\n // confirmed and was filled since stopped this job on our side, given\n // back; one that was already full is the workspace's limit to change. A\n // lookup that fails leaves the refusal as it was.\n const refused = async (\n refusal: string,\n spent: Pick<AiJobStepOutcome, 'outputs' | 'usage' | 'estCostUsd' | 'model' | 'stopReason'>,\n ): Promise<AiJobStepOutcome> => {\n const lookup = async <T>(read: () => Promise<T>, fallback: T): Promise<T> => {\n try {\n return await read()\n } catch (error) {\n console.error('ai form step: band refusal lookup failed', { jobId: job.$id, hostId, error })\n return fallback\n }\n }\n const reused = await lookup(() => siblingForm(firestore, { job, now }), null)\n if (reused) return { ...spent, outputs: [output(reused, { note: AI_FORM_REUSED_NOTE })] }\n const taken = await lookup(\n () => readAiDraftBandTakenSince(firestore, { kind: 'form', hostId, org, since: plan?.confirmedAt }),\n false,\n )\n if (taken) return { ...spent, failure: AI_FORM_BAND_TAKEN_COPY }\n return { ...spent, review: aiLimitReview(refusal) }\n }\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, and one that does not collect\n // a field the plan gives the form is built from the plan, told what the\n // source collects (AGL-3143 §14): copying it could only end in the review\n // below. A source this step cannot read still copies.\n const source = creation?.duplicateOf\n ? inventory.forms.find((form) => form.id === creation.duplicateOf)\n : undefined\n const sourceReview = source\n ? await aiCopiedFormReview(firestore, { hostId, formId: source.id, name: source.name, plan })\n : null\n const sourceShort = Boolean(sourceReview?.findings.length)\n const sourceLines =\n source && sourceShort ? await aiFormSourceLines(firestore, { hostId, id: source.id, name: source.name }) : []\n if (creation?.duplicateOf && source && !sourceShort) {\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 // And the one duplication hides: what the plan promised of the copy,\n // read back (AGL-3024). This branch generates nothing, so nothing\n // else can answer for it.\n const review = await aiCopiedFormReview(firestore, {\n hostId,\n formId: copy.id,\n name: copy.name,\n plan,\n })\n if (review) return aiUnspentOutcome(model, { review })\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 refused(copy.error, aiUnspentOutcome(model))\n }\n }\n\n const allowance = await aiDraftAllowanceRefusal(firestore, { kind: 'form', hostId, org })\n if (allowance) return refused(allowance, aiUnspentOutcome(model))\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 // The dataset the plan says the form writes to (AGL-3616), where this\n // workspace may bind one; otherwise the form is the form it always was.\n const dataset = await aiReadFormDataset(\n {\n hostId,\n id: creation?.writesTo,\n org,\n inventory,\n madeId: job.inputs?.[AI_FORM_DATASET_MADE_INPUT],\n },\n deps.datasetWriterFor ? { writerFor: deps.datasetWriterFor } : {},\n )\n const check = aiFormDraftCheck({ formId: draftId, name, decisions, dataset })\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: [\n aiJobFormPrompt(job, plan, name, {\n audience,\n submissions: decisions.submissions,\n }),\n ...sourceLines,\n ...(dataset ? [aiFormDatasetPromptLine(dataset)] : []),\n ].join('\\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 // Never written once the job is canceled (AGL-3616).\n throwIfAiJobCanceled(signal)\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 refused(write.error, spent)\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 draft.dataset ? aiFormDatasetNote(draft.dataset.dataset, draft.dataset.binding) : null,\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":["throwIfAiJobCanceled","checkFormContract","formFieldsCanYieldAnEmail","collectFormFieldNodeIds","FORM_COMPONENT_ID","FORM_ID_PROP","FORM_INPUT_ELEMENT_IDS","formFieldDeclsFromNodes","formInputName","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","AI_FORM_DATASET_MADE_INPUT","aiBindFormToDataset","aiFormDatasetNote","aiFormDatasetPromptLine","aiReadFormDataset","aiJobPlanCreationsRefusal","aiJobDraftId","aiDraftAdmissionRefusal","aiDraftAllowanceRefusal","aiSiteSubdomain","readAiDraft","readAiDraftBandTakenSince","writeAiDraft","AI_FORM_REUSED_NOTE","aiSiblingJobForm","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_FORM_CONSENT_CHECKBOX_NODE_ID","AI_FORM_CONSENT_FIELD_NAME","fieldName","AI_FORM_CONSENT_VALUE","aiFormConsentNodes","formNodeId","choiceLabel","checkbox","$id","type","NODE","componentId","pluginId","parentId","nodes","props","label","options","name","value","consentWrapperId","inputId","framing","Set","formControl","outer","String","parent","has","answers","every","id","removeSubtree","Array","isArray","filter","child","stack","length","next","pop","node","push","AI_JOB_FORM_INSTRUCTIONS","text","join","TREE_TOOL","AI_JOB_FORM_TOOL","description","strict","inputSchema","additionalProperties","required","properties","tree","routing","kind","enum","list","anyOf","cannotCollect","items","isRecord","oneLine","max","replace","trim","slice","parseAiFormAnswer","answer","decisions","submissions","proposed","includes","seen","entry","toLowerCase","add","aiFormDraft","identity","parsed","rootId","Object","entries","form","consentFieldName","formId","dataset","binding","undefined","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","aiCopiedFormReview","input","promised","plan","field","split","Boolean","exists","reason","findings","collected","missing","one","size","aiFormSourceLines","asked","flatMap","row","fieldType","listOf","aiFormOutputNote","sentences","aiJobFormPrompt","job","context","audience","SUBMISSION_WORDS","inbox","aiFormJobAdmission","orgId","org","ownCheck","AI_FORM_BAND_TAKEN_COPY","createAiJobFormStep","deps","readInventory","duplicate","siblingForm","now","signal","modelFor","orgSnapshot","Error","output","extra","resource","versionId","hostSubdomain","load","note","model","draftId","written","outputs","inventory","Promise","all","creation","refused","refusal","spent","lookup","read","fallback","error","console","jobId","reused","taken","since","confirmedAt","failure","review","source","duplicateOf","forms","sourceReview","sourceShort","sourceLines","sourceId","uid","createdBy","attemptKey","ok","status","allowance","inputs","writesTo","madeId","datasetWriterFor","writerFor","check","result","step","instructions","messages","role","content","tool","thinking","write","runAiJobFormStep","registerAiFormJob"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,oBAAoB,QAAQ,qBAAiB;AACtD,SACEC,iBAAiB,EACjBC,yBAAyB,QACpB,uCAAsC;AAC7C,SACEC,uBAAuB,EACvBC,iBAAiB,EACjBC,YAAY,EACZC,sBAAsB,EACtBC,uBAAuB,EACvBC,aAAa,EACbC,YAAY,EACZC,2BAA2B,EAC3BC,4BAA4B,QAIvB,+BAA8B;AACrC,SAASC,iBAAiB,QAAQ,sCAAqC;AACvE,SAASC,sBAAsB,QAAQ,2CAA0C;AAEjF,SAASC,QAAQ,QAAuB,2BAA0B;AAClE,SAASC,iBAAiB,QAAQ,qDAAoD;AAEtF,SAASC,iBAAiB,EAAEC,WAAW,QAAgC,0BAAsB;AAC7F,SAASC,aAAa,QAAQ,0BAAsB;AACpD,SAASC,gBAAgB,EAAEC,cAAc,QAAQ,0BAAsB;AACvE,SACEC,kBAAkB,EAClBC,sBAAsB,QAEjB,4BAAwB;AAG/B,SAASC,UAAU,QAAQ,qCAAiC;AAE5D,SAASC,iBAAiB,QAAQ,+BAA2B;AAC7D,SAASC,sBAAsB,QAA6B,wBAAoB;AAChF,SACEC,0BAA0B,EAC1BC,mBAAmB,EACnBC,iBAAiB,EACjBC,uBAAuB,EACvBC,iBAAiB,QAIZ,2BAAuB;AAC9B,SAASC,yBAAyB,QAAQ,6BAAyB;AACnE,SAASC,YAAY,QAAQ,wBAAoB;AACjD,SACEC,uBAAuB,EACvBC,uBAAuB,EACvBC,eAAe,EACfC,WAAW,EACXC,yBAAyB,EACzBC,YAAY,QAEP,qBAAiB;AACxB,SAASC,mBAAmB,EAAEC,gBAAgB,QAAQ,yBAAqB;AAC3E,SACEC,oBAAoB,EACpBC,eAAe,EACfC,gBAAgB,EAChBC,iBAAiB,EACjBC,cAAc,EACdC,aAAa,EACbC,cAAc,EACdC,cAAc,EACdC,oBAAoB,EACpBC,gBAAgB,QACX,yBAAqB;AAE5B,SAASC,eAAe,QAAQ,qBAAiB;AACjD,SAASC,iBAAiB,QAAQ,eAAW;AAE7C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkDC,GAED;;;;;CAKC,GACD,OAAO,MAAMC,yBAAyBlC,gBAAgB,CAAC,WAAW,CAACmC,SAAS,CAAA;AAE5E;;;;CAIC,GACD,OAAO,MAAMC,0BAA0BJ,gBAAgB;IACrDK,MAAMtC,aAAa,CAAC,WAAW;IAC/BoC,WAAWD;AACb,GAAE;AAEF,yDAAyD,GACzD,OAAO,MAAMI,8BAA8BF,wBAAwBG,SAAS,CAAA;AAE5E,4CAA4C,GAC5C,OAAO,MAAMC,2BAA2B,WAAU;AAElD,kDAAkD,GAClD,OAAO,MAAMC,wBAAwB;IAAC;IAAS;IAAQ;CAAO,CAAS;AAIvE,+EAA+E,GAC/E,OAAO,MAAMC,6BAA6B,EAAC;AAE3C,MAAMC,2BAA2B;AACjC,MAAMC,sBAAsB;AAE5B;;;CAGC,GACD,OAAO,MAAMC,0BAA0B,wBAAuB;AAE9D,yFAAyF,GACzF,OAAO,MAAMC,mCAAmC,2BAA0B;AAE1E,oFAAoF,GACpF,OAAO,MAAMC,6BAA6BvD,6BAA6BwD,SAAS,CAAA;AAEhF,4GAA4G,GAC5G,OAAO,MAAMC,wBAAwB,MAAK;AAE1C;;;;;CAKC,GACD,OAAO,SAASC,mBAAmBC,UAAkB;QAOrC/C,yBASAA;IAfd,MAAM,EAAEgD,WAAW,EAAEC,QAAQ,EAAE,GAAGlE;IAClC,OAAO;QACL;YACEmE,KAAKT;YACLU,MAAM5D,SAAS6D,IAAI;YACnBC,aAAaL;YACbM,QAAQ,GAAEtD,0BAAAA,UAAU,CAACgD,YAAY,qBAAvBhD,wBAAyBsD,QAAQ;YAC3CC,UAAUR;YACVS,OAAO;gBAACd;aAAiC;YACzCe,OAAO;gBAAEC,OAAOtE,6BAA6BuE,OAAO;YAAC;QACvD;QACA;YACET,KAAKR;YACLS,MAAM5D,SAAS6D,IAAI;YACnBC,aAAaJ;YACbK,QAAQ,GAAEtD,uBAAAA,UAAU,CAACiD,SAAS,qBAApBjD,qBAAsBsD,QAAQ;YACxCC,UAAUd;YACVe,OAAO,EAAE;YACTC,OAAO;gBAAEG,MAAMjB;gBAA4BkB,OAAOhB;YAAsB;QAC1E;KACD;AACH;AAEA;;;;;CAKC,GACD,SAASiB,iBAAiBN,KAAe,EAAEO,OAAe,EAAEhB,UAAkB;;QAGtDS;IAFtB,MAAMQ,UAAU,IAAIC,IAAY;QAAClF,uBAAuBiE,WAAW;QAAEjE,uBAAuBmF,WAAW;KAAC;IACxG,IAAIC,QAAQJ;IACZ,IAAIR,WAAWa,gBAAOZ,iBAAAA,KAAK,CAACO,QAAQ,qBAAdP,eAAgBD,QAAQ,mBAAI;IAClD,MAAOA,YAAYA,aAAaR,WAAY;YAMxBsB;QALlB,MAAMA,SAASb,KAAK,CAACD,SAAS;QAC9B,IAAI,CAACc,UAAU,CAACL,QAAQM,GAAG,CAACF,OAAOC,OAAOhB,WAAW,IAAI;QACzD,MAAMkB,UAAU3F,wBAAwB4E,OAAgBD;QACxD,IAAI,CAACgB,QAAQC,KAAK,CAAC,CAACC,KAAOtF,4BAA4BF,cAAcuE,KAAK,CAACiB,GAAG,KAAc;QAC5FN,QAAQZ;QACRA,WAAWa,QAAOC,mBAAAA,OAAOd,QAAQ,YAAfc,mBAAmB;IACvC;IACA,OAAOF;AACT;AAEA,mFAAmF,GACnF,SAASO,cAAclB,KAAe,EAAEiB,EAAU;;QACpBjB;IAA5B,MAAMa,SAASb,KAAK,CAACY,gBAAOZ,YAAAA,KAAK,CAACiB,GAAG,qBAATjB,UAAWD,QAAQ,mBAAI,IAAI;IACvD,IAAIc,UAAUM,MAAMC,OAAO,CAACP,OAAOb,KAAK,GAAG;QACzCa,OAAOb,KAAK,GAAG,AAACa,OAAOb,KAAK,CAAcqB,MAAM,CAAC,CAACC,QAAUA,UAAUL;IACxE;IACA,MAAMM,QAAQ;QAACN;KAAG;IAClB,MAAOM,MAAMC,MAAM,CAAE;QACnB,MAAMC,OAAOF,MAAMG,GAAG;QACtB,MAAMC,OAAO3B,KAAK,CAACyB,KAAK;QACxB,IAAI,CAACE,MAAM;QACX,IAAIR,MAAMC,OAAO,CAACO,KAAK3B,KAAK,GAAGuB,MAAMK,IAAI,IAAKD,KAAK3B,KAAK;QACxD,OAAOA,KAAK,CAACyB,KAAK;IACpB;AACF;AAEA,iEAAiE,GACjE,OAAO,MAAMI,2BAAqD;IAChE;QACEC,MAAM;YACJ;YACA;YACA;YACA;YACA;YACA;YACA;SACD,CAACC,IAAI,CAAC;IACT;CACD,CAAA;AAED,MAAMC,YAAY1F,mBAAmB;AAErC;;;;CAIC,GACD,OAAO,MAAM2F,mBAA2B;IACtC7B,MAAM4B,UAAU5B,IAAI;IACpB8B,aACE;IACFC,QAAQ;IACRC,aAAa;QACXzC,MAAM;QACN0C,sBAAsB;QACtBC,UAAU;YAAC;YAAQ;YAAW;SAAgB;QAC9CC,YAAY;YACVC,MAAM,AAACR,UAAUI,WAAW,CAAC,aAAa,AAA4B,CAAC,OAAO;YAC9EK,SAAS;gBACP9C,MAAM;gBACN0C,sBAAsB;gBACtBC,UAAU;oBAAC;oBAAQ;iBAAO;gBAC1BC,YAAY;oBACVG,MAAM;wBACJ/C,MAAM;wBACNgD,MAAM;+BAAI9D;yBAAsB;wBAChCqD,aACE;oBACJ;oBACAU,MAAM;wBACJC,OAAO;4BAAC;gCAAElD,MAAM;4BAAS;4BAAG;gCAAEA,MAAM;4BAAO;yBAAE;wBAC7CuC,aAAa;oBACf;gBACF;YACF;YACAY,eAAe;gBACbnD,MAAM;gBACNoD,OAAO;oBAAEpD,MAAM;gBAAS;gBACxBuC,aACE;YACJ;QACF;IACF;AACF,EAAC;AAwBD,SAASc,SAAS3C,KAAc;IAC9B,OAAO,OAAOA,UAAU,YAAYA,UAAU,QAAQ,CAACc,MAAMC,OAAO,CAACf;AACvE;AAEA,SAAS4C,QAAQ5C,KAAa,EAAE6C,GAAW;IACzC,OAAO7C,MAAM8C,OAAO,CAAC,QAAQ,KAAKC,IAAI,GAAGC,KAAK,CAAC,GAAGH,KAAKE,IAAI;AAC7D;AAEA;;;;;CAKC,GACD,OAAO,SAASE,kBACdC,MAA+B,EAC/BC,YAA6B;IAAEC,aAAa;AAAK,CAAC;QAMlBD;IAJhC,MAAMf,UAAUO,SAASO,MAAM,CAAC,UAAU,IAAIA,MAAM,CAAC,UAAU,GAAG,CAAC;IACnE,MAAMG,WAAW,AAAC7E,sBAA6C8E,QAAQ,CAAClB,OAAO,CAAC,OAAO,IAClFA,OAAO,CAAC,OAAO,GAChB;IACJ,MAAMC,QAA0Bc,yBAAAA,UAAUC,WAAW,YAArBD,yBAAyBE;IACzD,MAAMd,OAAO,OAAOH,OAAO,CAAC,OAAO,KAAK,WAAWQ,QAAQR,OAAO,CAAC,OAAO,EAAEzD,uBAAuB;IACnG,MAAM4E,OAAO,IAAInD;IACjB,MAAMqC,gBAA0B,EAAE;IAClC,KAAK,MAAMe,SAAS1C,MAAMC,OAAO,CAACmC,MAAM,CAAC,gBAAgB,IAAIA,MAAM,CAAC,gBAAgB,GAAG,EAAE,CAAE;QACzF,IAAI,OAAOM,UAAU,UAAU;QAC/B,MAAM/B,OAAOmB,QAAQY,OAAO9E;QAC5B,IAAI,CAAC+C,QAAQ8B,KAAK9C,GAAG,CAACgB,KAAKgC,WAAW,KAAK;QAC3CF,KAAKG,GAAG,CAACjC,KAAKgC,WAAW;QACzBhB,cAAclB,IAAI,CAACE;QACnB,IAAIgB,cAActB,MAAM,KAAK1C,4BAA4B;IAC3D;IACA,OAAO;QAAE2D,SAAS;YAAEC;YAAME,MAAMF,SAAS,UAAUE,OAAOA,OAAO;QAAK;QAAGE;IAAc;AACzF;AAgBA;;;;;;;;CAQC,GACD,OAAO,SAASkB,YACdxB,IAA+C,EAC/Ce,MAA+B,EAC/BU,QAAuG;QAE9DA;IAAzC,MAAMC,SAASZ,kBAAkBC,SAAQU,sBAAAA,SAAST,SAAS,YAAlBS,sBAAsB;QAAER,aAAa;IAAK;IACnF,MAAMlE,aAAaiD,KAAK2B,MAAM;IAC9B,MAAMnE,QAAkB,CAAC;IACzB,KAAK,MAAM,CAACiB,IAAIU,KAAK,IAAIyC,OAAOC,OAAO,CAAC7B,KAAKxC,KAAK,EAAG;YAGpC2B;QAFf3B,KAAK,CAACiB,GAAG,GAAG,aACPU;YACH1B,OAAO,cAAM0B,cAAAA,KAAK1B,KAAK,YAAV0B,cAAc,CAAC;YAC5B3B,OAAOmB,MAAMC,OAAO,CAACO,KAAK3B,KAAK,IAAI;mBAAK2B,KAAK3B,KAAK;aAAc,GAAG,EAAE;;IAEzE;IACA,MAAMsE,OAAOtE,KAAK,CAACT,WAAW;IAC9B,IAAIgF,mBAAkC;IACtC,IAAID,CAAAA,wBAAAA,KAAMzE,WAAW,MAAKxE,mBAAmB;QAC3C,MAAM4E,QAAQqE,KAAKrE,KAAK;QACxB,wEAAwE;QACxE,oEAAoE;QACpE,OAAOA,KAAK,CAAC,YAAY;QACzB,OAAOA,KAAK,CAAC,cAAc;QAC3BA,KAAK,CAAC3E,aAAa,GAAG2I,SAASO,MAAM;QACrCvE,KAAK,CAAC,WAAW,GAAGgE,SAAS7D,IAAI;QAEjC,KAAK,MAAMa,MAAM7F,wBAAwB4E,OAAgBT,YAAa;YACpE,IAAI,CAACS,KAAK,CAACiB,GAAG,IAAI,CAACtF,4BAA4BF,cAAcuE,KAAK,CAACiB,GAAG,IAAa;YACnFC,cAAclB,OAAOM,iBAAiBN,OAAOiB,IAAI1B;QACnD;QACA,IAAIpE,0BAA0BK,wBAAwBwE,OAAgBT,cAAc;YAClF,KAAK,MAAMoC,QAAQrC,mBAAmBC,YAAaS,KAAK,CAAC2B,KAAKjC,GAAG,CAAW,GAAGiC;YAC/E2C,KAAKtE,KAAK,GAAG;mBAAKsE,KAAKtE,KAAK;gBAAef;aAAwB;YACnEsF,mBAAmBpF;QACrB;QACAmF,KAAKvE,QAAQ,GAAGjE;IAClB;IACA,wEAAwE;IACxE,0BAA0B;IAC1B,MAAM2I,UACJR,SAASQ,OAAO,IAAIH,CAAAA,wBAAAA,KAAMzE,WAAW,MAAKxE,oBACtC;QAAEoJ,SAASR,SAASQ,OAAO;QAAEC,SAAS9H,oBAAoBoD,OAAOT,YAAY0E,SAASQ,OAAO;IAAE,IAC/FE;IACN3E,KAAK,CAAClE,uBAAuB,GAAG;QAC9B4D,KAAK5D;QACL+D,aAAa;QACbG,OAAO;YAACT;SAAW;IACrB;IACA,OAAO;QACLS;QACAmE,QAAQrI;QACRyD;QACAqF,QAAQpJ,wBAAwBwE,OAAgBT;QAChDgF;QACA9B,SAASyB,OAAOzB,OAAO,CAACC,IAAI,KAAK,SAAS;YAAEmC,MAAM;QAAK,IAAI;QAC3DtB,QAAQW;OACJO,UAAU;QAAEA;IAAQ,IAAI,CAAC;AAEjC;AAEA;;;;;;CAMC,GACD,OAAO,SAASK,sBACdC,KAAkB,EAClBP,MAAc,EACdQ,SAA2C;IAE3C,MAAMC,aAAoC/J,kBAAkB;QAC1DoJ,MAAM,aACAS,MAAMtC,OAAO,GAAG;YAAEA,SAASsC,MAAMtC,OAAO;QAAC,IAAI,CAAC,GAC9CsC,MAAMR,gBAAgB,GAAG;YAAEA,kBAAkBQ,MAAMR,gBAAgB;QAAC,IAAI,CAAC;QAE/EC;QACAxE,OAAO+E,MAAM/E,KAAK;QAClBT,YAAYwF,MAAMxF,UAAU;IAC9B,GAAG2F,GAAG,CAAC,CAACC,YAAe;YACrBC,MAAM;YACNC,MAAMF,UAAUE,IAAI;YACpBC,SAASH,UAAUG,OAAO;WACtBH,UAAUI,MAAM,GAAG;YAAEC,SAASxH,eAAe;gBAACmH,UAAUI,MAAM;aAAC,EAAEP;QAAW,IAAI,CAAC;IAEvF,IAAI,CAACD,MAAMH,MAAM,CAACpD,MAAM,EAAE;QACxByD,WAAWrD,IAAI,CAAC;YACdwD,MAAM;YACNC,MAAM;YACNC,SACE;QACJ;IACF;IACA,IAAIP,MAAMxB,MAAM,CAACd,OAAO,CAACC,IAAI,KAAK,UAAU,CAACvH,0BAA0B4J,MAAMH,MAAM,GAAG;QACpFK,WAAWrD,IAAI,CAAC;YACdwD,MAAM;YACNC,MAAM;YACNC,SACE;YACFG,OAAO;gBAAC;aAAe;QACzB;IACF;IACA,OAAOR;AACT;AAEA;;;;CAIC,GACD,OAAO,SAASS,iBAAiBzB,QAKhC;IAIC,MAAM0B,SAAS,IAAIC;IACnB,OAAO;QACLC,QAAQ,CAACrD,MAAMe;YACb,MAAMwB,QAAQf,YAAYxB,MAAMe,QAAQU;YACxC0B,OAAOG,GAAG,CAACtD,MAAMuC;YACjB,OAAOD,sBAAsBC,OAAOd,SAASO,MAAM,EAAEhC,KAAKwC,SAAS;QACrE;QACAe,UAAU,CAACvD,OAASmD,OAAOK,GAAG,CAACxD;IACjC;AACF;AAEA;;;;CAIC,GACD,OAAO,MAAMyD,+BACX,+EACA,4EAA2E;AAE7E;;;;;;;;;;;;;;;;;;;;;;;;;CAyBC,GACD,OAAO,eAAeC,yBACpBC,SAAsC,EACtCC,IAAwC;QAQ1BC;IANd,MAAMA,WAAW,MAAMF,UACpBG,UAAU,CAAC,SACXC,GAAG,CAACH,KAAKI,MAAM,EACfF,UAAU,CAAC,SACXC,GAAG,CAACH,KAAK5B,MAAM,EACfwB,GAAG;IACN,MAAM1B,QAAQ+B,iBAAAA,SAASI,IAAI,cAAbJ,iBAAmB;IACjC,MAAMrG,QAAQnE,kBAAkByI,wBAAAA,KAAMtE,KAAK;IAC3C,MAAM0G,UAAUxL,kBAAkB;QAChCoJ;QACAE,QAAQ4B,KAAK5B,MAAM;QACnBxE,OAAOA;QACPT,YAAY7D,aAAasE;IAC3B,GAAG2G,IAAI,CAAC,CAACxB,YAAcA,UAAUE,IAAI,KAAK;IAC1C,OAAOqB,UAAU,GAAGT,6BAA6B,CAAC,EAAES,QAAQpB,OAAO,EAAE,GAAG;AAC1E;AAEA;;;;;;;;;;;;;;CAcC,GACD,OAAO,eAAesB,mBACpBT,SAAsC,EACtCU,KAA+E;cAmB/DR;QAjBEpI;IAAlB,MAAM6I,WAAW,UAAC7I,kBAAAA,eAAe4I,MAAME,IAAI,EAAE,4BAA3B9I,gBAAoC2G,MAAM,mBAAI,EAAE,EAC/DM,GAAG,CAAC,CAAC8B;YAAUA;gBAAAA,gBAAAA,MAAMC,KAAK,CAAC,IAAI,CAAC,EAAE,qBAAnBD,cAAqB5D,IAAI,GAAGU,WAAW;OACtDzC,MAAM,CAAC,CAAC2F,QAA2BE,QAAQF;IAC9C,IAAI,CAACF,SAAStF,MAAM,EAAE,OAAO;IAC7B,MAAM6E,WAAW,MAAMF,UACpBG,UAAU,CAAC,SACXC,GAAG,CAACM,MAAML,MAAM,EAChBF,UAAU,CAAC,SACXC,GAAG,CAACM,MAAMrC,MAAM,EAChBwB,GAAG;IACN,IAAI,CAACK,SAASc,MAAM,EAAE;QACpB,OAAO;YACLC,QAAQ;YACR9B,SAAS,CAAC,CAAC,EAAEuB,MAAMzG,IAAI,CAAC,yLAAyL,CAAC;YAClNiH,UAAU,EAAE;QACd;IACF;IACA,MAAMzC,UAAUyB,gBAAAA,SAASL,GAAG,CAAC,qBAAbK,gBAA0B,EAAE;IAC5C,MAAMiB,YAAY,IAAI7G,IACpBmE,OACGM,GAAG,CAAC,CAAC8B;YAEO,MAAA;eADXA,SAAS,OAAOA,UAAU,WACtBpG,QAAO,QAAA,mBAAA,AAACoG,KAAiC,CAAC,YAAY,YAA/C,mBAAmD,AAACA,KAAiC,CAAC,OAAO,YAA7F,OAAiG,MACxG;OAEL3F,MAAM,CAAC6F,SACPhC,GAAG,CAAC,CAAC9E,OAASA,KAAK0D,WAAW;IAEnC,MAAMyD,UAAUT,SAASzF,MAAM,CAAC,CAACjB,OAAS,CAACkH,UAAUxG,GAAG,CAACV;IACzD,IAAI,CAACmH,QAAQ/F,MAAM,EAAE,OAAO;IAC5B,MAAMgG,MAAMD,QAAQ/F,MAAM,KAAK;IAC/B,OAAO5D,iBAAiB;QACtB0H,SAAS,CAAC,CAAC,EAAEuB,MAAMzG,IAAI,CAAC,mFAAmF,EACzGoH,MAAM,YAAY,SACnB,2BAA2B,CAAC;QAC7BvC,YAAY;YACV;gBACEG,MAAM;gBACNC,MAAM;gBACNC,SAAS,CAAC,2CAA2C,EAAEiC,QAAQxF,IAAI,CAAC,MAAM,wBAAwB,EAChGuF,UAAUG,IAAI,GAAG;uBAAIH;iBAAU,CAACvF,IAAI,CAAC,QAAQ,UAC9C,MAAM,EAAEyF,MAAM,OAAO,eAAe,6CAA6C,CAAC;YACrF;SACD;IACH;AACF;AAEA;;;;;CAKC,GACD,OAAO,eAAeE,kBACpBvB,SAAsC,EACtCU,KAAmD;QAGhBR;IADnC,MAAMA,WAAW,MAAMF,UAAUG,UAAU,CAAC,SAASC,GAAG,CAACM,MAAML,MAAM,EAAEF,UAAU,CAAC,SAASC,GAAG,CAACM,MAAM5F,EAAE,EAAE+E,GAAG;IAC5G,MAAMpB,SAASyB,SAASc,MAAM,IAAKd,gBAAAA,SAASL,GAAG,CAAC,qBAAbK,gBAA0B,EAAE,GAAkB,EAAE;IACnF,MAAMsB,QAAQ/C,OAAOgD,OAAO,CAAC,CAACZ;YAGRa,MAAAA,gBAEFA;QAJlB,IAAI,CAACb,SAAS,OAAOA,UAAU,UAAU,OAAO,EAAE;QAClD,MAAMa,MAAMb;QACZ,MAAM5G,OAAOQ,QAAOiH,QAAAA,iBAAAA,GAAG,CAAC,YAAY,YAAhBA,iBAAoBA,GAAG,CAAC,OAAO,YAA/BA,OAAmC,IAAIzE,IAAI;QAC/D,IAAI,CAAChD,MAAM,OAAO,EAAE;QACpB,MAAM0H,aAAYD,iBAAAA,GAAG,CAAC,YAAY,YAAhBA,iBAAoBA,GAAG,CAAC,OAAO;QACjD,MAAMlI,OAAO,OAAOmI,cAAc,WAAW,CAAC,EAAE,EAAEA,YAAYD,GAAG,CAAC,WAAW,GAAG,eAAe,GAAG,CAAC,CAAC,GAAG;QACvG,OAAO;YAAC,GAAGzH,OAAOT,MAAM;SAAC;IAC3B;IACA,IAAI,CAACgI,MAAMnG,MAAM,EAAE,OAAO,EAAE;IAC5B,OAAO;QACL,CAAC,yCAAyC,EAAEqF,MAAMzG,IAAI,CAAC,0EAA0E,EAAEuH,MAAM5F,IAAI,CAAC,MAAM,iDAAiD,CAAC;KACvM;AACH;AAEA,SAASgG,OAAOhF,KAAwB;IACtC,OAAOA,MAAMvB,MAAM,GAAG,IAClBuB,MAAMhB,IAAI,CAAC,MACX,GAAGgB,MAAMM,KAAK,CAAC,GAAG,CAAC,GAAGtB,IAAI,CAAC,MAAM,KAAK,EAAEgB,KAAK,CAACA,MAAMvB,MAAM,GAAG,EAAE,EAAE;AACvE;AAEA;;;;CAIC,GACD,OAAO,SAASwG,iBAAiBzE,MAAoB;IACnD,MAAM0E,YAAsB,EAAE;IAC9B,IAAI1E,OAAOd,OAAO,CAACC,IAAI,KAAK,QAAQ;QAClCuF,UAAUrG,IAAI,CACZ;IAEJ;IACA,IAAI2B,OAAOd,OAAO,CAACC,IAAI,KAAK,QAAQ;QAClC,MAAME,OAAOW,OAAOd,OAAO,CAACG,IAAI,GAAG,CAAC,MAAM,EAAEW,OAAOd,OAAO,CAACG,IAAI,CAAC,MAAM,CAAC,GAAG;QAC1EqF,UAAUrG,IAAI,CACZ,CAAC,6DAA6D,EAAEhG,6BAA6BuE,OAAO,CAAC,KAAK,EAAEyC,KAAK,oEAAoE,CAAC;IAE1L;IACA,IAAIW,OAAOT,aAAa,CAACtB,MAAM,EAAE;QAC/ByG,UAAUrG,IAAI,CACZ,CAAC,qBAAqB,EAAEmG,OAAOxE,OAAOT,aAAa,EAAE,0BAA0B,EAC7ES,OAAOT,aAAa,CAACtB,MAAM,KAAK,IAAI,OAAO,OAC5C,KAAK,CAAC;IAEX;IACA,OAAOyG,UAAUzG,MAAM,GAAGyG,UAAUlG,IAAI,CAAC,OAAO;AAClD;AAEA;;;;;;;;;;;;;;;CAeC,GACD,OAAO,SAASmG,gBACdC,GAAyB,EACzBpB,IAAsB,EACtB3G,IAAY,EACZgI,UAAyE,CAAC,CAAC;QAEzDA;IAAlB,MAAMC,WAAW,EAACD,oBAAAA,QAAQC,QAAQ,YAAhBD,oBAAoB,IAAIhF,IAAI;IAC9C,OAAO;QACL,CAAC,WAAW,EAAEhD,MAAM;QACpBiI,WAAW,CAAC,2BAA2B,EAAEA,UAAU,GAAG;QACtDD,QAAQ3E,WAAW,GAAG,CAAC,0BAA0B,EAAE6E,gBAAgB,CAACF,QAAQ3E,WAAW,CAAC,EAAE,GAAG;QAC7F3F,eAAeqK;WACZjK,qBAAqB6I;KACzB,CACE1F,MAAM,CAAC6F,SACPnF,IAAI,CAAC;AACV;AAEA,8EAA8E,GAC9E,MAAMuG,mBAAsD;IAC1DC,OAAO;IACP1D,MAAM;AACR;AAEA;;;;;CAKC,GACD,OAAO,MAAM2D,qBAAqC,CAACJ,UACjDlL,wBAAwBkL,QAAQjC,SAAS,EAAE;QACzCsC,OAAOL,QAAQK,KAAK;QACpBjC,QAAQ4B,QAAQ5B,MAAM;QACtB9D,MAAM;QACNgG,KAAKN,QAAQM,GAAG;QAChBC,UAAU,CAACnC,SAAWxJ,0BAA0B,QAAQoL,SAAS5B;IACnE,GAAE;AAEJ;;;;CAIC,GACD,OAAO,MAAMoC,0BACX,kLAAiL;AAanL,OAAO,SAASC,oBAAoBC,OAA0B,CAAC,CAAC;QACxCA,qBACJA,iBACEA;IAFpB,MAAMC,iBAAgBD,sBAAAA,KAAKC,aAAa,YAAlBD,sBAAsBrM;IAC5C,MAAMuM,aAAYF,kBAAAA,KAAKE,SAAS,YAAdF,kBAAkB9M;IACpC,MAAMiN,eAAcH,oBAAAA,KAAKG,WAAW,YAAhBH,oBAAoBrL;IACxC,OAAO,OAAO,EAAE0K,GAAG,EAAEe,GAAG,EAAEC,MAAM,EAAEhD,SAAS,EAAEiD,QAAQ,EAAE;kBA4BxCC;YAsGDlB;QAjIZ,MAAM3B,SAAS2B,IAAI3B,MAAM;QACzB,IAAI,CAACA,QAAQ,MAAM,IAAI8C,MAAM;QAC7B,MAAMC,SAAS,CACbxE,OACAyE,QAAgE,CAAC,CAAC,GACjD;gBACjBC,UAAU;gBACVxI,IAAI8D,MAAM9D,EAAE;gBACZyI,WAAW3E,MAAM2E,SAAS;gBAC1BlD;gBACAmD,eAAe5E,MAAM4E,aAAa;gBAClCzJ,OAAO6E,MAAM3E,IAAI;eACboJ,MAAMI,IAAI,GAAG;gBAAEA,MAAMJ,MAAMI,IAAI;YAAC,IAAI,CAAC,GACrCJ,MAAMK,IAAI,GAAG;gBAAEA,MAAML,MAAMK,IAAI;YAAC,IAAI,CAAC;QAE3C,yEAAyE;QACzE,MAAMC,gBAAQV,4BAAAA,SAAW,8BAAe/M,eAAe;QAEvD,kGAAkG;QAClG,MAAM0N,UAAU9M,aAAakL,KAAK;QAClC,MAAM6B,UAAU,MAAM3M,YAAY8I,WAAW;YAAEzD,MAAM;YAAQ8D;YAAQvF,IAAI8I;QAAQ;QACjF,IAAIC,SAAS,OAAO7L,iBAAiB2L,OAAO;YAAEG,SAAS;gBAACV,OAAOS;aAAS;QAAC;QAEzE,MAAM,CAACE,WAAWb,YAAY,GAAG,MAAMc,QAAQC,GAAG,CAAC;YACjDrB,cAAcZ,IAAIM,KAAK,EAAEjC,QAAQ;gBAAEL;YAAU;YAC7CA,UAAUG,UAAU,CAAC,QAAQC,GAAG,CAAC4B,IAAIM,KAAK,EAAEzC,GAAG;SAChD;QACD,MAAM0C,OAAOW,oBAAAA,YAAY5C,IAAI,cAAhB4C,oBAAsB;QACnC,MAAMtC,OAAOpJ,gBAAgBwK;QAC7B,MAAMkC,WAAWpM,eAAe8I,MAAM;QACtC,MAAM3G,OAAOiK,CAAAA,4BAAAA,SAAUjK,IAAI,KAAIxB;QAC/B,0EAA0E;QAC1E,yEAAyE;QACzE,sEAAsE;QACtE,qEAAqE;QACrE,wEAAwE;QACxE,kDAAkD;QAClD,MAAM0L,UAAU,OACdC,SACAC;YAEA,MAAMC,SAAS,OAAUC,MAAwBC;gBAC/C,IAAI;oBACF,OAAO,MAAMD;gBACf,EAAE,OAAOE,OAAO;oBACdC,QAAQD,KAAK,CAAC,4CAA4C;wBAAEE,OAAO3C,IAAIzI,GAAG;wBAAE8G;wBAAQoE;oBAAM;oBAC1F,OAAOD;gBACT;YACF;YACA,MAAMI,SAAS,MAAMN,OAAO,IAAMxB,YAAY9C,WAAW;oBAAEgC;oBAAKe;gBAAI,IAAI;YACxE,IAAI6B,QAAQ,OAAO,aAAKP;gBAAOP,SAAS;oBAACV,OAAOwB,QAAQ;wBAAElB,MAAMrM;oBAAoB;iBAAG;;YACvF,MAAMwN,QAAQ,MAAMP,OAClB,IAAMnN,0BAA0B6I,WAAW;oBAAEzD,MAAM;oBAAQ8D;oBAAQkC;oBAAKuC,KAAK,EAAElE,wBAAAA,KAAMmE,WAAW;gBAAC,IACjG;YAEF,IAAIF,OAAO,OAAO,aAAKR;gBAAOW,SAASvC;;YACvC,OAAO,aAAK4B;gBAAOY,QAAQrN,cAAcwM;;QAC3C;QAEA,yEAAyE;QACzE,uEAAuE;QACvE,0EAA0E;QAC1E,wEAAwE;QACxE,0EAA0E;QAC1E,sDAAsD;QACtD,MAAMc,SAAShB,CAAAA,4BAAAA,SAAUiB,WAAW,IAChCpB,UAAUqB,KAAK,CAAC5E,IAAI,CAAC,CAACrC,OAASA,KAAKrD,EAAE,KAAKoJ,SAASiB,WAAW,IAC/D3G;QACJ,MAAM6G,eAAeH,SACjB,MAAMzE,mBAAmBT,WAAW;YAAEK;YAAQhC,QAAQ6G,OAAOpK,EAAE;YAAEb,MAAMiL,OAAOjL,IAAI;YAAE2G;QAAK,KACzF;QACJ,MAAM0E,cAAcvE,QAAQsE,gCAAAA,aAAcnE,QAAQ,CAAC7F,MAAM;QACzD,MAAMkK,cACJL,UAAUI,cAAc,MAAM/D,kBAAkBvB,WAAW;YAAEK;YAAQvF,IAAIoK,OAAOpK,EAAE;YAAEb,MAAMiL,OAAOjL,IAAI;QAAC,KAAK,EAAE;QAC/G,IAAIiK,CAAAA,4BAAAA,SAAUiB,WAAW,KAAID,UAAU,CAACI,aAAa;YACnD,MAAMrF,OAAO,MAAM4C,UAAU,QAAQ;gBACnCP,OAAON,IAAIM,KAAK;gBAChBjC;gBACAmF,UAAUtB,SAASiB,WAAW;gBAC9BlL;gBACAwL,KAAKzD,IAAI0D,SAAS;gBAClBnD,KAAKA;gBACL,sEAAsE;gBACtE,oEAAoE;gBACpE,sEAAsE;gBACtE,mEAAmE;gBACnEoD,YAAY/B;YACd;YACA,IAAI3D,KAAK2F,EAAE,EAAE;gBACX,oEAAoE;gBACpE,kEAAkE;gBAClE,sCAAsC;gBACtC,MAAMrF,UAAU,MAAMR,yBAAyBC,WAAW;oBAAEK;oBAAQhC,QAAQ4B,KAAKnF,EAAE;gBAAC;gBACpF,IAAIyF,SAAS,OAAO,aAAKvI,iBAAiB2L;oBAAQqB,SAASzE;;gBAC3D,MAAMiD,gBAAgB,MAAMvM,gBAAgB+I,WAAWK;gBACvD,qEAAqE;gBACrE,kEAAkE;gBAClE,0BAA0B;gBAC1B,MAAM4E,SAAS,MAAMxE,mBAAmBT,WAAW;oBACjDK;oBACAhC,QAAQ4B,KAAKnF,EAAE;oBACfb,MAAMgG,KAAKhG,IAAI;oBACf2G;gBACF;gBACA,IAAIqE,QAAQ,OAAOjN,iBAAiB2L,OAAO;oBAAEsB;gBAAO;gBACpD,OAAOjN,iBAAiB2L,OAAO;oBAC7BG,SAAS;wBAACV,OAAO;4BAAEtI,IAAImF,KAAKnF,EAAE;4BAAEyI,WAAWtD,KAAKsD,SAAS;4BAAEtJ,MAAMgG,KAAKhG,IAAI;4BAAEuJ;wBAAc;qBAAG;gBAC/F;YACF;YACA,IAAIvD,KAAK2F,EAAE,KAAK,SAAS3F,KAAK4F,MAAM,KAAK,KAAK;gBAC5C,OAAO1B,QAAQlE,KAAKwE,KAAK,EAAEzM,iBAAiB2L;YAC9C;QACF;QAEA,MAAMmC,YAAY,MAAM9O,wBAAwBgJ,WAAW;YAAEzD,MAAM;YAAQ8D;YAAQkC;QAAI;QACvF,IAAIuD,WAAW,OAAO3B,QAAQ2B,WAAW9N,iBAAiB2L;QAE1D,kEAAkE;QAClE,wEAAwE;QACxE,MAAMtG,YAA6B;YAAEC,aAAaxH,kBAAkBkM,IAAI+D,MAAM;QAAE;QAChF,MAAM,EAAE7D,QAAQ,EAAE,GAAGnM,YAAYiM,IAAI+D,MAAM;QAC3C,sEAAsE;QACtE,wEAAwE;QACxE,MAAMzH,UAAU,MAAM1H,kBACpB;YACEyJ;YACAvF,EAAE,EAAEoJ,4BAAAA,SAAU8B,QAAQ;YACtBzD;YACAwB;YACAkC,MAAM,GAAEjE,cAAAA,IAAI+D,MAAM,qBAAV/D,WAAY,CAACxL,2BAA2B;QAClD,GACAmM,KAAKuD,gBAAgB,GAAG;YAAEC,WAAWxD,KAAKuD,gBAAgB;QAAC,IAAI,CAAC;QAElE,MAAME,QAAQ7G,iBAAiB;YAAElB,QAAQuF;YAAS3J;YAAMoD;YAAWiB;QAAQ;QAC3E,MAAM+H,SAAS,MAAMjQ,uBAAuB,QAAQ;YAClDkQ,MAAM;YACN3C;YACA4C,cAAc7K;YACdqI;YACAyC,UAAU;gBACR;oBACEC,MAAM;oBACNC,SAAS;wBACP3E,gBAAgBC,KAAKpB,MAAM3G,MAAM;4BAC/BiI;4BACA5E,aAAaD,UAAUC,WAAW;wBACpC;2BACGiI;2BACCjH,UAAU;4BAAC3H,wBAAwB2H;yBAAS,GAAG,EAAE;qBACtD,CAAC1C,IAAI,CAAC;gBACT;aACD;YACD+K,MAAM7K;YACN1D,WAAWC,wBAAwBD,SAAS,CAACuL;YAC7CiD,UAAU;YACVlH,QAAQ0G,MAAM1G,MAAM;WAChBsD,SAAS;YAAEA;QAAO,IAAI,CAAC;QAE7B,MAAMqB,QAAQ3M,kBAAkB2O;QAChC,IAAIA,OAAOR,MAAM,KAAK,WAAW,OAAO,aAAKxB;YAAOF,SAAS;;QAC7D,IAAIkC,OAAOR,MAAM,KAAK,eAAe,OAAO,aAAKxB;YAAOY,QAAQxN,iBAAiB4O;;QAEjF,MAAMzH,QAAQwH,MAAMxG,QAAQ,CAACyG,OAAOnM,KAAK;QACzC,IAAI,CAAC0E,OAAO,MAAM,IAAIuE,MAAM;QAC5B,qDAAqD;QACrDrO,qBAAqBkO;QACrB,MAAM6D,QAAQ,MAAMzP,aAAa4I,WAAW;YAC1CzD,MAAM;YACN8D;YACAvF,IAAI8I;YACJ6B,KAAKzD,IAAI0D,SAAS;YAClBnD;YACAtI;YACAJ,OAAO+E,MAAM/E,KAAK;YAClBsE,MAAM;gBACJH,QAAQY,MAAMZ,MAAM;gBACpB5E,YAAYwF,MAAMxF,UAAU;gBAC5BqF,QAAQG,MAAMH,MAAM;gBACpBL,kBAAkBQ,MAAMR,gBAAgB;gBACxC9B,SAASsC,MAAMtC,OAAO;YACxB;YACAyG;QACF;QACA,IAAI8D,MAAMjB,EAAE,KAAK,OAAO;YACtB,IAAIiB,MAAMhB,MAAM,KAAK,KAAK,MAAM,IAAI1C,MAAM,CAAC,KAAK,EAAE9C,OAAO,sCAAsC,CAAC;YAChG,OAAO8D,QAAQ0C,MAAMpC,KAAK,EAAEJ;QAC9B;QACA,kFAAkF;QAClF,MAAMX,OAAO;YACX7B,iBAAiBjD,MAAMxB,MAAM;YAC7BwB,MAAMN,OAAO,GAAG5H,kBAAkBkI,MAAMN,OAAO,CAACA,OAAO,EAAEM,MAAMN,OAAO,CAACC,OAAO,IAAI;YAClFhH,qBAAqB;gBACnB8E,MAAM;oBAAE2B,QAAQqI,OAAOnM,KAAK,CAAC8D,MAAM;oBAAEnE,OAAOwM,OAAOnM,KAAK,CAACL,KAAK;gBAAuC;gBACrGkK;YACF;SACD,CACE7I,MAAM,CAAC6F,SACPnF,IAAI,CAAC;QACR,OAAO,aACFyI;YACHP,SAAS;gBAACV,OAAOyD,OAAO;oBAAEpD,MAAM4C,OAAOnM,KAAK,CAACuJ,IAAI;oBAAEC;gBAAK;aAAG;;IAE/D;AACF;AAEA,OAAO,MAAMoD,mBAAmBpE,sBAAqB;AAErD,6FAA6F,GAC7F,OAAO,SAASqE;IACd7O,kBAAkB,QAAQ4O,kBAAkB;QAAEtO,WAAWD;IAA4B;IACrFhC,uBAAuB,QAAQ8L;AACjC"}
|
|
@@ -19,6 +19,7 @@ import { aiHostPublishContext, aiSiteStatusTable } from "../model/ai-host-publis
|
|
|
19
19
|
import { aiInstructionsWithSiteContext } from "../model/ai-site-context.js";
|
|
20
20
|
import { readAiSiteContext } from "../runtime/site-context.js";
|
|
21
21
|
import { AI_ROUTING_TABLE, aiModelForStep } from "../providers/routing.js";
|
|
22
|
+
import { aiDevEffortFor } from "../runtime/ai-dev-env.js";
|
|
22
23
|
import { AI_ACCEPTABLE_USE_BLOCK, runAiRequest } from "../runtime/ai-runtime.js";
|
|
23
24
|
import { AI_GENERATION_MAX_ATTEMPTS } from "../runtime/ai-generation-bounds.js";
|
|
24
25
|
import { checkAiInsightAnswer } from "../runtime/ai-insight-check.js";
|
|
@@ -147,7 +148,7 @@ async function readHost(firestore, orgId, hostId) {
|
|
|
147
148
|
};
|
|
148
149
|
}
|
|
149
150
|
export const runAiJobInsightStep = async (context)=>{
|
|
150
|
-
var _ref, _job_hostId, _context_org, _site_host, _job_expiresAt;
|
|
151
|
+
var _ref, _job_hostId, _context_org, _site_host, _aiDevEffortFor, _job_expiresAt;
|
|
151
152
|
var _job_inputs, _job_inputs1, _site_host1;
|
|
152
153
|
const { job, firestore, signal, now } = context;
|
|
153
154
|
const model = (_ref = context.modelFor == null ? void 0 : context.modelFor.call(context, 'job.insight')) != null ? _ref : aiModelForStep('job.insight');
|
|
@@ -194,6 +195,8 @@ export const runAiJobInsightStep = async (context)=>{
|
|
|
194
195
|
contact: false
|
|
195
196
|
}) : null)
|
|
196
197
|
];
|
|
198
|
+
// A development run's effort A/B arm (AGL-3781), else the row's.
|
|
199
|
+
const effort = (_aiDevEffortFor = aiDevEffortFor('job.insight')) != null ? _aiDevEffortFor : AI_ROUTING_TABLE['job.insight'].effort;
|
|
197
200
|
const ask = (messages, tool, ceiling)=>runAiRequest(_extends({
|
|
198
201
|
model,
|
|
199
202
|
system,
|
|
@@ -204,6 +207,8 @@ export const runAiJobInsightStep = async (context)=>{
|
|
|
204
207
|
maxTokens: Math.max(1, Math.min(ceiling, left()))
|
|
205
208
|
}, AI_ROUTING_TABLE['job.insight'].thinking ? {
|
|
206
209
|
thinking: AI_ROUTING_TABLE['job.insight'].thinking
|
|
210
|
+
} : {}, effort ? {
|
|
211
|
+
effort
|
|
207
212
|
} : {}, {
|
|
208
213
|
stream: false
|
|
209
214
|
}, signal ? {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-insight-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 { ResolvedPluginFigureReader } from '@aglyn/aglyn/plugin-manager/plugin-figures'\nimport {\n AI_INSIGHT_ASK_SURFACES,\n AI_INSIGHT_DIGEST_DAYS,\n AI_INSIGHT_DIGEST_READERS,\n AI_INSIGHT_QUESTION_MAX_CHARS,\n AI_INSIGHTS_COLLECTION,\n aiInsightDays,\n aiInsightOutput,\n aiInsightSurface,\n aiInsightSurfaceNeedsSite,\n type AiInsightRecord,\n type AiInsightSurface,\n type AiInsightTable,\n} from '../model/ai-insight'\nimport { aiHostPublishContext, aiSiteStatusTable } from '../model/ai-host-publish-context'\nimport { aiInstructionsWithSiteContext } from '../model/ai-site-context'\nimport { readAiSiteContext } from '../runtime/site-context'\nimport { AI_ROUTING_TABLE, aiModelForStep } from '../providers/routing'\nimport {\n AI_ACCEPTABLE_USE_BLOCK,\n runAiRequest,\n type AiMessage,\n type AiResult,\n type AiSystemBlock,\n type AiUsage,\n} from '../runtime/ai-runtime'\nimport { AI_GENERATION_MAX_ATTEMPTS } from '../runtime/ai-generation-bounds'\nimport { checkAiInsightAnswer, type AiInsightCheck } from '../runtime/ai-insight-check'\nimport {\n AI_INSIGHT_ANSWER_TOOL_NAME,\n AI_INSIGHT_READ_TOOL_NAME,\n aiInsightAnswerTool,\n aiInsightReadTool,\n parseAiInsightAnswer,\n parseAiInsightReads,\n type AiInsightReadRequest,\n} from '../tools/ai-insight-tool'\nimport { aiDatasetCatalog } from '../insights/ai-dataset-catalog'\nimport {\n aiInsightDatasetCatalog,\n aiInsightReaderCatalog,\n aiInsightReaderDays,\n aiInsightReaders,\n aiInsightTableText,\n readAiInsightTables,\n} from '../insights/ai-insight-readers'\nimport { assistExchangeExpiry } from '../usage/assist-usage'\nimport { registerAiJobAdmission, type AiJobAdmissionContext, type AiJobAdmissionRefusal } from './ai-job-admission'\nimport { AI_INSIGHT_READ_MAX_TOKENS, AI_JOB_INSIGHT_STEP_BUDGET, AI_JOB_INSIGHT_STEP_MINIMUM_MS } from './ai-job-insight-budget'\nimport type { AiJobStepContext, AiJobStepOutcome, AiJobStepRunner } from './ai-job-text-step'\nimport { registerAiJobStep } from './ai-jobs'\n\n/**\n * The `insight` step (AGL-2915): a question about a site's figures, answered\n * from tables code read, with every insight traced to the rows it cites.\n *\n * ## Two exchanges, and the model never writes a query\n *\n * 1. **The reads.** The model is shown the question and the readers this job\n * may read — each an id, what it answers, its windows and parameters — and\n * answers `read_figures` with a choice among them. It names nothing else: no\n * collection, no field path, no filter. A choice that names no reader this\n * job may read is dropped, and a job left with none reads the surface's\n * defaults. A weekly digest makes no read call: code reads the digest's\n * readers.\n * 2. **The answer.** Code reads the tables through the owners' readers, and the\n * model is shown them and answers `submit_insights`. The trace\n * (`runtime/ai-insight-check.ts`) keeps each insight whose every number is\n * in a row it cites and leaves out the rest. An answer that is not a call,\n * or whose every insight was left out, is asked for once more with the\n * reasons; what the second answer keeps is the answer.\n *\n * Both calls share one allowance of output, twice the routing ceiling, which\n * is what the step's least time is planned against.\n *\n * ## What reaches the model\n *\n * The question, the names and descriptions of the readers, the tables, the\n * site's publish state (published or not, its address, its pages' paths), and\n * for a datasets question the names of the datasets the member may see with\n * their fields' names and types. A table holds counts, sums and rates labeled\n * by a page path, a referring site, a form's, a service's, a product's or a\n * campaign's name, or the values of a dataset field with at most sixty\n * different values that at least three records share. No\n * record, visitor, contact, order, booking, submission or recipient is read\n * into a prompt, and every text cell is masked of email addresses and phone\n * numbers on its way into a table.\n *\n * ## What it writes\n *\n * The answer, at `orgs/{orgId}/aiInsights/{jobId}` under the job's own\n * expiry, and an output that names it and says how many insights it holds.\n * Nothing else: no draft, no setting, no message.\n */\n\ntype Firestore = FirebaseFirestore.Firestore\n\nexport const AI_INSIGHT_NO_SURFACE_COPY = 'This question does not say which page it was asked from.'\nexport const AI_INSIGHT_NO_SITE_COPY = 'Open the site whose numbers you are asking about.'\nexport const AI_INSIGHT_NO_READERS_COPY =\n 'There are no figures to answer from here yet: the features this page reports on are not on for this workspace.'\nexport const AI_INSIGHT_NO_TABLES_COPY = 'The figures could not be read. Try again in a moment.'\nexport const AI_INSIGHT_UNREADABLE_COPY = 'The AI did not answer with insights. Try asking in different words.'\n\n/** The digest's question: what a week's insights cover, in the order a person reads them. */\nexport const AI_INSIGHT_DIGEST_QUESTION =\n 'What changed on this site this week? Write the week’s insights: the change in traffic, the most viewed page, the form that turned the most views into submissions, a campaign that did better than the rest, and any day that stood out — only those the tables support.'\n\n/** The rules both calls are sent: static, byte-identical for every workspace. */\nexport const AI_INSIGHT_RULES = [\n 'You answer a workspace owner’s questions about their own figures: page views, orders, bookings, forms, campaigns, A/B tests and datasets. You never see a record, and you never write a query: code reads figures through fixed readers and gives you their tables.',\n '',\n `When you are given a question and a list of readers, call ${AI_INSIGHT_READ_TOOL_NAME} once with the readers whose tables answer it, most useful first, and the window each should cover. Choose only readers from the list, with their parameters' values taken from what the list and the question give you.`,\n '',\n `When you are given tables, call ${AI_INSIGHT_ANSWER_TOOL_NAME} once. Each table has a handle (t1, t2, …) and numbered rows. Rules for the insights:`,\n '- Say what matters most to the question first. Leave out what the tables do not support.',\n '- Every insight cites the table and the rows its figures come from.',\n '- Every number you write appears in a row you cite, as the table shows it or rounded. Never add, subtract, average or otherwise compute a figure yourself. The only other numbers you may write are the length of a table’s window, a date in it, and how many rows you cite.',\n '- Say a figure rose or fell only when a change you quote shows it.',\n '- Name nobody, and write no email address or phone number.',\n '- Plain sentences: no markdown, no headings, no lists.',\n '- When the tables cannot answer part of the question, say so in gap in one sentence with no numbers; otherwise gap is null.',\n '- Whether the site is published, and its address, come only from the Site status table. Page views and visitors never show whether a site is live: never say or suggest a site is not live, not launched or not published because of its traffic.',\n '- Write in the language of the question.',\n].join('\\n')\n\n/** The system blocks both calls send: the rules and the platform's acceptable-use rules. */\nexport const AI_JOB_INSIGHT_SYSTEM: AiSystemBlock[] = [\n { text: `${AI_INSIGHT_RULES}\\n\\n${AI_ACCEPTABLE_USE_BLOCK}`, cacheBreakpoint: true },\n]\n\nconst ZERO_USAGE: AiUsage = { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 }\n\nconst str = (value: unknown): string => (typeof value === 'string' ? value.trim() : '')\n\n/** What the step's calls have spent so far, in the outcome's shape. */\nclass InsightSpend {\n usage: AiUsage = { ...ZERO_USAGE }\n estCostUsd = 0\n stopReason: string | null = null\n\n constructor(readonly model: string) {}\n\n add(result: AiResult): void {\n this.usage = {\n inputTokens: this.usage.inputTokens + result.usage.inputTokens,\n outputTokens: this.usage.outputTokens + result.usage.outputTokens,\n cacheReadTokens: this.usage.cacheReadTokens + result.usage.cacheReadTokens,\n cacheWriteTokens: this.usage.cacheWriteTokens + result.usage.cacheWriteTokens,\n }\n this.estCostUsd = Math.round((this.estCostUsd + result.estCostUsd) * 1_000_000) / 1_000_000\n this.stopReason = result.stopReason\n }\n\n outcome(): Pick<AiJobStepOutcome, 'usage' | 'estCostUsd' | 'model' | 'stopReason' | 'effort'> {\n return { usage: this.usage, estCostUsd: this.estCostUsd, model: this.model, stopReason: this.stopReason, effort: null }\n }\n}\n\n/** The read call's user turn: the question, the window asked for, the readers and, for datasets, the datasets. */\nexport function aiInsightReadPrompt(input: {\n question: string\n days: number\n readers: readonly ResolvedPluginFigureReader[]\n datasets: string | null\n}): string {\n return [\n `Question: ${input.question}`,\n `The window picked on the page: ${input.days} days. Use it unless the question names another window a reader lists.`,\n 'Readers you may read here:',\n aiInsightReaderCatalog(input.readers),\n ...(input.datasets === null ? [] : ['', 'Datasets you may read, with their fields:', input.datasets]),\n ].join('\\n')\n}\n\n/** The answer call's user turn: the question and every table. */\nexport function aiInsightAnswerPrompt(question: string, tables: readonly AiInsightTable[]): string {\n return [`Question: ${question}`, '', 'Tables:', '', tables.map(aiInsightTableText).join('\\n\\n')].join('\\n')\n}\n\n/** What a re-ask tells the model: why nothing it wrote could be kept. */\nexport function aiInsightReaskMessage(check: AiInsightCheck | null): string {\n const reasons = check?.findings.length\n ? `None of those insights could be kept: ${[...new Set(check.findings)].slice(0, 8).join('; ')}.`\n : `That answer did not come through ${AI_INSIGHT_ANSWER_TOOL_NAME}.`\n return `${reasons} Answer again with ${AI_INSIGHT_ANSWER_TOOL_NAME}: cite the rows each insight's figures are in, and write every number as a row you cite shows it.`\n}\n\n/** The reads a surface falls back to when the model named none it may read: the digest's own readers. */\nfunction defaultReads(readers: readonly ResolvedPluginFigureReader[], days: number): AiInsightReadRequest[] {\n const ids = readers.map(({ reader }) => reader.id)\n const preferred = AI_INSIGHT_DIGEST_READERS.filter((id) => ids.includes(id))\n const chosen = preferred.length ? preferred : ids\n return chosen.map((id) => ({\n reader: id,\n days: aiInsightReaderDays(readers.find(({ reader }) => reader.id === id)?.reader.windows ?? [], days),\n params: {},\n }))\n}\n\nasync function readHost(\n firestore: Firestore,\n orgId: string,\n hostId: string | null,\n): Promise<{ host: Record<string, unknown> | null; ok: boolean }> {\n if (!hostId) return { host: null, ok: true }\n const snapshot = await firestore.collection('hosts').doc(hostId).get()\n const host = snapshot.exists ? ((snapshot.data() ?? {}) as Record<string, unknown>) : null\n return { host, ok: Boolean(host) && host?.['orgId'] === orgId }\n}\n\nexport const runAiJobInsightStep: AiJobStepRunner = async (context: AiJobStepContext) => {\n const { job, firestore, signal, now } = context\n const model = context.modelFor?.('job.insight') ?? aiModelForStep('job.insight')\n const spend = new InsightSpend(model)\n const stop = (failure: string): AiJobStepOutcome => ({ outputs: [], ...spend.outcome(), failure })\n\n const surface = aiInsightSurface(job.inputs)\n if (!surface) return stop(AI_INSIGHT_NO_SURFACE_COPY)\n const hostId = job.hostId ?? null\n if (aiInsightSurfaceNeedsSite(surface) && !hostId) return stop(AI_INSIGHT_NO_SITE_COPY)\n // The door checks the caller belongs to the org; that the site does is\n // checked here, before anything about it is read.\n const site = await readHost(firestore, job.orgId, hostId)\n if (!site.ok) return stop(AI_INSIGHT_NO_SITE_COPY)\n const readers = await aiInsightReaders({\n orgId: job.orgId,\n hostId,\n org: context.org ?? null,\n host: site.host,\n surface,\n })\n if (!readers.length) return stop(AI_INSIGHT_NO_READERS_COPY)\n\n const digest = surface === 'digest'\n const days = digest ? AI_INSIGHT_DIGEST_DAYS : aiInsightDays(job.inputs?.['days'])\n const question = digest ? AI_INSIGHT_DIGEST_QUESTION : job.brief.slice(0, AI_INSIGHT_QUESTION_MAX_CHARS)\n const maxTokens = AI_JOB_INSIGHT_STEP_BUDGET.maxTokens(model)\n const allowance = AI_GENERATION_MAX_ATTEMPTS * maxTokens\n const left = () => allowance - spend.usage.outputTokens\n // What the business is and what the owner's edits showed (AGL-3661), so an\n // answer reads the numbers for THIS business. No contact details: an\n // insight names nobody. No publish state either: that is the Site status\n // table's to say, and the rules send the model there.\n const siteContext = await readAiSiteContext(firestore, { orgId: job.orgId, hostId, host: site.host ?? null })\n const system = [\n ...aiInstructionsWithSiteContext(\n AI_JOB_INSIGHT_SYSTEM,\n siteContext ? { ...siteContext, publish: null, contact: false } : null,\n ),\n ]\n const ask = (messages: AiMessage[], tool: 'read' | 'answer', ceiling: number) =>\n runAiRequest({\n model,\n system,\n messages,\n tools: [tool === 'read' ? aiInsightReadTool() : aiInsightAnswerTool()],\n maxTokens: Math.max(1, Math.min(ceiling, left())),\n ...(AI_ROUTING_TABLE['job.insight'].thinking ? { thinking: AI_ROUTING_TABLE['job.insight'].thinking } : {}),\n stream: false,\n ...(signal ? { signal } : {}),\n })\n\n // 1. The reads.\n const base = { orgId: job.orgId, hostId, uid: digest ? null : job.createdBy, now }\n let requests: AiInsightReadRequest[]\n if (digest) {\n requests = defaultReads(readers, days)\n } else {\n const datasets =\n surface === 'datasets'\n ? aiInsightDatasetCatalog(await aiDatasetCatalog(firestore, { orgId: job.orgId, hostId, uid: job.createdBy }))\n : null\n const result = await ask(\n [{ role: 'user', content: aiInsightReadPrompt({ question, days, readers, datasets }) }],\n 'read',\n AI_INSIGHT_READ_MAX_TOKENS,\n )\n spend.add(result)\n if (result.kind === 'refusal') return { outputs: [], ...spend.outcome(), refused: true }\n requests = parseAiInsightReads(result.toolUse.find((use) => use.name === AI_INSIGHT_READ_TOOL_NAME)?.input)\n }\n const maxTables = digest ? AI_INSIGHT_DIGEST_READERS.length : undefined\n let read = await readAiInsightTables(readers, requests, base, maxTables)\n if (!read.tables.length && !digest) read = await readAiInsightTables(readers, defaultReads(readers, days), base)\n if (read.refusals.length) console.warn('ai insight reads refused', { jobId: job.$id, refusals: read.refusals })\n if (!read.tables.length) return stop(AI_INSIGHT_NO_TABLES_COPY)\n // A site's question is told whether the site is live, so traffic is never\n // read as launch status. Appended, so the readers' refs stay as they were.\n const tables =\n !digest && site.host\n ? [...read.tables, aiSiteStatusTable(aiHostPublishContext(site.host), `t${read.tables.length + 1}`)]\n : read.tables\n\n // 2. The answer, and at most one re-ask.\n let messages: AiMessage[] = [{ role: 'user', content: aiInsightAnswerPrompt(question, tables) }]\n let checked: AiInsightCheck | null = null\n for (let attempt = 0; attempt < AI_GENERATION_MAX_ATTEMPTS && left() > 0; attempt += 1) {\n const result = await ask(messages, 'answer', maxTokens)\n spend.add(result)\n if (result.kind === 'refusal') return { outputs: [], ...spend.outcome(), refused: true }\n const answer = parseAiInsightAnswer(result.toolUse.find((use) => use.name === AI_INSIGHT_ANSWER_TOOL_NAME)?.input)\n const check = answer ? checkAiInsightAnswer(answer, tables) : null\n if (check) checked = check\n if (check && (check.kept.length || !answer?.insights.length)) break\n messages = [\n ...messages,\n { role: 'assistant', content: `Answered with ${AI_INSIGHT_ANSWER_TOOL_NAME}.` },\n { role: 'user', content: aiInsightReaskMessage(check) },\n ]\n }\n if (!checked) return stop(AI_INSIGHT_UNREADABLE_COPY)\n\n const record: AiInsightRecord = {\n jobId: job.$id,\n orgId: job.orgId,\n hostId,\n surface,\n createdBy: job.createdBy,\n question: digest ? str(job.brief) || 'Weekly insights' : question,\n days,\n insights: checked.kept,\n gap: checked.gap,\n tables,\n left: checked.left.length,\n week: digest ? str(job.inputs?.['week']) || null : null,\n createdAtMs: now.getTime(),\n }\n if (checked.left.length) {\n console.info('ai insight left out untraced insights', { jobId: job.$id, findings: checked.findings })\n }\n // Keyed by the job, so a step run again replaces its own answer.\n await firestore\n .collection('orgs')\n .doc(job.orgId)\n .collection(AI_INSIGHTS_COLLECTION)\n .doc(job.$id)\n .set({ ...(JSON.parse(JSON.stringify(record)) as AiInsightRecord), expiresAt: job.expiresAt ?? assistExchangeExpiry(now) })\n return {\n outputs: [aiInsightOutput(job, record, str(site.host?.['subdomain']) || null)],\n ...spend.outcome(),\n }\n}\n\n/**\n * Whether a question may be asked here: a surface a person asks from, a site\n * of the job's own org where the surface needs one, and at least one reader\n * the workspace may read. A digest is made by the weekly sweep and never at a\n * door.\n */\nexport async function aiInsightAdmissionRefusal(\n context: AiJobAdmissionContext,\n): Promise<AiJobAdmissionRefusal | null> {\n const surface: AiInsightSurface | null = aiInsightSurface(context.inputs)\n if (!surface || !AI_INSIGHT_ASK_SURFACES.includes(surface)) {\n return { status: 400, error: 'Ask about your numbers from a site’s Analytics, Data or CRM Reports page.' }\n }\n if (aiInsightSurfaceNeedsSite(surface) && !context.hostId) {\n return { status: 400, error: AI_INSIGHT_NO_SITE_COPY }\n }\n const site = await readHost(context.firestore, context.orgId, context.hostId)\n if (!site.ok) return { status: 404, error: 'Unknown site' }\n const readers = await aiInsightReaders({\n orgId: context.orgId,\n hostId: context.hostId,\n org: context.org,\n host: site.host,\n surface,\n })\n return readers.length ? null : { status: 403, error: AI_INSIGHT_NO_READERS_COPY }\n}\n\n/**\n * The insight kind's registrations, called by the console surface\n * (`registerAiJobKinds` in `server.ts`), never made by importing this module\n * (AGL-3025).\n */\nexport function registerAiInsightJob(): void {\n registerAiJobStep('insight', runAiJobInsightStep, { minimumMs: AI_JOB_INSIGHT_STEP_MINIMUM_MS })\n registerAiJobAdmission('insight', aiInsightAdmissionRefusal)\n}\n"],"names":["AI_INSIGHT_ASK_SURFACES","AI_INSIGHT_DIGEST_DAYS","AI_INSIGHT_DIGEST_READERS","AI_INSIGHT_QUESTION_MAX_CHARS","AI_INSIGHTS_COLLECTION","aiInsightDays","aiInsightOutput","aiInsightSurface","aiInsightSurfaceNeedsSite","aiHostPublishContext","aiSiteStatusTable","aiInstructionsWithSiteContext","readAiSiteContext","AI_ROUTING_TABLE","aiModelForStep","AI_ACCEPTABLE_USE_BLOCK","runAiRequest","AI_GENERATION_MAX_ATTEMPTS","checkAiInsightAnswer","AI_INSIGHT_ANSWER_TOOL_NAME","AI_INSIGHT_READ_TOOL_NAME","aiInsightAnswerTool","aiInsightReadTool","parseAiInsightAnswer","parseAiInsightReads","aiDatasetCatalog","aiInsightDatasetCatalog","aiInsightReaderCatalog","aiInsightReaderDays","aiInsightReaders","aiInsightTableText","readAiInsightTables","assistExchangeExpiry","registerAiJobAdmission","AI_INSIGHT_READ_MAX_TOKENS","AI_JOB_INSIGHT_STEP_BUDGET","AI_JOB_INSIGHT_STEP_MINIMUM_MS","registerAiJobStep","AI_INSIGHT_NO_SURFACE_COPY","AI_INSIGHT_NO_SITE_COPY","AI_INSIGHT_NO_READERS_COPY","AI_INSIGHT_NO_TABLES_COPY","AI_INSIGHT_UNREADABLE_COPY","AI_INSIGHT_DIGEST_QUESTION","AI_INSIGHT_RULES","join","AI_JOB_INSIGHT_SYSTEM","text","cacheBreakpoint","ZERO_USAGE","inputTokens","outputTokens","cacheReadTokens","cacheWriteTokens","str","value","trim","InsightSpend","add","result","usage","estCostUsd","Math","round","stopReason","outcome","model","effort","aiInsightReadPrompt","input","question","days","readers","datasets","aiInsightAnswerPrompt","tables","map","aiInsightReaskMessage","check","reasons","findings","length","Set","slice","defaultReads","ids","reader","id","preferred","filter","includes","chosen","find","windows","params","readHost","firestore","orgId","hostId","snapshot","host","ok","collection","doc","get","exists","data","Boolean","runAiJobInsightStep","context","job","site","signal","now","modelFor","spend","stop","failure","outputs","surface","inputs","org","digest","brief","maxTokens","allowance","left","siteContext","system","publish","contact","ask","messages","tool","ceiling","tools","max","min","thinking","stream","base","uid","createdBy","requests","role","content","kind","refused","toolUse","use","name","maxTables","undefined","read","refusals","console","warn","jobId","$id","checked","attempt","answer","kept","insights","record","gap","week","createdAtMs","getTime","info","set","JSON","parse","stringify","expiresAt","aiInsightAdmissionRefusal","status","error","registerAiInsightJob","minimumMs"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-insight-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 { ResolvedPluginFigureReader } from '@aglyn/aglyn/plugin-manager/plugin-figures'\nimport {\n AI_INSIGHT_ASK_SURFACES,\n AI_INSIGHT_DIGEST_DAYS,\n AI_INSIGHT_DIGEST_READERS,\n AI_INSIGHT_QUESTION_MAX_CHARS,\n AI_INSIGHTS_COLLECTION,\n aiInsightDays,\n aiInsightOutput,\n aiInsightSurface,\n aiInsightSurfaceNeedsSite,\n type AiInsightRecord,\n type AiInsightSurface,\n type AiInsightTable,\n} from '../model/ai-insight'\nimport { aiHostPublishContext, aiSiteStatusTable } from '../model/ai-host-publish-context'\nimport { aiInstructionsWithSiteContext } from '../model/ai-site-context'\nimport { readAiSiteContext } from '../runtime/site-context'\nimport { AI_ROUTING_TABLE, aiModelForStep } from '../providers/routing'\nimport { aiDevEffortFor } from '../runtime/ai-dev-env'\nimport {\n AI_ACCEPTABLE_USE_BLOCK,\n runAiRequest,\n type AiMessage,\n type AiResult,\n type AiSystemBlock,\n type AiUsage,\n} from '../runtime/ai-runtime'\nimport { AI_GENERATION_MAX_ATTEMPTS } from '../runtime/ai-generation-bounds'\nimport { checkAiInsightAnswer, type AiInsightCheck } from '../runtime/ai-insight-check'\nimport {\n AI_INSIGHT_ANSWER_TOOL_NAME,\n AI_INSIGHT_READ_TOOL_NAME,\n aiInsightAnswerTool,\n aiInsightReadTool,\n parseAiInsightAnswer,\n parseAiInsightReads,\n type AiInsightReadRequest,\n} from '../tools/ai-insight-tool'\nimport { aiDatasetCatalog } from '../insights/ai-dataset-catalog'\nimport {\n aiInsightDatasetCatalog,\n aiInsightReaderCatalog,\n aiInsightReaderDays,\n aiInsightReaders,\n aiInsightTableText,\n readAiInsightTables,\n} from '../insights/ai-insight-readers'\nimport { assistExchangeExpiry } from '../usage/assist-usage'\nimport { registerAiJobAdmission, type AiJobAdmissionContext, type AiJobAdmissionRefusal } from './ai-job-admission'\nimport { AI_INSIGHT_READ_MAX_TOKENS, AI_JOB_INSIGHT_STEP_BUDGET, AI_JOB_INSIGHT_STEP_MINIMUM_MS } from './ai-job-insight-budget'\nimport type { AiJobStepContext, AiJobStepOutcome, AiJobStepRunner } from './ai-job-text-step'\nimport { registerAiJobStep } from './ai-jobs'\n\n/**\n * The `insight` step (AGL-2915): a question about a site's figures, answered\n * from tables code read, with every insight traced to the rows it cites.\n *\n * ## Two exchanges, and the model never writes a query\n *\n * 1. **The reads.** The model is shown the question and the readers this job\n * may read — each an id, what it answers, its windows and parameters — and\n * answers `read_figures` with a choice among them. It names nothing else: no\n * collection, no field path, no filter. A choice that names no reader this\n * job may read is dropped, and a job left with none reads the surface's\n * defaults. A weekly digest makes no read call: code reads the digest's\n * readers.\n * 2. **The answer.** Code reads the tables through the owners' readers, and the\n * model is shown them and answers `submit_insights`. The trace\n * (`runtime/ai-insight-check.ts`) keeps each insight whose every number is\n * in a row it cites and leaves out the rest. An answer that is not a call,\n * or whose every insight was left out, is asked for once more with the\n * reasons; what the second answer keeps is the answer.\n *\n * Both calls share one allowance of output, twice the routing ceiling, which\n * is what the step's least time is planned against.\n *\n * ## What reaches the model\n *\n * The question, the names and descriptions of the readers, the tables, the\n * site's publish state (published or not, its address, its pages' paths), and\n * for a datasets question the names of the datasets the member may see with\n * their fields' names and types. A table holds counts, sums and rates labeled\n * by a page path, a referring site, a form's, a service's, a product's or a\n * campaign's name, or the values of a dataset field with at most sixty\n * different values that at least three records share. No\n * record, visitor, contact, order, booking, submission or recipient is read\n * into a prompt, and every text cell is masked of email addresses and phone\n * numbers on its way into a table.\n *\n * ## What it writes\n *\n * The answer, at `orgs/{orgId}/aiInsights/{jobId}` under the job's own\n * expiry, and an output that names it and says how many insights it holds.\n * Nothing else: no draft, no setting, no message.\n */\n\ntype Firestore = FirebaseFirestore.Firestore\n\nexport const AI_INSIGHT_NO_SURFACE_COPY = 'This question does not say which page it was asked from.'\nexport const AI_INSIGHT_NO_SITE_COPY = 'Open the site whose numbers you are asking about.'\nexport const AI_INSIGHT_NO_READERS_COPY =\n 'There are no figures to answer from here yet: the features this page reports on are not on for this workspace.'\nexport const AI_INSIGHT_NO_TABLES_COPY = 'The figures could not be read. Try again in a moment.'\nexport const AI_INSIGHT_UNREADABLE_COPY = 'The AI did not answer with insights. Try asking in different words.'\n\n/** The digest's question: what a week's insights cover, in the order a person reads them. */\nexport const AI_INSIGHT_DIGEST_QUESTION =\n 'What changed on this site this week? Write the week’s insights: the change in traffic, the most viewed page, the form that turned the most views into submissions, a campaign that did better than the rest, and any day that stood out — only those the tables support.'\n\n/** The rules both calls are sent: static, byte-identical for every workspace. */\nexport const AI_INSIGHT_RULES = [\n 'You answer a workspace owner’s questions about their own figures: page views, orders, bookings, forms, campaigns, A/B tests and datasets. You never see a record, and you never write a query: code reads figures through fixed readers and gives you their tables.',\n '',\n `When you are given a question and a list of readers, call ${AI_INSIGHT_READ_TOOL_NAME} once with the readers whose tables answer it, most useful first, and the window each should cover. Choose only readers from the list, with their parameters' values taken from what the list and the question give you.`,\n '',\n `When you are given tables, call ${AI_INSIGHT_ANSWER_TOOL_NAME} once. Each table has a handle (t1, t2, …) and numbered rows. Rules for the insights:`,\n '- Say what matters most to the question first. Leave out what the tables do not support.',\n '- Every insight cites the table and the rows its figures come from.',\n '- Every number you write appears in a row you cite, as the table shows it or rounded. Never add, subtract, average or otherwise compute a figure yourself. The only other numbers you may write are the length of a table’s window, a date in it, and how many rows you cite.',\n '- Say a figure rose or fell only when a change you quote shows it.',\n '- Name nobody, and write no email address or phone number.',\n '- Plain sentences: no markdown, no headings, no lists.',\n '- When the tables cannot answer part of the question, say so in gap in one sentence with no numbers; otherwise gap is null.',\n '- Whether the site is published, and its address, come only from the Site status table. Page views and visitors never show whether a site is live: never say or suggest a site is not live, not launched or not published because of its traffic.',\n '- Write in the language of the question.',\n].join('\\n')\n\n/** The system blocks both calls send: the rules and the platform's acceptable-use rules. */\nexport const AI_JOB_INSIGHT_SYSTEM: AiSystemBlock[] = [\n { text: `${AI_INSIGHT_RULES}\\n\\n${AI_ACCEPTABLE_USE_BLOCK}`, cacheBreakpoint: true },\n]\n\nconst ZERO_USAGE: AiUsage = { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 }\n\nconst str = (value: unknown): string => (typeof value === 'string' ? value.trim() : '')\n\n/** What the step's calls have spent so far, in the outcome's shape. */\nclass InsightSpend {\n usage: AiUsage = { ...ZERO_USAGE }\n estCostUsd = 0\n stopReason: string | null = null\n\n constructor(readonly model: string) {}\n\n add(result: AiResult): void {\n this.usage = {\n inputTokens: this.usage.inputTokens + result.usage.inputTokens,\n outputTokens: this.usage.outputTokens + result.usage.outputTokens,\n cacheReadTokens: this.usage.cacheReadTokens + result.usage.cacheReadTokens,\n cacheWriteTokens: this.usage.cacheWriteTokens + result.usage.cacheWriteTokens,\n }\n this.estCostUsd = Math.round((this.estCostUsd + result.estCostUsd) * 1_000_000) / 1_000_000\n this.stopReason = result.stopReason\n }\n\n outcome(): Pick<AiJobStepOutcome, 'usage' | 'estCostUsd' | 'model' | 'stopReason' | 'effort'> {\n return { usage: this.usage, estCostUsd: this.estCostUsd, model: this.model, stopReason: this.stopReason, effort: null }\n }\n}\n\n/** The read call's user turn: the question, the window asked for, the readers and, for datasets, the datasets. */\nexport function aiInsightReadPrompt(input: {\n question: string\n days: number\n readers: readonly ResolvedPluginFigureReader[]\n datasets: string | null\n}): string {\n return [\n `Question: ${input.question}`,\n `The window picked on the page: ${input.days} days. Use it unless the question names another window a reader lists.`,\n 'Readers you may read here:',\n aiInsightReaderCatalog(input.readers),\n ...(input.datasets === null ? [] : ['', 'Datasets you may read, with their fields:', input.datasets]),\n ].join('\\n')\n}\n\n/** The answer call's user turn: the question and every table. */\nexport function aiInsightAnswerPrompt(question: string, tables: readonly AiInsightTable[]): string {\n return [`Question: ${question}`, '', 'Tables:', '', tables.map(aiInsightTableText).join('\\n\\n')].join('\\n')\n}\n\n/** What a re-ask tells the model: why nothing it wrote could be kept. */\nexport function aiInsightReaskMessage(check: AiInsightCheck | null): string {\n const reasons = check?.findings.length\n ? `None of those insights could be kept: ${[...new Set(check.findings)].slice(0, 8).join('; ')}.`\n : `That answer did not come through ${AI_INSIGHT_ANSWER_TOOL_NAME}.`\n return `${reasons} Answer again with ${AI_INSIGHT_ANSWER_TOOL_NAME}: cite the rows each insight's figures are in, and write every number as a row you cite shows it.`\n}\n\n/** The reads a surface falls back to when the model named none it may read: the digest's own readers. */\nfunction defaultReads(readers: readonly ResolvedPluginFigureReader[], days: number): AiInsightReadRequest[] {\n const ids = readers.map(({ reader }) => reader.id)\n const preferred = AI_INSIGHT_DIGEST_READERS.filter((id) => ids.includes(id))\n const chosen = preferred.length ? preferred : ids\n return chosen.map((id) => ({\n reader: id,\n days: aiInsightReaderDays(readers.find(({ reader }) => reader.id === id)?.reader.windows ?? [], days),\n params: {},\n }))\n}\n\nasync function readHost(\n firestore: Firestore,\n orgId: string,\n hostId: string | null,\n): Promise<{ host: Record<string, unknown> | null; ok: boolean }> {\n if (!hostId) return { host: null, ok: true }\n const snapshot = await firestore.collection('hosts').doc(hostId).get()\n const host = snapshot.exists ? ((snapshot.data() ?? {}) as Record<string, unknown>) : null\n return { host, ok: Boolean(host) && host?.['orgId'] === orgId }\n}\n\nexport const runAiJobInsightStep: AiJobStepRunner = async (context: AiJobStepContext) => {\n const { job, firestore, signal, now } = context\n const model = context.modelFor?.('job.insight') ?? aiModelForStep('job.insight')\n const spend = new InsightSpend(model)\n const stop = (failure: string): AiJobStepOutcome => ({ outputs: [], ...spend.outcome(), failure })\n\n const surface = aiInsightSurface(job.inputs)\n if (!surface) return stop(AI_INSIGHT_NO_SURFACE_COPY)\n const hostId = job.hostId ?? null\n if (aiInsightSurfaceNeedsSite(surface) && !hostId) return stop(AI_INSIGHT_NO_SITE_COPY)\n // The door checks the caller belongs to the org; that the site does is\n // checked here, before anything about it is read.\n const site = await readHost(firestore, job.orgId, hostId)\n if (!site.ok) return stop(AI_INSIGHT_NO_SITE_COPY)\n const readers = await aiInsightReaders({\n orgId: job.orgId,\n hostId,\n org: context.org ?? null,\n host: site.host,\n surface,\n })\n if (!readers.length) return stop(AI_INSIGHT_NO_READERS_COPY)\n\n const digest = surface === 'digest'\n const days = digest ? AI_INSIGHT_DIGEST_DAYS : aiInsightDays(job.inputs?.['days'])\n const question = digest ? AI_INSIGHT_DIGEST_QUESTION : job.brief.slice(0, AI_INSIGHT_QUESTION_MAX_CHARS)\n const maxTokens = AI_JOB_INSIGHT_STEP_BUDGET.maxTokens(model)\n const allowance = AI_GENERATION_MAX_ATTEMPTS * maxTokens\n const left = () => allowance - spend.usage.outputTokens\n // What the business is and what the owner's edits showed (AGL-3661), so an\n // answer reads the numbers for THIS business. No contact details: an\n // insight names nobody. No publish state either: that is the Site status\n // table's to say, and the rules send the model there.\n const siteContext = await readAiSiteContext(firestore, { orgId: job.orgId, hostId, host: site.host ?? null })\n const system = [\n ...aiInstructionsWithSiteContext(\n AI_JOB_INSIGHT_SYSTEM,\n siteContext ? { ...siteContext, publish: null, contact: false } : null,\n ),\n ]\n // A development run's effort A/B arm (AGL-3781), else the row's.\n const effort = aiDevEffortFor('job.insight') ?? AI_ROUTING_TABLE['job.insight'].effort\n const ask = (messages: AiMessage[], tool: 'read' | 'answer', ceiling: number) =>\n runAiRequest({\n model,\n system,\n messages,\n tools: [tool === 'read' ? aiInsightReadTool() : aiInsightAnswerTool()],\n maxTokens: Math.max(1, Math.min(ceiling, left())),\n ...(AI_ROUTING_TABLE['job.insight'].thinking ? { thinking: AI_ROUTING_TABLE['job.insight'].thinking } : {}),\n ...(effort ? { effort } : {}),\n stream: false,\n ...(signal ? { signal } : {}),\n })\n\n // 1. The reads.\n const base = { orgId: job.orgId, hostId, uid: digest ? null : job.createdBy, now }\n let requests: AiInsightReadRequest[]\n if (digest) {\n requests = defaultReads(readers, days)\n } else {\n const datasets =\n surface === 'datasets'\n ? aiInsightDatasetCatalog(await aiDatasetCatalog(firestore, { orgId: job.orgId, hostId, uid: job.createdBy }))\n : null\n const result = await ask(\n [{ role: 'user', content: aiInsightReadPrompt({ question, days, readers, datasets }) }],\n 'read',\n AI_INSIGHT_READ_MAX_TOKENS,\n )\n spend.add(result)\n if (result.kind === 'refusal') return { outputs: [], ...spend.outcome(), refused: true }\n requests = parseAiInsightReads(result.toolUse.find((use) => use.name === AI_INSIGHT_READ_TOOL_NAME)?.input)\n }\n const maxTables = digest ? AI_INSIGHT_DIGEST_READERS.length : undefined\n let read = await readAiInsightTables(readers, requests, base, maxTables)\n if (!read.tables.length && !digest) read = await readAiInsightTables(readers, defaultReads(readers, days), base)\n if (read.refusals.length) console.warn('ai insight reads refused', { jobId: job.$id, refusals: read.refusals })\n if (!read.tables.length) return stop(AI_INSIGHT_NO_TABLES_COPY)\n // A site's question is told whether the site is live, so traffic is never\n // read as launch status. Appended, so the readers' refs stay as they were.\n const tables =\n !digest && site.host\n ? [...read.tables, aiSiteStatusTable(aiHostPublishContext(site.host), `t${read.tables.length + 1}`)]\n : read.tables\n\n // 2. The answer, and at most one re-ask.\n let messages: AiMessage[] = [{ role: 'user', content: aiInsightAnswerPrompt(question, tables) }]\n let checked: AiInsightCheck | null = null\n for (let attempt = 0; attempt < AI_GENERATION_MAX_ATTEMPTS && left() > 0; attempt += 1) {\n const result = await ask(messages, 'answer', maxTokens)\n spend.add(result)\n if (result.kind === 'refusal') return { outputs: [], ...spend.outcome(), refused: true }\n const answer = parseAiInsightAnswer(result.toolUse.find((use) => use.name === AI_INSIGHT_ANSWER_TOOL_NAME)?.input)\n const check = answer ? checkAiInsightAnswer(answer, tables) : null\n if (check) checked = check\n if (check && (check.kept.length || !answer?.insights.length)) break\n messages = [\n ...messages,\n { role: 'assistant', content: `Answered with ${AI_INSIGHT_ANSWER_TOOL_NAME}.` },\n { role: 'user', content: aiInsightReaskMessage(check) },\n ]\n }\n if (!checked) return stop(AI_INSIGHT_UNREADABLE_COPY)\n\n const record: AiInsightRecord = {\n jobId: job.$id,\n orgId: job.orgId,\n hostId,\n surface,\n createdBy: job.createdBy,\n question: digest ? str(job.brief) || 'Weekly insights' : question,\n days,\n insights: checked.kept,\n gap: checked.gap,\n tables,\n left: checked.left.length,\n week: digest ? str(job.inputs?.['week']) || null : null,\n createdAtMs: now.getTime(),\n }\n if (checked.left.length) {\n console.info('ai insight left out untraced insights', { jobId: job.$id, findings: checked.findings })\n }\n // Keyed by the job, so a step run again replaces its own answer.\n await firestore\n .collection('orgs')\n .doc(job.orgId)\n .collection(AI_INSIGHTS_COLLECTION)\n .doc(job.$id)\n .set({ ...(JSON.parse(JSON.stringify(record)) as AiInsightRecord), expiresAt: job.expiresAt ?? assistExchangeExpiry(now) })\n return {\n outputs: [aiInsightOutput(job, record, str(site.host?.['subdomain']) || null)],\n ...spend.outcome(),\n }\n}\n\n/**\n * Whether a question may be asked here: a surface a person asks from, a site\n * of the job's own org where the surface needs one, and at least one reader\n * the workspace may read. A digest is made by the weekly sweep and never at a\n * door.\n */\nexport async function aiInsightAdmissionRefusal(\n context: AiJobAdmissionContext,\n): Promise<AiJobAdmissionRefusal | null> {\n const surface: AiInsightSurface | null = aiInsightSurface(context.inputs)\n if (!surface || !AI_INSIGHT_ASK_SURFACES.includes(surface)) {\n return { status: 400, error: 'Ask about your numbers from a site’s Analytics, Data or CRM Reports page.' }\n }\n if (aiInsightSurfaceNeedsSite(surface) && !context.hostId) {\n return { status: 400, error: AI_INSIGHT_NO_SITE_COPY }\n }\n const site = await readHost(context.firestore, context.orgId, context.hostId)\n if (!site.ok) return { status: 404, error: 'Unknown site' }\n const readers = await aiInsightReaders({\n orgId: context.orgId,\n hostId: context.hostId,\n org: context.org,\n host: site.host,\n surface,\n })\n return readers.length ? null : { status: 403, error: AI_INSIGHT_NO_READERS_COPY }\n}\n\n/**\n * The insight kind's registrations, called by the console surface\n * (`registerAiJobKinds` in `server.ts`), never made by importing this module\n * (AGL-3025).\n */\nexport function registerAiInsightJob(): void {\n registerAiJobStep('insight', runAiJobInsightStep, { minimumMs: AI_JOB_INSIGHT_STEP_MINIMUM_MS })\n registerAiJobAdmission('insight', aiInsightAdmissionRefusal)\n}\n"],"names":["AI_INSIGHT_ASK_SURFACES","AI_INSIGHT_DIGEST_DAYS","AI_INSIGHT_DIGEST_READERS","AI_INSIGHT_QUESTION_MAX_CHARS","AI_INSIGHTS_COLLECTION","aiInsightDays","aiInsightOutput","aiInsightSurface","aiInsightSurfaceNeedsSite","aiHostPublishContext","aiSiteStatusTable","aiInstructionsWithSiteContext","readAiSiteContext","AI_ROUTING_TABLE","aiModelForStep","aiDevEffortFor","AI_ACCEPTABLE_USE_BLOCK","runAiRequest","AI_GENERATION_MAX_ATTEMPTS","checkAiInsightAnswer","AI_INSIGHT_ANSWER_TOOL_NAME","AI_INSIGHT_READ_TOOL_NAME","aiInsightAnswerTool","aiInsightReadTool","parseAiInsightAnswer","parseAiInsightReads","aiDatasetCatalog","aiInsightDatasetCatalog","aiInsightReaderCatalog","aiInsightReaderDays","aiInsightReaders","aiInsightTableText","readAiInsightTables","assistExchangeExpiry","registerAiJobAdmission","AI_INSIGHT_READ_MAX_TOKENS","AI_JOB_INSIGHT_STEP_BUDGET","AI_JOB_INSIGHT_STEP_MINIMUM_MS","registerAiJobStep","AI_INSIGHT_NO_SURFACE_COPY","AI_INSIGHT_NO_SITE_COPY","AI_INSIGHT_NO_READERS_COPY","AI_INSIGHT_NO_TABLES_COPY","AI_INSIGHT_UNREADABLE_COPY","AI_INSIGHT_DIGEST_QUESTION","AI_INSIGHT_RULES","join","AI_JOB_INSIGHT_SYSTEM","text","cacheBreakpoint","ZERO_USAGE","inputTokens","outputTokens","cacheReadTokens","cacheWriteTokens","str","value","trim","InsightSpend","add","result","usage","estCostUsd","Math","round","stopReason","outcome","model","effort","aiInsightReadPrompt","input","question","days","readers","datasets","aiInsightAnswerPrompt","tables","map","aiInsightReaskMessage","check","reasons","findings","length","Set","slice","defaultReads","ids","reader","id","preferred","filter","includes","chosen","find","windows","params","readHost","firestore","orgId","hostId","snapshot","host","ok","collection","doc","get","exists","data","Boolean","runAiJobInsightStep","context","job","site","signal","now","modelFor","spend","stop","failure","outputs","surface","inputs","org","digest","brief","maxTokens","allowance","left","siteContext","system","publish","contact","ask","messages","tool","ceiling","tools","max","min","thinking","stream","base","uid","createdBy","requests","role","content","kind","refused","toolUse","use","name","maxTables","undefined","read","refusals","console","warn","jobId","$id","checked","attempt","answer","kept","insights","record","gap","week","createdAtMs","getTime","info","set","JSON","parse","stringify","expiresAt","aiInsightAdmissionRefusal","status","error","registerAiInsightJob","minimumMs"],"mappings":"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"}
|
|
@@ -203,6 +203,25 @@ export declare function aiSiteEmptyGalleryCheck(): (plan: AiBuildPlan) => AiDoct
|
|
|
203
203
|
* next address the page leaves free, and the header links them all the same.
|
|
204
204
|
*/
|
|
205
205
|
export declare function aiSiteBlogStandInCheck(): (plan: AiBuildPlan) => AiDoctrineViolation[];
|
|
206
|
+
/** The code a site plan in another language with no English photo words is re-asked under. */
|
|
207
|
+
export declare const AI_SITE_PHOTO_WORDS_CODE = "plan-photo-words";
|
|
208
|
+
/**
|
|
209
|
+
* A site plan in another language that gave no English photo words
|
|
210
|
+
* (AGL-3779): its stock photos have nothing in English to be searched by, and
|
|
211
|
+
* two Spanish bakeries' heroes, searched by their kind of site, took a coffee
|
|
212
|
+
* mug. Missing covers null, left out, and words the plan's reader dropped as
|
|
213
|
+
* not plain English ("panadería pasteles"). An English plan, and one whose
|
|
214
|
+
* language is unknown, owes none.
|
|
215
|
+
*/
|
|
216
|
+
export declare function aiSitePhotoWordsViolations(plan: Pick<AiBuildPlan, 'language' | 'photoWords'>): AiDoctrineViolation[];
|
|
217
|
+
/**
|
|
218
|
+
* A site plan's missing English photo words, asked about on the FIRST answer
|
|
219
|
+
* only, like a thin home: an answer that still gives none is kept, and its
|
|
220
|
+
* photos fall back to its business type in English, else its kind of site
|
|
221
|
+
* (`aiStockSourceTerms`), since a start stopped over its photo words would be
|
|
222
|
+
* no site at all.
|
|
223
|
+
*/
|
|
224
|
+
export declare function aiSitePhotoWordsCheck(): (plan: AiBuildPlan) => AiDoctrineViolation[];
|
|
206
225
|
/**
|
|
207
226
|
* What a TEMPLATE job's plan is told its creation's `fields` are (AGL-3143
|
|
208
227
|
* §11): the subject's binding tokens, listed so the planner promises only
|