@aglyn/plugins-ai 1.0.0-beta.228 → 1.0.0-beta.229
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-jobs-list-page.component.js +12 -1
- package/src/lib/components/ai-jobs-list-page.component.js.map +1 -1
- package/src/lib/components/ai-site-build-page.component.d.ts +1 -1
- package/src/lib/components/ai-site-build-page.component.js +38 -5
- package/src/lib/components/ai-site-build-page.component.js.map +1 -1
- package/src/lib/components/ai-site-start-card.component.d.ts +10 -1
- package/src/lib/components/ai-site-start-card.component.js +7 -4
- package/src/lib/components/ai-site-start-card.component.js.map +1 -1
- package/src/lib/components/assist-jobs-drawer.component.d.ts +11 -0
- package/src/lib/components/assist-jobs-drawer.component.js +37 -1
- package/src/lib/components/assist-jobs-drawer.component.js.map +1 -1
- package/src/lib/jobs/ai-job-layout-site-pages.d.ts +70 -0
- package/src/lib/jobs/ai-job-layout-site-pages.js +216 -0
- package/src/lib/jobs/ai-job-layout-site-pages.js.map +1 -0
- package/src/lib/jobs/ai-job-layout-step.d.ts +2 -2
- package/src/lib/jobs/ai-job-layout-step.js +19 -1
- package/src/lib/jobs/ai-job-layout-step.js.map +1 -1
- package/src/lib/jobs/ai-job-site-step.js +7 -0
- package/src/lib/jobs/ai-job-site-step.js.map +1 -1
- package/src/lib/jobs/ai-jobs.d.ts +10 -0
- package/src/lib/jobs/ai-jobs.js +121 -51
- package/src/lib/jobs/ai-jobs.js.map +1 -1
- package/src/lib/model/ai-job-failure-copy.d.ts +20 -1
- package/src/lib/model/ai-job-failure-copy.js +28 -1
- package/src/lib/model/ai-job-failure-copy.js.map +1 -1
- package/src/lib/model/ai-job-notice.d.ts +3 -0
- package/src/lib/model/ai-job-notice.js +11 -2
- package/src/lib/model/ai-job-notice.js.map +1 -1
- package/src/lib/model/ai-jobs.types.d.ts +7 -0
- package/src/lib/model/ai-jobs.types.js.map +1 -1
- package/src/lib/model/ai-site-build-progress.d.ts +8 -0
- package/src/lib/model/ai-site-build-progress.js +66 -12
- package/src/lib/model/ai-site-build-progress.js.map +1 -1
- package/src/lib/model/ai-site-start-seo.d.ts +16 -0
- package/src/lib/model/ai-site-start-seo.js +54 -4
- package/src/lib/model/ai-site-start-seo.js.map +1 -1
- package/src/lib/model/ai-site-start.d.ts +14 -0
- package/src/lib/model/ai-site-start.js +43 -0
- package/src/lib/model/ai-site-start.js.map +1 -1
- package/src/lib/runtime/ai-doctrine.d.ts +6 -0
- package/src/lib/runtime/ai-doctrine.js +5 -1
- package/src/lib/runtime/ai-doctrine.js.map +1 -1
- package/src/lib/usage/assist-job-refund.d.ts +3 -2
- package/src/lib/usage/assist-job-refund.js +2 -1
- package/src/lib/usage/assist-job-refund.js.map +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-site-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 { resolveEffectivePlan } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport { resolveOrgIdForHost } from '@aglyn/tenant-data-admin/server/organizations'\nimport {\n isAiPlanNewRef,\n type AiBuildPlanCreate,\n type AiBuildPlanCreateKind,\n type AiBuildPlanScreen,\n} from '../model/ai-build-plan'\nimport type {\n AiJob,\n AiJobKind,\n AiJobOutput,\n AiJobOutputResource,\n AiJobPlan,\n} from '../model/ai-jobs.types'\nimport {\n AI_SITE_EMAIL_TYPE,\n AI_SITE_MAX_SECTIONS,\n AI_SITE_PAGES,\n aiSitePagesRefusal,\n aiSitePlanRefusal,\n aiSiteSubmissions,\n aiSiteWords,\n parseAiSiteJobInputs,\n type AiSiteJobInputs,\n} from '../model/ai-site-job'\nimport {\n AI_SITE_SEO_OUTPUT_ID,\n aiSiteSeoProposalForInputs,\n} from '../model/ai-site-start-seo'\nimport { aiModelForStep } from '../providers/routing'\nimport { registerAiJobAdmission, type AiJobAdmission } from './ai-job-admission'\nimport { aiOriginJobId, aiRecordedJobDraftId } from './ai-job-draft-ids'\nimport { readAiDraftNodes } from './ai-job-drafts'\nimport { aiPageSectionNodeId } from './ai-job-page-sections'\nimport { aiJobPublishesSite, aiPublishGuidedSite } from './ai-site-publish'\nimport { aiConfirmedPlan, aiUnspentOutcome } from './ai-job-generation'\nimport {\n AI_JOB_BRIEF_MAX_CHARS,\n type AiJobStepContext,\n type AiJobStepOutcome,\n type AiJobStepRunner,\n} from './ai-job-text-step'\nimport { AI_JOB_PAGE_STEP_MINIMUM_MS } from './ai-job-page-budget'\nimport {\n aiJobStepRunMinimumMs,\n aiJobStepRunnerFor,\n registerAiJobStep,\n registerAiJobStepPasses,\n} from './ai-jobs'\n\n/**\n * The site scaffold (AGL-2911): the generation step of a `site` job, run once\n * a member confirmed the plan their brief produced.\n *\n * ── It builds nothing itself ─────────────────────────────────────────────\n *\n * A scaffold is a whole site: a palette suggestion, the layout its pages\n * render inside, the contact form they place, four to eight pages with their\n * search listings, and a welcome email. Every one of those is already a job\n * kind with a step of its own, so this step generates nothing and asks no\n * model anything. It works through the confirmed plan one UNIT at a time and\n * hands each unit to the runner registered for the kind that owns it — the\n * theme step for the palette, the page step for a page — under a job of that\n * kind derived from this one.\n *\n * That is why the scaffold cannot drift from the thing it scaffolds: a page\n * it builds is a page job's page, held to the same doctrine, written by the\n * same draft writer, left unpublished by the same rule. A kind whose step\n * this deployment has not loaded is simply not among the units, so nothing\n * here knows which issues have landed.\n *\n * ── One unit a pass ──────────────────────────────────────────────────────\n *\n * The step runs ONE delegated pass and asks the machine to continue, so every\n * pass is one reservation, one provider exchange and one recorded spend, and\n * the org's monthly ceiling binds a scaffold exactly as it binds a chat turn.\n * Credits running out mid-scaffold is therefore the machine's own park: the\n * job waits as `needs_input` with the meter's words, and the beat resumes it\n * where it stopped once the workspace's standing has changed. A pass needs the\n * time its unit's own step needs (`aiSiteJobRunMinimumMs`), so the beat starts\n * a palette change only with a palette change's time left.\n *\n * Where it stopped is read from the job's OUTPUTS rather than kept anywhere:\n * each unit reports exactly one output when it completes, so the units\n * already represented there are the ones already built. A pass interrupted\n * between its work and its record repeats a unit that then finds its own\n * draft and reports it, which is what every step's draft id already gives.\n *\n * ── Never published ──────────────────────────────────────────────────────\n *\n * Nothing here publishes, routes or sends: each delegated step writes the\n * unpublished draft it already wrote for a job of its own kind, and the\n * palette and navigation travel as proposals a member applies.\n *\n * ── The page job builds its creations the same way (AGL-3031) ────────────\n *\n * A page job whose plan creates a layout, forms or components builds them\n * before its page through this same machinery: the units a creation implies\n * (`aiCreationUnit`), where the job stands read from its outputs\n * (`aiSitePendingUnits`), one delegated pass a unit (`aiRunJobUnit`), and the\n * `new:<name>` references resolved to what was built (`aiSiteBuiltRefs`,\n * `aiSiteUnitJob`). There is one loop, and it is the machine's.\n */\n\n/** What a scaffold's units are, in the order it builds them; a component is a page job's. */\nexport type AiSiteUnitKind = 'theme' | 'layout' | 'form' | 'component' | 'page' | 'email'\n\nexport interface AiSiteUnit {\n kind: AiSiteUnitKind\n /** The job kind whose registered runner builds it. */\n jobKind: AiJobKind\n /** The resource the unit reports when it completes. */\n resource: AiJobOutputResource\n /** Stable within the job: what the unit's derived job and draft are addressed by. */\n slot: string\n /** The plan screen a page unit builds. */\n screen?: AiBuildPlanScreen\n /** The creation a theme, layout or form unit builds. */\n creation?: AiBuildPlanCreate\n /** What the unit is, for the derived job's brief. */\n label: string\n}\n\n/** The job kind and output resource each unit is built through. */\nconst UNIT_KINDS: Record<\n AiSiteUnitKind,\n { jobKind: AiJobKind; resource: AiJobOutputResource }\n> = {\n theme: { jobKind: 'theme', resource: 'theme' },\n layout: { jobKind: 'layout', resource: 'layout' },\n form: { jobKind: 'form', resource: 'form' },\n component: { jobKind: 'component', resource: 'reusableComponent' },\n page: { jobKind: 'page', resource: 'screen' },\n email: { jobKind: 'email', resource: 'emailScreen' },\n}\n\n/** The unit kind a creation is built as, where a unit builds it. */\nconst CREATION_UNIT_KINDS: Partial<Record<AiBuildPlanCreateKind, AiSiteUnitKind>> = {\n 'theme-change': 'theme',\n layout: 'layout',\n form: 'form',\n component: 'component',\n}\n\n/**\n * The unit one creation is built as, under the slot its derived job and draft\n * are addressed by; `null` for a creation no unit builds.\n */\nexport function aiCreationUnit(creation: AiBuildPlanCreate, slot: string): AiSiteUnit | null {\n const kind = CREATION_UNIT_KINDS[creation.kind]\n if (!kind) return null\n return { kind, ...UNIT_KINDS[kind], slot, creation, label: creation.name }\n}\n\n/** The creation kinds a unit is built from, by unit kind. */\nconst CREATION_UNITS: Array<{\n unit: AiSiteUnitKind\n create: AiBuildPlanCreateKind\n}> = [\n { unit: 'theme', create: 'theme-change' },\n { unit: 'layout', create: 'layout' },\n { unit: 'form', create: 'form' },\n]\n\nexport const AI_SITE_NO_PLAN_COPY =\n 'This site job has no confirmed plan to build.'\n\n/** What a scaffold answers where the step that builds a page is not loaded. */\nexport const AI_SITE_NO_PAGE_STEP_COPY =\n 'Building a whole site is not available yet.'\n\n/** A unit that finished without reporting what it built; the scaffold cannot go on. */\nexport const AI_SITE_UNIT_EMPTY_COPY =\n 'Part of this site could not be built. Try the site brief again.'\n\n/**\n * The most passes a scaffold's step may take: the largest plan it admits —\n * eight pages of eight sections, its three creations and a welcome email —\n * with nothing to spare, so a runner that never finishes is still bounded\n * while a real site is not.\n */\nexport const AI_SITE_MAX_PASSES =\n AI_SITE_PAGES.max * (AI_SITE_MAX_SECTIONS + 1) + CREATION_UNITS.length + 1\n\n/**\n * The units a plan implies, in build order: the palette first, because a\n * member reads it while the pages are still building; then the layout every\n * page renders inside and the form they place, because a page binds both by\n * id and so needs them to exist; then the pages; then the welcome email,\n * which is about the site rather than part of it.\n */\nexport function aiSiteJobUnits(\n plan: Pick<AiJobPlan, 'create' | 'screens'>,\n options: { welcomeEmail?: boolean } = {},\n): AiSiteUnit[] {\n const units: AiSiteUnit[] = []\n for (const { unit, create } of CREATION_UNITS) {\n const creation = plan.create.find((entry) => entry.kind === create)\n if (!creation) continue\n units.push({\n kind: unit,\n ...UNIT_KINDS[unit],\n slot: unit[0],\n creation,\n label: creation.name,\n })\n }\n plan.screens.forEach((screen, index) => {\n units.push({\n kind: 'page',\n ...UNIT_KINDS.page,\n slot: `p${index}`,\n screen,\n label: screen.title,\n })\n })\n if (options.welcomeEmail) {\n units.push({\n kind: 'email',\n ...UNIT_KINDS.email,\n slot: 'e',\n label: 'Welcome email',\n })\n }\n return units\n}\n\n/**\n * The units a scaffold still owes after the outputs recorded so far. Each\n * unit reports one output of its own resource when it completes, so the\n * outputs already recorded are consumed against the units in build order.\n */\nexport function aiSitePendingUnits(\n units: readonly AiSiteUnit[],\n outputs: readonly Pick<AiJobOutput, 'resource'>[],\n): AiSiteUnit[] {\n const done = new Map<AiJobOutputResource, number>()\n for (const output of outputs) {\n done.set(output.resource, (done.get(output.resource) ?? 0) + 1)\n }\n return units.filter((unit) => {\n const left = done.get(unit.resource) ?? 0\n if (left <= 0) return true\n done.set(unit.resource, left - 1)\n return false\n })\n}\n\n/**\n * The site's own search listing as the scaffold reports it (AGL-2918), or\n * nothing when the job already reported one or its inputs describe no site.\n *\n * It costs nothing and asks no model: the values are arithmetic on the\n * answers (`aiSiteSeoProposal`), so they ride out on the first pass that\n * builds anything rather than waiting for a pass of their own. Its resource\n * is `seo`, which no unit reports, so it cannot be mistaken for a unit's\n * output by `aiSitePendingUnits` and cannot move where the scaffold thinks\n * it is.\n */\nexport function aiSiteSeoOutputs(job: AiJob): AiJobOutput[] {\n const outputs = job.outputs ?? []\n if (outputs.some((output) => output.resource === 'seo' && output.id === AI_SITE_SEO_OUTPUT_ID)) {\n return []\n }\n const proposal = aiSiteSeoProposalForInputs(job.inputs)\n if (!proposal) return []\n return [\n {\n resource: 'seo',\n id: AI_SITE_SEO_OUTPUT_ID,\n hostId: job.hostId ?? null,\n label: 'The site’s search title and description',\n proposal: proposal as unknown as Record<string, unknown>,\n },\n ]\n}\n\n/** A record an earlier unit created, as a later one references it. */\nexport interface AiSiteBuiltRef {\n id: string\n label: string\n /** Only what becomes a record a page can name; a palette change is not one. */\n kind: 'layout' | 'form' | 'component'\n}\n\n/**\n * What the scaffold has already created, as the plan's `new:<name>`\n * references resolve to: the id of the record each completed creation wrote.\n * A page unit is told about the layout and the form by id, exactly as a page\n * job is told about records the site already had.\n */\nexport function aiSiteBuiltRefs(\n units: readonly AiSiteUnit[],\n outputs: readonly Pick<AiJobOutput, 'resource' | 'id' | 'label'>[],\n): Map<string, AiSiteBuiltRef> {\n const byResource = new Map<\n AiJobOutputResource,\n Array<Pick<AiJobOutput, 'id' | 'label'>>\n >()\n for (const output of outputs) {\n const rows = byResource.get(output.resource) ?? []\n rows.push(output)\n byResource.set(output.resource, rows)\n }\n const built = new Map<string, AiSiteBuiltRef>()\n for (const unit of units) {\n const rows = byResource.get(unit.resource)\n const row = rows?.shift()\n if (!row || !unit.creation) continue\n // A theme change is a proposal with no record to reference; a layout, a\n // form and a component become ids a page can name.\n if (\n unit.creation.kind !== 'layout' &&\n unit.creation.kind !== 'form' &&\n unit.creation.kind !== 'component'\n ) {\n continue\n }\n built.set(unit.creation.name.toLowerCase(), {\n id: row.id,\n label: row.label || unit.creation.name,\n kind: unit.creation.kind,\n })\n }\n return built\n}\n\ntype BuiltRefs = ReturnType<typeof aiSiteBuiltRefs>\n\n/** The layout the scaffold built, when it built one. */\nfunction aiSiteBuiltLayoutId(built: BuiltRefs): string | null {\n for (const entry of built.values()) if (entry.kind === 'layout') return entry.id\n return null\n}\n\n/** A plan reference as the unit's own job reads it: an id the site now has, or nothing. */\nexport function aiSiteResolvedRef(ref: string | null, built: BuiltRefs): string | null {\n if (!ref) return null\n if (!isAiPlanNewRef(ref)) return ref\n return (\n built.get(\n ref\n .slice(ref.indexOf(':') + 1)\n .trim()\n .toLowerCase(),\n )?.id ?? null\n )\n}\n\n/**\n * The site's own words, as every unit's brief carries them.\n *\n * The audience is here rather than left to the brief's prose (AGL-2918). A\n * guided start writes it into the brief itself, but an agency batch's brief\n * is a member's own sentence and may never mention it, and a theme, a layout\n * or a form asked to serve nobody in particular serves nobody in particular.\n */\nexport function aiSiteBriefLines(\n brief: string,\n inputs: AiSiteJobInputs,\n): string[] {\n const lines = [brief.trim()]\n const site = [\n inputs.businessName ? `name: ${inputs.businessName}` : '',\n `business: ${inputs.businessType}`,\n inputs.audience ? `for: ${inputs.audience}` : '',\n inputs.city ? `city: ${inputs.city}` : '',\n inputs.brand ? `brand: ${inputs.brand}` : '',\n ].filter(Boolean)\n lines.push(`Site — ${site.join('; ')}.`)\n return lines\n}\n\n/**\n * What the welcome email is told (AGL-2918), beyond that it is a welcome\n * email.\n *\n * The scaffold's email unit used to carry one sentence, and a yes/no toggle\n * decided whether it ran — so the draft was a welcome email for a business\n * in general, to nobody in particular, about nothing that had happened. The\n * two answers it is missing are who wrote in and what now becomes of what\n * they wrote, and the second of those is the person's own routing answer, so\n * the email and the form it acknowledges cannot say different things.\n *\n * ⛔ A DRAFT either way. Nothing here sends, schedules or enrolls anybody:\n * the email step writes an unpublished email design and stops.\n */\nexport function aiSiteEmailBriefLines(\n inputs: Readonly<Record<string, unknown>> | null | undefined,\n): string[] {\n const lines = [\n 'Write the welcome email this site sends someone who gets in touch.',\n ]\n const { audience } = aiSiteWords(inputs)\n if (audience) lines.push(`Write it to ${audience}.`)\n const submissions = aiSiteSubmissions(inputs)\n if (submissions === 'lead') {\n lines.push('Their message is a sales lead, so say that somebody will be in touch about it.')\n } else if (submissions === 'inbox') {\n lines.push('Their message has been read, so say it arrived and that a reply is coming.')\n }\n return lines\n}\n\n/**\n * The id a unit's job is named by, and so the id its draft is written under\n * (AGL-3079): the id the plan entry it builds recorded, or for the welcome\n * email the id recorded on this job's step. A unit that writes no draft — a\n * palette change — and a unit whose plan recorded no id are named by this\n * job's id and the unit's slot, the name every draft of such a plan has.\n */\nexport function aiSiteUnitJobId(job: Pick<AiJob, '$id' | 'steps'>, unit: AiSiteUnit): string {\n const recorded =\n unit.creation?.id ?? unit.screen?.id ?? (unit.kind === 'email' ? aiRecordedJobDraftId(job, 'email') : null)\n return recorded || `${job.$id}-${unit.slot}`\n}\n\n/**\n * The job a unit is built under: this job's org, site, creator and model,\n * the kind that owns the unit, and a plan narrowed to the unit alone.\n *\n * The derived job carries NO steps and NO outputs of the scaffold's: a page\n * job's step reads its own draft and its own plan, and what the other units\n * produced is not its business. Its `$id` is the id the unit's draft is\n * written under (`aiSiteUnitJobId`), which is what a step with no id of its\n * own recorded names its draft by, so a unit re-run after its write finds its\n * own draft rather than writing a second.\n *\n * A creation unit reuses what the plan reuses less what the plan's screens\n * place themselves (AGL-3031): a component a page's section places is the\n * page's to place, and a layout or a card told to place it would be refused\n * for leaving it out.\n */\nexport function aiSiteUnitJob(\n job: AiJob,\n unit: AiSiteUnit,\n built: BuiltRefs,\n): AiJob {\n const plan = job.plan as AiJobPlan\n const labels = { ...(plan.labels ?? {}) }\n for (const entry of built.values()) labels[entry.id] = entry.label\n const inputs = parseAiSiteJobInputs(job.inputs)\n const brief =\n typeof inputs === 'string'\n ? [job.brief]\n : aiSiteBriefLines(job.brief, inputs)\n const unitPlan: AiJobPlan = {\n ...plan,\n labels,\n reuse: [...plan.reuse],\n create: [],\n screens: [],\n }\n if (unit.creation) {\n const placedByScreens = new Set(\n plan.screens.flatMap((screen) => screen.sections.flatMap((section) => section.uses)),\n )\n unitPlan.reuse = plan.reuse.filter((entry) => !placedByScreens.has(entry.id))\n unitPlan.create = [\n {\n ...unit.creation,\n duplicateOf: aiSiteResolvedRef(unit.creation.duplicateOf, built),\n },\n ]\n brief.push(\n `Build the ${unit.creation.kind} “${unit.creation.name}”: ${unit.creation.why}`,\n )\n }\n if (unit.screen) {\n const screen = unit.screen\n // Everything the scaffold has already built is a record the site has, so\n // the page is planned against it the way a page job is planned against\n // what the site always had.\n for (const entry of built.values()) {\n unitPlan.reuse.push({\n kind: entry.kind,\n id: entry.id,\n purpose: `the ${entry.kind} this site’s pages are built on`,\n })\n }\n unitPlan.screens = [\n {\n ...screen,\n // A page the plan named no layout for renders inside the one the\n // scaffold built (AGL-3596); the page step falls back to the site's.\n layout: aiSiteResolvedRef(screen.layout, built) ?? aiSiteBuiltLayoutId(built),\n template: aiSiteResolvedRef(screen.template, built),\n // A scaffold GENERATES every page from its plan (AGL-3596). A page\n // copied from one the site has is not built: a plan that named the\n // starter home page as its home's start produced the starter again,\n // byte for byte, reported as written. A page job keeps rule 15.\n duplicateOf: job.kind === 'site' ? null : aiSiteResolvedRef(screen.duplicateOf, built),\n // A dataset the plan creates and the site has not built yet keeps its\n // name, which the page's draft still binds by (AGL-3475).\n record: screen.record\n ? {\n ...screen.record,\n dataset: aiSiteResolvedRef(screen.record.dataset, built) ?? screen.record.dataset,\n }\n : null,\n sections: screen.sections.map((section) => ({\n ...section,\n uses: section.uses\n .map((ref) => aiSiteResolvedRef(ref, built))\n .filter((ref): ref is string => !!ref),\n })),\n },\n ]\n brief.push(\n `Build the page “${screen.title}” of this site, at ${screen.slug}.`,\n )\n }\n if (unit.kind === 'email') {\n brief.push(...aiSiteEmailBriefLines(job.inputs))\n }\n // The job the member started travels with every unit, so what the unit\n // writes names it (AGL-3596).\n const unitInputs = { ...job.inputs, originJobId: aiOriginJobId(job) }\n return {\n ...job,\n $id: aiSiteUnitJobId(job, unit),\n kind: unit.jobKind,\n steps: [],\n outputs: [],\n plan: unitPlan,\n inputs:\n // A theme change on a site with no theme of its own is a new palette.\n unit.kind === 'theme'\n ? { ...unitInputs, mode: 'create' }\n : // The email step reads the kind off the inputs it is handed, so a\n // job the scaffold composed says which kind it is exactly as a\n // member's own does.\n unit.kind === 'email'\n ? { ...unitInputs, emailType: AI_SITE_EMAIL_TYPE }\n : unitInputs,\n brief: brief.join('\\n').slice(0, AI_JOB_BRIEF_MAX_CHARS),\n }\n}\n\n/**\n * A scaffold is admitted with inputs that read, for a site of the job's own\n * org, and only where this deployment has loaded the step that builds a page:\n * a scaffold whose pages nothing can build is not a scaffold.\n */\nexport const aiSiteJobAdmission: AiJobAdmission = async (context) => {\n const inputs = parseAiSiteJobInputs(context.inputs)\n if (typeof inputs === 'string') return { status: 400, error: inputs }\n // The workspace's own page band (AGL-3594): one or two pages on the Free\n // taste, four to eight on a paid plan.\n const freeTaste = aiSiteFreeTaste(context.org)\n const pages = aiSitePagesRefusal(inputs.pages, freeTaste)\n if (pages) return { status: 400, error: pages }\n if (context.plan) {\n const shape = aiSitePlanRefusal(context.plan, { freeTaste })\n if (shape) return { status: 400, error: shape }\n }\n if (!context.hostId) {\n return {\n status: 400,\n error: 'Open the site the scaffold is for before starting the job',\n }\n }\n const owner = await resolveOrgIdForHost(context.hostId)\n if (!owner || owner !== context.orgId)\n return { status: 404, error: 'Unknown site' }\n if (!aiJobStepRunnerFor('page')) {\n return { status: 400, error: AI_SITE_NO_PAGE_STEP_COPY }\n }\n return null\n}\n\n/** Whether a scaffold's workspace spends the Free taste (AGL-3594); a missing org reads as paid, as the band has always been. */\nexport function aiSiteFreeTaste(org: object | null | undefined): boolean {\n return Boolean(org) && resolveEffectivePlan(org as never) === 'free'\n}\n\n/**\n * Whether a scaffold drafts its welcome email (AGL-3594): where it was asked\n * for, and never on the Free taste, whose credits its pages need.\n */\nexport function aiSiteWelcomeEmail(inputs: AiSiteJobInputs, freeTaste: boolean): boolean {\n return inputs.welcomeEmail && !freeTaste\n}\n\n/** What one delegated pass came to: the unit's spend as the job records it, and whether the unit is built. */\nexport interface AiJobUnitPass {\n outcome: AiJobStepOutcome\n /** The unit reported what it built and asked for no pass of its own. */\n built: boolean\n}\n\n/**\n * One delegated pass (AGL-2911, AGL-3031): the unit handed to the runner of\n * the kind that owns it, under the job derived for it, and its outcome as the\n * delegating job records it. A delegate proposes a plan for its own job,\n * which does not exist, so no plan is carried back. A unit that stopped for a\n * person, refused or failed is the delegating job's stop; a unit that asked\n * for another pass keeps its turn; and a unit that finished without reporting\n * what it built fails with `emptyCopy`, since another pass would ask the same\n * thing again. The tokens it spent are on the bill either way.\n */\nexport async function aiRunJobUnit(\n context: AiJobStepContext,\n input: {\n unit: AiSiteUnit\n units: readonly AiSiteUnit[]\n runner: AiJobStepRunner\n emptyCopy: string\n },\n): Promise<AiJobUnitPass> {\n const { job } = context\n const outputs = job.outputs ?? []\n const outcome = await input.runner({\n ...context,\n job: aiSiteUnitJob(job, input.unit, aiSiteBuiltRefs(input.units, outputs)),\n })\n const spent: AiJobStepOutcome = {\n outputs: outcome.outputs,\n usage: outcome.usage,\n estCostUsd: outcome.estCostUsd,\n model: outcome.model,\n stopReason: outcome.stopReason,\n ...(outcome.effort ? { effort: outcome.effort } : {}),\n ...(outcome.refused ? { refused: true } : {}),\n ...(outcome.failure ? { failure: outcome.failure } : {}),\n ...(outcome.review ? { review: outcome.review } : {}),\n }\n if (spent.review || spent.refused || spent.failure) return { outcome: spent, built: false }\n if (outcome.continue) return { outcome: { ...spent, continue: true }, built: false }\n if (!outcome.outputs.length) return { outcome: { ...spent, failure: input.emptyCopy }, built: false }\n return { outcome: spent, built: true }\n}\n\nexport interface AiJobSiteStepDeps {\n /** The runner registry; specs hand in a fake for a kind they drive. */\n runnerFor?: typeof aiJobStepRunnerFor\n /** The draft reader a built page is checked through; specs hand in a fake. */\n readNodes?: typeof readAiDraftNodes\n /** The guided start's publish; specs hand in a fake. */\n publish?: typeof aiPublishGuidedSite\n}\n\n/** A page the scaffold reported built that holds none of its plan's sections. */\nexport const AI_SITE_PAGE_NOT_WRITTEN_COPY =\n 'A page of this site was not written from its plan. Try the site brief again.'\n\n/**\n * Whether a page unit's draft holds every section its plan named, under the\n * ids the page step writes them by (AGL-3596). A page counts as written only\n * then: a run that reported a page and wrote none of its sections — a copy of\n * a page the site already had, byte for byte — is not a page the job built,\n * and is never reported done.\n */\nexport async function aiSitePageWritten(\n firestore: FirebaseFirestore.Firestore,\n input: { hostId: string; unitJobId: string; screen: AiBuildPlanScreen; draftId: string },\n readNodes: typeof readAiDraftNodes = readAiDraftNodes,\n): Promise<boolean> {\n if (!input.screen.sections.length) return false\n const stored = await readNodes(firestore, { kind: 'screen', hostId: input.hostId, id: input.draftId })\n if (!stored) return false\n return input.screen.sections.every((_, index) => aiPageSectionNodeId(input.unitJobId, index) in stored.nodes)\n}\n\nexport function createAiJobSiteStep(\n deps: AiJobSiteStepDeps = {},\n): AiJobStepRunner {\n const runnerFor = deps.runnerFor ?? aiJobStepRunnerFor\n const readNodes = deps.readNodes ?? readAiDraftNodes\n const publish = deps.publish ?? aiPublishGuidedSite\n /** A guided start's last pass puts what it built on the site, once (AGL-3596). */\n const published = async (\n context: AiJobStepContext,\n outcome: AiJobStepOutcome,\n ): Promise<AiJobStepOutcome> => {\n const { job } = context\n if (!aiJobPublishesSite(job) || job.sitePublish || !job.hostId) return outcome\n const sitePublish = await publish(context.firestore, {\n job,\n outputs: [...(job.outputs ?? []), ...outcome.outputs],\n now: context.now,\n }).catch((error: unknown) => {\n // The site is built either way; the pages stay drafts and say so.\n console.error('ai site publish threw', { orgId: job.orgId, jobId: job.$id, error })\n return null\n })\n return sitePublish ? { ...outcome, sitePublish } : outcome\n }\n return async (context): Promise<AiJobStepOutcome> => {\n const { job } = context\n // The scaffold asks no model of its own. What it names where it spends\n // nothing is the model its plan ran on, which is the one exchange this\n // job has already had of its own.\n const model = context.modelFor?.('job.plan') ?? aiModelForStep('job.plan')\n const plan = aiConfirmedPlan(job)\n if (!plan)\n return { ...aiUnspentOutcome(model), failure: AI_SITE_NO_PLAN_COPY }\n // The resume door refuses such a plan at confirmation once it can read\n // one; a plan confirmed before a site changed still stops here, unspent.\n const freeTaste = aiSiteFreeTaste(context.org)\n const refusal = aiSitePlanRefusal(plan, { freeTaste })\n if (refusal) return { ...aiUnspentOutcome(model), failure: refusal }\n const inputs = parseAiSiteJobInputs(job.inputs)\n if (typeof inputs === 'string')\n return { ...aiUnspentOutcome(model), failure: inputs }\n\n // A kind this deployment has not loaded is not among the units at all, so\n // a scaffold owes only what something can build.\n const units = aiSiteJobUnits(plan, {\n welcomeEmail: aiSiteWelcomeEmail(inputs, freeTaste),\n }).filter((unit) => runnerFor(unit.jobKind))\n const outputs = job.outputs ?? []\n const pending = aiSitePendingUnits(units, outputs)\n if (!pending.length) return published(context, aiUnspentOutcome(model))\n const unit = pending[0]\n const runner = runnerFor(unit.jobKind)\n if (!runner) return aiUnspentOutcome(model)\n\n const pass = await aiRunJobUnit(context, {\n unit,\n units,\n runner,\n emptyCopy: AI_SITE_UNIT_EMPTY_COPY,\n })\n // The site's own listing rides out beside the first unit's output, once\n // (AGL-2918): it is derived from the answers rather than generated, so it\n // is ready before anything is built and costs the pass nothing.\n const outcome: AiJobStepOutcome = {\n ...pass.outcome,\n outputs: [...aiSiteSeoOutputs(job), ...pass.outcome.outputs],\n }\n // A page counts as built only when its plan's sections are in it\n // (AGL-3596): otherwise the pass fails, which is our failure and gives\n // the job's credits back, and the page it reported is not reported.\n if (pass.built && unit.kind === 'page' && unit.screen && job.hostId) {\n const page = pass.outcome.outputs.find((output) => output.resource === 'screen')\n const written =\n page !== undefined &&\n (await aiSitePageWritten(\n context.firestore,\n { hostId: job.hostId, unitJobId: aiSiteUnitJobId(job, unit), screen: unit.screen, draftId: page.id },\n readNodes,\n ))\n if (!written) {\n return {\n ...outcome,\n outputs: outcome.outputs.filter((output) => output.resource !== 'screen'),\n failure: AI_SITE_PAGE_NOT_WRITTEN_COPY,\n }\n }\n }\n // A built unit continues the scaffold while units remain after it.\n if (pass.built && pending.length > 1) return { ...outcome, continue: true }\n return pass.built ? published(context, outcome) : outcome\n }\n}\n\nexport const runAiJobSiteStep = createAiJobSiteStep()\n\n/**\n * The least time a scaffold's next pass needs (AGL-3035). The scaffold asks no\n * model itself: each pass hands one unit to the step registered for its kind,\n * under the job derived for it, so it needs what that step registers for that\n * job — a palette change's or a layout's time for those units, a page pass's\n * for a page. A scaffold with nothing left to build, or with no plan it can\n * build, spends nothing, and a page pass's time covers it.\n */\nexport function aiSiteJobRunMinimumMs(job: AiJob): number {\n const plan = aiConfirmedPlan(job)\n const inputs = parseAiSiteJobInputs(job.inputs)\n if (!plan || typeof inputs === 'string') return AI_JOB_PAGE_STEP_MINIMUM_MS\n const units = aiSiteJobUnits(plan, { welcomeEmail: inputs.welcomeEmail }).filter((unit) =>\n aiJobStepRunnerFor(unit.jobKind),\n )\n const outputs = job.outputs ?? []\n const [unit] = aiSitePendingUnits(units, outputs)\n if (!unit) return AI_JOB_PAGE_STEP_MINIMUM_MS\n return aiJobStepRunMinimumMs(aiSiteUnitJob(job, unit, aiSiteBuiltRefs(units, outputs)))\n}\n\n/**\n * Registers the site scaffold with the least time a pass needs — every\n * scaffold builds pages, and a page pass needs the least of any unit — and the\n * time its next pass needs, the passes a whole site's plan may take, and the\n * check a site job passes before it is created or resumed.\n */\nexport function registerAiSiteJob(): void {\n registerAiJobStep('site', runAiJobSiteStep, {\n minimumMs: AI_JOB_PAGE_STEP_MINIMUM_MS,\n minimumMsFor: aiSiteJobRunMinimumMs,\n })\n registerAiJobStepPasses('site', AI_SITE_MAX_PASSES)\n registerAiJobAdmission('site', aiSiteJobAdmission)\n}\n"],"names":["resolveEffectivePlan","resolveOrgIdForHost","isAiPlanNewRef","AI_SITE_EMAIL_TYPE","AI_SITE_MAX_SECTIONS","AI_SITE_PAGES","aiSitePagesRefusal","aiSitePlanRefusal","aiSiteSubmissions","aiSiteWords","parseAiSiteJobInputs","AI_SITE_SEO_OUTPUT_ID","aiSiteSeoProposalForInputs","aiModelForStep","registerAiJobAdmission","aiOriginJobId","aiRecordedJobDraftId","readAiDraftNodes","aiPageSectionNodeId","aiJobPublishesSite","aiPublishGuidedSite","aiConfirmedPlan","aiUnspentOutcome","AI_JOB_BRIEF_MAX_CHARS","AI_JOB_PAGE_STEP_MINIMUM_MS","aiJobStepRunMinimumMs","aiJobStepRunnerFor","registerAiJobStep","registerAiJobStepPasses","UNIT_KINDS","theme","jobKind","resource","layout","form","component","page","email","CREATION_UNIT_KINDS","aiCreationUnit","creation","slot","kind","label","name","CREATION_UNITS","unit","create","AI_SITE_NO_PLAN_COPY","AI_SITE_NO_PAGE_STEP_COPY","AI_SITE_UNIT_EMPTY_COPY","AI_SITE_MAX_PASSES","max","length","aiSiteJobUnits","plan","options","units","find","entry","push","screens","forEach","screen","index","title","welcomeEmail","aiSitePendingUnits","outputs","done","Map","output","set","get","filter","left","aiSiteSeoOutputs","job","some","id","proposal","inputs","hostId","aiSiteBuiltRefs","byResource","rows","built","row","shift","toLowerCase","aiSiteBuiltLayoutId","values","aiSiteResolvedRef","ref","slice","indexOf","trim","aiSiteBriefLines","brief","lines","site","businessName","businessType","audience","city","brand","Boolean","join","aiSiteEmailBriefLines","submissions","aiSiteUnitJobId","recorded","$id","aiSiteUnitJob","labels","unitPlan","reuse","placedByScreens","Set","flatMap","sections","section","uses","has","duplicateOf","why","purpose","template","record","dataset","map","slug","unitInputs","originJobId","steps","mode","emailType","aiSiteJobAdmission","context","status","error","freeTaste","aiSiteFreeTaste","org","pages","shape","owner","orgId","aiSiteWelcomeEmail","aiRunJobUnit","input","outcome","runner","spent","usage","estCostUsd","model","stopReason","effort","refused","failure","review","continue","emptyCopy","AI_SITE_PAGE_NOT_WRITTEN_COPY","aiSitePageWritten","firestore","readNodes","stored","draftId","every","_","unitJobId","nodes","createAiJobSiteStep","deps","runnerFor","publish","published","sitePublish","now","catch","console","jobId","modelFor","refusal","pending","pass","written","undefined","runAiJobSiteStep","aiSiteJobRunMinimumMs","registerAiSiteJob","minimumMs","minimumMsFor"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-site-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 { resolveEffectivePlan } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport { resolveOrgIdForHost } from '@aglyn/tenant-data-admin/server/organizations'\nimport {\n isAiPlanNewRef,\n type AiBuildPlanCreate,\n type AiBuildPlanCreateKind,\n type AiBuildPlanScreen,\n} from '../model/ai-build-plan'\nimport type {\n AiJob,\n AiJobKind,\n AiJobOutput,\n AiJobOutputResource,\n AiJobPlan,\n} from '../model/ai-jobs.types'\nimport {\n AI_SITE_EMAIL_TYPE,\n AI_SITE_MAX_SECTIONS,\n AI_SITE_PAGES,\n aiSitePagesRefusal,\n aiSitePlanRefusal,\n aiSiteSubmissions,\n aiSiteWords,\n parseAiSiteJobInputs,\n type AiSiteJobInputs,\n} from '../model/ai-site-job'\nimport {\n AI_SITE_SEO_OUTPUT_ID,\n aiSiteSeoProposalForInputs,\n} from '../model/ai-site-start-seo'\nimport { aiModelForStep } from '../providers/routing'\nimport { registerAiJobAdmission, type AiJobAdmission } from './ai-job-admission'\nimport { aiOriginJobId, aiRecordedJobDraftId } from './ai-job-draft-ids'\nimport { readAiDraftNodes } from './ai-job-drafts'\nimport { AI_LAYOUT_SITE_PAGES_INPUT, aiLayoutSitePagesOfPlan } from './ai-job-layout-site-pages'\nimport { aiPageSectionNodeId } from './ai-job-page-sections'\nimport { aiJobPublishesSite, aiPublishGuidedSite } from './ai-site-publish'\nimport { aiConfirmedPlan, aiUnspentOutcome } from './ai-job-generation'\nimport {\n AI_JOB_BRIEF_MAX_CHARS,\n type AiJobStepContext,\n type AiJobStepOutcome,\n type AiJobStepRunner,\n} from './ai-job-text-step'\nimport { AI_JOB_PAGE_STEP_MINIMUM_MS } from './ai-job-page-budget'\nimport {\n aiJobStepRunMinimumMs,\n aiJobStepRunnerFor,\n registerAiJobStep,\n registerAiJobStepPasses,\n} from './ai-jobs'\n\n/**\n * The site scaffold (AGL-2911): the generation step of a `site` job, run once\n * a member confirmed the plan their brief produced.\n *\n * ── It builds nothing itself ─────────────────────────────────────────────\n *\n * A scaffold is a whole site: a palette suggestion, the layout its pages\n * render inside, the contact form they place, four to eight pages with their\n * search listings, and a welcome email. Every one of those is already a job\n * kind with a step of its own, so this step generates nothing and asks no\n * model anything. It works through the confirmed plan one UNIT at a time and\n * hands each unit to the runner registered for the kind that owns it — the\n * theme step for the palette, the page step for a page — under a job of that\n * kind derived from this one.\n *\n * That is why the scaffold cannot drift from the thing it scaffolds: a page\n * it builds is a page job's page, held to the same doctrine, written by the\n * same draft writer, left unpublished by the same rule. A kind whose step\n * this deployment has not loaded is simply not among the units, so nothing\n * here knows which issues have landed.\n *\n * ── One unit a pass ──────────────────────────────────────────────────────\n *\n * The step runs ONE delegated pass and asks the machine to continue, so every\n * pass is one reservation, one provider exchange and one recorded spend, and\n * the org's monthly ceiling binds a scaffold exactly as it binds a chat turn.\n * Credits running out mid-scaffold is therefore the machine's own park: the\n * job waits as `needs_input` with the meter's words, and the beat resumes it\n * where it stopped once the workspace's standing has changed. A pass needs the\n * time its unit's own step needs (`aiSiteJobRunMinimumMs`), so the beat starts\n * a palette change only with a palette change's time left.\n *\n * Where it stopped is read from the job's OUTPUTS rather than kept anywhere:\n * each unit reports exactly one output when it completes, so the units\n * already represented there are the ones already built. A pass interrupted\n * between its work and its record repeats a unit that then finds its own\n * draft and reports it, which is what every step's draft id already gives.\n *\n * ── Never published ──────────────────────────────────────────────────────\n *\n * Nothing here publishes, routes or sends: each delegated step writes the\n * unpublished draft it already wrote for a job of its own kind, and the\n * palette and navigation travel as proposals a member applies.\n *\n * ── The page job builds its creations the same way (AGL-3031) ────────────\n *\n * A page job whose plan creates a layout, forms or components builds them\n * before its page through this same machinery: the units a creation implies\n * (`aiCreationUnit`), where the job stands read from its outputs\n * (`aiSitePendingUnits`), one delegated pass a unit (`aiRunJobUnit`), and the\n * `new:<name>` references resolved to what was built (`aiSiteBuiltRefs`,\n * `aiSiteUnitJob`). There is one loop, and it is the machine's.\n */\n\n/** What a scaffold's units are, in the order it builds them; a component is a page job's. */\nexport type AiSiteUnitKind = 'theme' | 'layout' | 'form' | 'component' | 'page' | 'email'\n\nexport interface AiSiteUnit {\n kind: AiSiteUnitKind\n /** The job kind whose registered runner builds it. */\n jobKind: AiJobKind\n /** The resource the unit reports when it completes. */\n resource: AiJobOutputResource\n /** Stable within the job: what the unit's derived job and draft are addressed by. */\n slot: string\n /** The plan screen a page unit builds. */\n screen?: AiBuildPlanScreen\n /** The creation a theme, layout or form unit builds. */\n creation?: AiBuildPlanCreate\n /** What the unit is, for the derived job's brief. */\n label: string\n}\n\n/** The job kind and output resource each unit is built through. */\nconst UNIT_KINDS: Record<\n AiSiteUnitKind,\n { jobKind: AiJobKind; resource: AiJobOutputResource }\n> = {\n theme: { jobKind: 'theme', resource: 'theme' },\n layout: { jobKind: 'layout', resource: 'layout' },\n form: { jobKind: 'form', resource: 'form' },\n component: { jobKind: 'component', resource: 'reusableComponent' },\n page: { jobKind: 'page', resource: 'screen' },\n email: { jobKind: 'email', resource: 'emailScreen' },\n}\n\n/** The unit kind a creation is built as, where a unit builds it. */\nconst CREATION_UNIT_KINDS: Partial<Record<AiBuildPlanCreateKind, AiSiteUnitKind>> = {\n 'theme-change': 'theme',\n layout: 'layout',\n form: 'form',\n component: 'component',\n}\n\n/**\n * The unit one creation is built as, under the slot its derived job and draft\n * are addressed by; `null` for a creation no unit builds.\n */\nexport function aiCreationUnit(creation: AiBuildPlanCreate, slot: string): AiSiteUnit | null {\n const kind = CREATION_UNIT_KINDS[creation.kind]\n if (!kind) return null\n return { kind, ...UNIT_KINDS[kind], slot, creation, label: creation.name }\n}\n\n/** The creation kinds a unit is built from, by unit kind. */\nconst CREATION_UNITS: Array<{\n unit: AiSiteUnitKind\n create: AiBuildPlanCreateKind\n}> = [\n { unit: 'theme', create: 'theme-change' },\n { unit: 'layout', create: 'layout' },\n { unit: 'form', create: 'form' },\n]\n\nexport const AI_SITE_NO_PLAN_COPY =\n 'This site job has no confirmed plan to build.'\n\n/** What a scaffold answers where the step that builds a page is not loaded. */\nexport const AI_SITE_NO_PAGE_STEP_COPY =\n 'Building a whole site is not available yet.'\n\n/** A unit that finished without reporting what it built; the scaffold cannot go on. */\nexport const AI_SITE_UNIT_EMPTY_COPY =\n 'Part of this site could not be built. Try the site brief again.'\n\n/**\n * The most passes a scaffold's step may take: the largest plan it admits —\n * eight pages of eight sections, its three creations and a welcome email —\n * with nothing to spare, so a runner that never finishes is still bounded\n * while a real site is not.\n */\nexport const AI_SITE_MAX_PASSES =\n AI_SITE_PAGES.max * (AI_SITE_MAX_SECTIONS + 1) + CREATION_UNITS.length + 1\n\n/**\n * The units a plan implies, in build order: the palette first, because a\n * member reads it while the pages are still building; then the layout every\n * page renders inside and the form they place, because a page binds both by\n * id and so needs them to exist; then the pages; then the welcome email,\n * which is about the site rather than part of it.\n */\nexport function aiSiteJobUnits(\n plan: Pick<AiJobPlan, 'create' | 'screens'>,\n options: { welcomeEmail?: boolean } = {},\n): AiSiteUnit[] {\n const units: AiSiteUnit[] = []\n for (const { unit, create } of CREATION_UNITS) {\n const creation = plan.create.find((entry) => entry.kind === create)\n if (!creation) continue\n units.push({\n kind: unit,\n ...UNIT_KINDS[unit],\n slot: unit[0],\n creation,\n label: creation.name,\n })\n }\n plan.screens.forEach((screen, index) => {\n units.push({\n kind: 'page',\n ...UNIT_KINDS.page,\n slot: `p${index}`,\n screen,\n label: screen.title,\n })\n })\n if (options.welcomeEmail) {\n units.push({\n kind: 'email',\n ...UNIT_KINDS.email,\n slot: 'e',\n label: 'Welcome email',\n })\n }\n return units\n}\n\n/**\n * The units a scaffold still owes after the outputs recorded so far. Each\n * unit reports one output of its own resource when it completes, so the\n * outputs already recorded are consumed against the units in build order.\n */\nexport function aiSitePendingUnits(\n units: readonly AiSiteUnit[],\n outputs: readonly Pick<AiJobOutput, 'resource'>[],\n): AiSiteUnit[] {\n const done = new Map<AiJobOutputResource, number>()\n for (const output of outputs) {\n done.set(output.resource, (done.get(output.resource) ?? 0) + 1)\n }\n return units.filter((unit) => {\n const left = done.get(unit.resource) ?? 0\n if (left <= 0) return true\n done.set(unit.resource, left - 1)\n return false\n })\n}\n\n/**\n * The site's own search listing as the scaffold reports it (AGL-2918), or\n * nothing when the job already reported one or its inputs describe no site.\n *\n * It costs nothing and asks no model: the values are arithmetic on the\n * answers (`aiSiteSeoProposal`), so they ride out on the first pass that\n * builds anything rather than waiting for a pass of their own. Its resource\n * is `seo`, which no unit reports, so it cannot be mistaken for a unit's\n * output by `aiSitePendingUnits` and cannot move where the scaffold thinks\n * it is.\n */\nexport function aiSiteSeoOutputs(job: AiJob): AiJobOutput[] {\n const outputs = job.outputs ?? []\n if (outputs.some((output) => output.resource === 'seo' && output.id === AI_SITE_SEO_OUTPUT_ID)) {\n return []\n }\n const proposal = aiSiteSeoProposalForInputs(job.inputs)\n if (!proposal) return []\n return [\n {\n resource: 'seo',\n id: AI_SITE_SEO_OUTPUT_ID,\n hostId: job.hostId ?? null,\n label: 'The site’s search title and description',\n proposal: proposal as unknown as Record<string, unknown>,\n },\n ]\n}\n\n/** A record an earlier unit created, as a later one references it. */\nexport interface AiSiteBuiltRef {\n id: string\n label: string\n /** Only what becomes a record a page can name; a palette change is not one. */\n kind: 'layout' | 'form' | 'component'\n}\n\n/**\n * What the scaffold has already created, as the plan's `new:<name>`\n * references resolve to: the id of the record each completed creation wrote.\n * A page unit is told about the layout and the form by id, exactly as a page\n * job is told about records the site already had.\n */\nexport function aiSiteBuiltRefs(\n units: readonly AiSiteUnit[],\n outputs: readonly Pick<AiJobOutput, 'resource' | 'id' | 'label'>[],\n): Map<string, AiSiteBuiltRef> {\n const byResource = new Map<\n AiJobOutputResource,\n Array<Pick<AiJobOutput, 'id' | 'label'>>\n >()\n for (const output of outputs) {\n const rows = byResource.get(output.resource) ?? []\n rows.push(output)\n byResource.set(output.resource, rows)\n }\n const built = new Map<string, AiSiteBuiltRef>()\n for (const unit of units) {\n const rows = byResource.get(unit.resource)\n const row = rows?.shift()\n if (!row || !unit.creation) continue\n // A theme change is a proposal with no record to reference; a layout, a\n // form and a component become ids a page can name.\n if (\n unit.creation.kind !== 'layout' &&\n unit.creation.kind !== 'form' &&\n unit.creation.kind !== 'component'\n ) {\n continue\n }\n built.set(unit.creation.name.toLowerCase(), {\n id: row.id,\n label: row.label || unit.creation.name,\n kind: unit.creation.kind,\n })\n }\n return built\n}\n\ntype BuiltRefs = ReturnType<typeof aiSiteBuiltRefs>\n\n/** The layout the scaffold built, when it built one. */\nfunction aiSiteBuiltLayoutId(built: BuiltRefs): string | null {\n for (const entry of built.values()) if (entry.kind === 'layout') return entry.id\n return null\n}\n\n/** A plan reference as the unit's own job reads it: an id the site now has, or nothing. */\nexport function aiSiteResolvedRef(ref: string | null, built: BuiltRefs): string | null {\n if (!ref) return null\n if (!isAiPlanNewRef(ref)) return ref\n return (\n built.get(\n ref\n .slice(ref.indexOf(':') + 1)\n .trim()\n .toLowerCase(),\n )?.id ?? null\n )\n}\n\n/**\n * The site's own words, as every unit's brief carries them.\n *\n * The audience is here rather than left to the brief's prose (AGL-2918). A\n * guided start writes it into the brief itself, but an agency batch's brief\n * is a member's own sentence and may never mention it, and a theme, a layout\n * or a form asked to serve nobody in particular serves nobody in particular.\n */\nexport function aiSiteBriefLines(\n brief: string,\n inputs: AiSiteJobInputs,\n): string[] {\n const lines = [brief.trim()]\n const site = [\n inputs.businessName ? `name: ${inputs.businessName}` : '',\n `business: ${inputs.businessType}`,\n inputs.audience ? `for: ${inputs.audience}` : '',\n inputs.city ? `city: ${inputs.city}` : '',\n inputs.brand ? `brand: ${inputs.brand}` : '',\n ].filter(Boolean)\n lines.push(`Site — ${site.join('; ')}.`)\n return lines\n}\n\n/**\n * What the welcome email is told (AGL-2918), beyond that it is a welcome\n * email.\n *\n * The scaffold's email unit used to carry one sentence, and a yes/no toggle\n * decided whether it ran — so the draft was a welcome email for a business\n * in general, to nobody in particular, about nothing that had happened. The\n * two answers it is missing are who wrote in and what now becomes of what\n * they wrote, and the second of those is the person's own routing answer, so\n * the email and the form it acknowledges cannot say different things.\n *\n * ⛔ A DRAFT either way. Nothing here sends, schedules or enrolls anybody:\n * the email step writes an unpublished email design and stops.\n */\nexport function aiSiteEmailBriefLines(\n inputs: Readonly<Record<string, unknown>> | null | undefined,\n): string[] {\n const lines = [\n 'Write the welcome email this site sends someone who gets in touch.',\n ]\n const { audience } = aiSiteWords(inputs)\n if (audience) lines.push(`Write it to ${audience}.`)\n const submissions = aiSiteSubmissions(inputs)\n if (submissions === 'lead') {\n lines.push('Their message is a sales lead, so say that somebody will be in touch about it.')\n } else if (submissions === 'inbox') {\n lines.push('Their message has been read, so say it arrived and that a reply is coming.')\n }\n return lines\n}\n\n/**\n * The id a unit's job is named by, and so the id its draft is written under\n * (AGL-3079): the id the plan entry it builds recorded, or for the welcome\n * email the id recorded on this job's step. A unit that writes no draft — a\n * palette change — and a unit whose plan recorded no id are named by this\n * job's id and the unit's slot, the name every draft of such a plan has.\n */\nexport function aiSiteUnitJobId(job: Pick<AiJob, '$id' | 'steps'>, unit: AiSiteUnit): string {\n const recorded =\n unit.creation?.id ?? unit.screen?.id ?? (unit.kind === 'email' ? aiRecordedJobDraftId(job, 'email') : null)\n return recorded || `${job.$id}-${unit.slot}`\n}\n\n/**\n * The job a unit is built under: this job's org, site, creator and model,\n * the kind that owns the unit, and a plan narrowed to the unit alone.\n *\n * The derived job carries NO steps and NO outputs of the scaffold's: a page\n * job's step reads its own draft and its own plan, and what the other units\n * produced is not its business. Its `$id` is the id the unit's draft is\n * written under (`aiSiteUnitJobId`), which is what a step with no id of its\n * own recorded names its draft by, so a unit re-run after its write finds its\n * own draft rather than writing a second.\n *\n * A creation unit reuses what the plan reuses less what the plan's screens\n * place themselves (AGL-3031): a component a page's section places is the\n * page's to place, and a layout or a card told to place it would be refused\n * for leaving it out.\n */\nexport function aiSiteUnitJob(\n job: AiJob,\n unit: AiSiteUnit,\n built: BuiltRefs,\n): AiJob {\n const plan = job.plan as AiJobPlan\n const labels = { ...(plan.labels ?? {}) }\n for (const entry of built.values()) labels[entry.id] = entry.label\n const inputs = parseAiSiteJobInputs(job.inputs)\n const brief =\n typeof inputs === 'string'\n ? [job.brief]\n : aiSiteBriefLines(job.brief, inputs)\n const unitPlan: AiJobPlan = {\n ...plan,\n labels,\n reuse: [...plan.reuse],\n create: [],\n screens: [],\n }\n if (unit.creation) {\n const placedByScreens = new Set(\n plan.screens.flatMap((screen) => screen.sections.flatMap((section) => section.uses)),\n )\n unitPlan.reuse = plan.reuse.filter((entry) => !placedByScreens.has(entry.id))\n unitPlan.create = [\n {\n ...unit.creation,\n duplicateOf: aiSiteResolvedRef(unit.creation.duplicateOf, built),\n },\n ]\n brief.push(\n `Build the ${unit.creation.kind} “${unit.creation.name}”: ${unit.creation.why}`,\n )\n }\n if (unit.screen) {\n const screen = unit.screen\n // Everything the scaffold has already built is a record the site has, so\n // the page is planned against it the way a page job is planned against\n // what the site always had.\n for (const entry of built.values()) {\n unitPlan.reuse.push({\n kind: entry.kind,\n id: entry.id,\n purpose: `the ${entry.kind} this site’s pages are built on`,\n })\n }\n unitPlan.screens = [\n {\n ...screen,\n // A page the plan named no layout for renders inside the one the\n // scaffold built (AGL-3596); the page step falls back to the site's.\n layout: aiSiteResolvedRef(screen.layout, built) ?? aiSiteBuiltLayoutId(built),\n template: aiSiteResolvedRef(screen.template, built),\n // A scaffold GENERATES every page from its plan (AGL-3596). A page\n // copied from one the site has is not built: a plan that named the\n // starter home page as its home's start produced the starter again,\n // byte for byte, reported as written. A page job keeps rule 15.\n duplicateOf: job.kind === 'site' ? null : aiSiteResolvedRef(screen.duplicateOf, built),\n // A dataset the plan creates and the site has not built yet keeps its\n // name, which the page's draft still binds by (AGL-3475).\n record: screen.record\n ? {\n ...screen.record,\n dataset: aiSiteResolvedRef(screen.record.dataset, built) ?? screen.record.dataset,\n }\n : null,\n sections: screen.sections.map((section) => ({\n ...section,\n uses: section.uses\n .map((ref) => aiSiteResolvedRef(ref, built))\n .filter((ref): ref is string => !!ref),\n })),\n },\n ]\n brief.push(\n `Build the page “${screen.title}” of this site, at ${screen.slug}.`,\n )\n }\n if (unit.kind === 'email') {\n brief.push(...aiSiteEmailBriefLines(job.inputs))\n }\n // The job the member started travels with every unit, so what the unit\n // writes names it (AGL-3596).\n const unitInputs: Record<string, unknown> = { ...job.inputs, originJobId: aiOriginJobId(job) }\n // The layout is built before the pages, so it is told them (AGL-3596): their\n // ids are minted on the plan, and the platform writes the header's links.\n if (job.kind === 'site' && unit.kind === 'layout') {\n const pages = aiLayoutSitePagesOfPlan(plan.screens)\n if (pages.length) unitInputs[AI_LAYOUT_SITE_PAGES_INPUT] = pages\n }\n return {\n ...job,\n $id: aiSiteUnitJobId(job, unit),\n kind: unit.jobKind,\n steps: [],\n outputs: [],\n plan: unitPlan,\n inputs:\n // A theme change on a site with no theme of its own is a new palette.\n unit.kind === 'theme'\n ? { ...unitInputs, mode: 'create' }\n : // The email step reads the kind off the inputs it is handed, so a\n // job the scaffold composed says which kind it is exactly as a\n // member's own does.\n unit.kind === 'email'\n ? { ...unitInputs, emailType: AI_SITE_EMAIL_TYPE }\n : unitInputs,\n brief: brief.join('\\n').slice(0, AI_JOB_BRIEF_MAX_CHARS),\n }\n}\n\n/**\n * A scaffold is admitted with inputs that read, for a site of the job's own\n * org, and only where this deployment has loaded the step that builds a page:\n * a scaffold whose pages nothing can build is not a scaffold.\n */\nexport const aiSiteJobAdmission: AiJobAdmission = async (context) => {\n const inputs = parseAiSiteJobInputs(context.inputs)\n if (typeof inputs === 'string') return { status: 400, error: inputs }\n // The workspace's own page band (AGL-3594): one or two pages on the Free\n // taste, four to eight on a paid plan.\n const freeTaste = aiSiteFreeTaste(context.org)\n const pages = aiSitePagesRefusal(inputs.pages, freeTaste)\n if (pages) return { status: 400, error: pages }\n if (context.plan) {\n const shape = aiSitePlanRefusal(context.plan, { freeTaste })\n if (shape) return { status: 400, error: shape }\n }\n if (!context.hostId) {\n return {\n status: 400,\n error: 'Open the site the scaffold is for before starting the job',\n }\n }\n const owner = await resolveOrgIdForHost(context.hostId)\n if (!owner || owner !== context.orgId)\n return { status: 404, error: 'Unknown site' }\n if (!aiJobStepRunnerFor('page')) {\n return { status: 400, error: AI_SITE_NO_PAGE_STEP_COPY }\n }\n return null\n}\n\n/** Whether a scaffold's workspace spends the Free taste (AGL-3594); a missing org reads as paid, as the band has always been. */\nexport function aiSiteFreeTaste(org: object | null | undefined): boolean {\n return Boolean(org) && resolveEffectivePlan(org as never) === 'free'\n}\n\n/**\n * Whether a scaffold drafts its welcome email (AGL-3594): where it was asked\n * for, and never on the Free taste, whose credits its pages need.\n */\nexport function aiSiteWelcomeEmail(inputs: AiSiteJobInputs, freeTaste: boolean): boolean {\n return inputs.welcomeEmail && !freeTaste\n}\n\n/** What one delegated pass came to: the unit's spend as the job records it, and whether the unit is built. */\nexport interface AiJobUnitPass {\n outcome: AiJobStepOutcome\n /** The unit reported what it built and asked for no pass of its own. */\n built: boolean\n}\n\n/**\n * One delegated pass (AGL-2911, AGL-3031): the unit handed to the runner of\n * the kind that owns it, under the job derived for it, and its outcome as the\n * delegating job records it. A delegate proposes a plan for its own job,\n * which does not exist, so no plan is carried back. A unit that stopped for a\n * person, refused or failed is the delegating job's stop; a unit that asked\n * for another pass keeps its turn; and a unit that finished without reporting\n * what it built fails with `emptyCopy`, since another pass would ask the same\n * thing again. The tokens it spent are on the bill either way.\n */\nexport async function aiRunJobUnit(\n context: AiJobStepContext,\n input: {\n unit: AiSiteUnit\n units: readonly AiSiteUnit[]\n runner: AiJobStepRunner\n emptyCopy: string\n },\n): Promise<AiJobUnitPass> {\n const { job } = context\n const outputs = job.outputs ?? []\n const outcome = await input.runner({\n ...context,\n job: aiSiteUnitJob(job, input.unit, aiSiteBuiltRefs(input.units, outputs)),\n })\n const spent: AiJobStepOutcome = {\n outputs: outcome.outputs,\n usage: outcome.usage,\n estCostUsd: outcome.estCostUsd,\n model: outcome.model,\n stopReason: outcome.stopReason,\n ...(outcome.effort ? { effort: outcome.effort } : {}),\n ...(outcome.refused ? { refused: true } : {}),\n ...(outcome.failure ? { failure: outcome.failure } : {}),\n ...(outcome.review ? { review: outcome.review } : {}),\n }\n if (spent.review || spent.refused || spent.failure) return { outcome: spent, built: false }\n if (outcome.continue) return { outcome: { ...spent, continue: true }, built: false }\n if (!outcome.outputs.length) return { outcome: { ...spent, failure: input.emptyCopy }, built: false }\n return { outcome: spent, built: true }\n}\n\nexport interface AiJobSiteStepDeps {\n /** The runner registry; specs hand in a fake for a kind they drive. */\n runnerFor?: typeof aiJobStepRunnerFor\n /** The draft reader a built page is checked through; specs hand in a fake. */\n readNodes?: typeof readAiDraftNodes\n /** The guided start's publish; specs hand in a fake. */\n publish?: typeof aiPublishGuidedSite\n}\n\n/** A page the scaffold reported built that holds none of its plan's sections. */\nexport const AI_SITE_PAGE_NOT_WRITTEN_COPY =\n 'A page of this site was not written from its plan. Try the site brief again.'\n\n/**\n * Whether a page unit's draft holds every section its plan named, under the\n * ids the page step writes them by (AGL-3596). A page counts as written only\n * then: a run that reported a page and wrote none of its sections — a copy of\n * a page the site already had, byte for byte — is not a page the job built,\n * and is never reported done.\n */\nexport async function aiSitePageWritten(\n firestore: FirebaseFirestore.Firestore,\n input: { hostId: string; unitJobId: string; screen: AiBuildPlanScreen; draftId: string },\n readNodes: typeof readAiDraftNodes = readAiDraftNodes,\n): Promise<boolean> {\n if (!input.screen.sections.length) return false\n const stored = await readNodes(firestore, { kind: 'screen', hostId: input.hostId, id: input.draftId })\n if (!stored) return false\n return input.screen.sections.every((_, index) => aiPageSectionNodeId(input.unitJobId, index) in stored.nodes)\n}\n\nexport function createAiJobSiteStep(\n deps: AiJobSiteStepDeps = {},\n): AiJobStepRunner {\n const runnerFor = deps.runnerFor ?? aiJobStepRunnerFor\n const readNodes = deps.readNodes ?? readAiDraftNodes\n const publish = deps.publish ?? aiPublishGuidedSite\n /** A guided start's last pass puts what it built on the site, once (AGL-3596). */\n const published = async (\n context: AiJobStepContext,\n outcome: AiJobStepOutcome,\n ): Promise<AiJobStepOutcome> => {\n const { job } = context\n if (!aiJobPublishesSite(job) || job.sitePublish || !job.hostId) return outcome\n const sitePublish = await publish(context.firestore, {\n job,\n outputs: [...(job.outputs ?? []), ...outcome.outputs],\n now: context.now,\n }).catch((error: unknown) => {\n // The site is built either way; the pages stay drafts and say so.\n console.error('ai site publish threw', { orgId: job.orgId, jobId: job.$id, error })\n return null\n })\n return sitePublish ? { ...outcome, sitePublish } : outcome\n }\n return async (context): Promise<AiJobStepOutcome> => {\n const { job } = context\n // The scaffold asks no model of its own. What it names where it spends\n // nothing is the model its plan ran on, which is the one exchange this\n // job has already had of its own.\n const model = context.modelFor?.('job.plan') ?? aiModelForStep('job.plan')\n const plan = aiConfirmedPlan(job)\n if (!plan)\n return { ...aiUnspentOutcome(model), failure: AI_SITE_NO_PLAN_COPY }\n // The resume door refuses such a plan at confirmation once it can read\n // one; a plan confirmed before a site changed still stops here, unspent.\n const freeTaste = aiSiteFreeTaste(context.org)\n const refusal = aiSitePlanRefusal(plan, { freeTaste })\n if (refusal) return { ...aiUnspentOutcome(model), failure: refusal }\n const inputs = parseAiSiteJobInputs(job.inputs)\n if (typeof inputs === 'string')\n return { ...aiUnspentOutcome(model), failure: inputs }\n\n // A kind this deployment has not loaded is not among the units at all, so\n // a scaffold owes only what something can build.\n const units = aiSiteJobUnits(plan, {\n welcomeEmail: aiSiteWelcomeEmail(inputs, freeTaste),\n }).filter((unit) => runnerFor(unit.jobKind))\n const outputs = job.outputs ?? []\n const pending = aiSitePendingUnits(units, outputs)\n if (!pending.length) return published(context, aiUnspentOutcome(model))\n const unit = pending[0]\n const runner = runnerFor(unit.jobKind)\n if (!runner) return aiUnspentOutcome(model)\n\n const pass = await aiRunJobUnit(context, {\n unit,\n units,\n runner,\n emptyCopy: AI_SITE_UNIT_EMPTY_COPY,\n })\n // The site's own listing rides out beside the first unit's output, once\n // (AGL-2918): it is derived from the answers rather than generated, so it\n // is ready before anything is built and costs the pass nothing.\n const outcome: AiJobStepOutcome = {\n ...pass.outcome,\n outputs: [...aiSiteSeoOutputs(job), ...pass.outcome.outputs],\n }\n // A page counts as built only when its plan's sections are in it\n // (AGL-3596): otherwise the pass fails, which is our failure and gives\n // the job's credits back, and the page it reported is not reported.\n if (pass.built && unit.kind === 'page' && unit.screen && job.hostId) {\n const page = pass.outcome.outputs.find((output) => output.resource === 'screen')\n const written =\n page !== undefined &&\n (await aiSitePageWritten(\n context.firestore,\n { hostId: job.hostId, unitJobId: aiSiteUnitJobId(job, unit), screen: unit.screen, draftId: page.id },\n readNodes,\n ))\n if (!written) {\n return {\n ...outcome,\n outputs: outcome.outputs.filter((output) => output.resource !== 'screen'),\n failure: AI_SITE_PAGE_NOT_WRITTEN_COPY,\n }\n }\n }\n // A built unit continues the scaffold while units remain after it.\n if (pass.built && pending.length > 1) return { ...outcome, continue: true }\n return pass.built ? published(context, outcome) : outcome\n }\n}\n\nexport const runAiJobSiteStep = createAiJobSiteStep()\n\n/**\n * The least time a scaffold's next pass needs (AGL-3035). The scaffold asks no\n * model itself: each pass hands one unit to the step registered for its kind,\n * under the job derived for it, so it needs what that step registers for that\n * job — a palette change's or a layout's time for those units, a page pass's\n * for a page. A scaffold with nothing left to build, or with no plan it can\n * build, spends nothing, and a page pass's time covers it.\n */\nexport function aiSiteJobRunMinimumMs(job: AiJob): number {\n const plan = aiConfirmedPlan(job)\n const inputs = parseAiSiteJobInputs(job.inputs)\n if (!plan || typeof inputs === 'string') return AI_JOB_PAGE_STEP_MINIMUM_MS\n const units = aiSiteJobUnits(plan, { welcomeEmail: inputs.welcomeEmail }).filter((unit) =>\n aiJobStepRunnerFor(unit.jobKind),\n )\n const outputs = job.outputs ?? []\n const [unit] = aiSitePendingUnits(units, outputs)\n if (!unit) return AI_JOB_PAGE_STEP_MINIMUM_MS\n return aiJobStepRunMinimumMs(aiSiteUnitJob(job, unit, aiSiteBuiltRefs(units, outputs)))\n}\n\n/**\n * Registers the site scaffold with the least time a pass needs — every\n * scaffold builds pages, and a page pass needs the least of any unit — and the\n * time its next pass needs, the passes a whole site's plan may take, and the\n * check a site job passes before it is created or resumed.\n */\nexport function registerAiSiteJob(): void {\n registerAiJobStep('site', runAiJobSiteStep, {\n minimumMs: AI_JOB_PAGE_STEP_MINIMUM_MS,\n minimumMsFor: aiSiteJobRunMinimumMs,\n })\n registerAiJobStepPasses('site', AI_SITE_MAX_PASSES)\n registerAiJobAdmission('site', aiSiteJobAdmission)\n}\n"],"names":["resolveEffectivePlan","resolveOrgIdForHost","isAiPlanNewRef","AI_SITE_EMAIL_TYPE","AI_SITE_MAX_SECTIONS","AI_SITE_PAGES","aiSitePagesRefusal","aiSitePlanRefusal","aiSiteSubmissions","aiSiteWords","parseAiSiteJobInputs","AI_SITE_SEO_OUTPUT_ID","aiSiteSeoProposalForInputs","aiModelForStep","registerAiJobAdmission","aiOriginJobId","aiRecordedJobDraftId","readAiDraftNodes","AI_LAYOUT_SITE_PAGES_INPUT","aiLayoutSitePagesOfPlan","aiPageSectionNodeId","aiJobPublishesSite","aiPublishGuidedSite","aiConfirmedPlan","aiUnspentOutcome","AI_JOB_BRIEF_MAX_CHARS","AI_JOB_PAGE_STEP_MINIMUM_MS","aiJobStepRunMinimumMs","aiJobStepRunnerFor","registerAiJobStep","registerAiJobStepPasses","UNIT_KINDS","theme","jobKind","resource","layout","form","component","page","email","CREATION_UNIT_KINDS","aiCreationUnit","creation","slot","kind","label","name","CREATION_UNITS","unit","create","AI_SITE_NO_PLAN_COPY","AI_SITE_NO_PAGE_STEP_COPY","AI_SITE_UNIT_EMPTY_COPY","AI_SITE_MAX_PASSES","max","length","aiSiteJobUnits","plan","options","units","find","entry","push","screens","forEach","screen","index","title","welcomeEmail","aiSitePendingUnits","outputs","done","Map","output","set","get","filter","left","aiSiteSeoOutputs","job","some","id","proposal","inputs","hostId","aiSiteBuiltRefs","byResource","rows","built","row","shift","toLowerCase","aiSiteBuiltLayoutId","values","aiSiteResolvedRef","ref","slice","indexOf","trim","aiSiteBriefLines","brief","lines","site","businessName","businessType","audience","city","brand","Boolean","join","aiSiteEmailBriefLines","submissions","aiSiteUnitJobId","recorded","$id","aiSiteUnitJob","labels","unitPlan","reuse","placedByScreens","Set","flatMap","sections","section","uses","has","duplicateOf","why","purpose","template","record","dataset","map","slug","unitInputs","originJobId","pages","steps","mode","emailType","aiSiteJobAdmission","context","status","error","freeTaste","aiSiteFreeTaste","org","shape","owner","orgId","aiSiteWelcomeEmail","aiRunJobUnit","input","outcome","runner","spent","usage","estCostUsd","model","stopReason","effort","refused","failure","review","continue","emptyCopy","AI_SITE_PAGE_NOT_WRITTEN_COPY","aiSitePageWritten","firestore","readNodes","stored","draftId","every","_","unitJobId","nodes","createAiJobSiteStep","deps","runnerFor","publish","published","sitePublish","now","catch","console","jobId","modelFor","refusal","pending","pass","written","undefined","runAiJobSiteStep","aiSiteJobRunMinimumMs","registerAiSiteJob","minimumMs","minimumMsFor"],"mappings":"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"}
|
|
@@ -267,6 +267,8 @@ export interface ListAiJobsOptions {
|
|
|
267
267
|
* answers an `in` over statuses as it answers one status.
|
|
268
268
|
*/
|
|
269
269
|
export declare function listAiJobs(firestore: Firestore, orgId: string, options?: ListAiJobsOptions): Promise<AiJob[]>;
|
|
270
|
+
/** The most of an org's newest jobs a site's list scans while its index builds (AGL-3596). */
|
|
271
|
+
export declare const AI_JOBS_HOST_FALLBACK_SCAN = 300;
|
|
270
272
|
/** The wire form. `running` is the lease, read against `now`. */
|
|
271
273
|
export declare function aiJobSummary(job: AiJob, now?: Date): AiJobSummary;
|
|
272
274
|
export interface ClaimedAiJobStep {
|
|
@@ -323,6 +325,12 @@ export interface RecordStepInput {
|
|
|
323
325
|
tokens?: AiJobStepTokenRun;
|
|
324
326
|
/** What a guided site start put live (AGL-3596), kept on the job. */
|
|
325
327
|
sitePublish?: AiJobSitePublish;
|
|
328
|
+
/**
|
|
329
|
+
* A park the caller announces itself (AGL-3596): after the give-back it
|
|
330
|
+
* owes is recorded, so the notice can say it, or never, for a park the
|
|
331
|
+
* caller resolves at once.
|
|
332
|
+
*/
|
|
333
|
+
quiet?: boolean;
|
|
326
334
|
}
|
|
327
335
|
/** One run of a step's runner, as the machine measured it (AGL-2937). */
|
|
328
336
|
export interface AiJobStepTokenRun {
|
|
@@ -358,6 +366,8 @@ export interface RecordedStep {
|
|
|
358
366
|
job: AiJob;
|
|
359
367
|
/** Steps still to run after this one. */
|
|
360
368
|
remaining: number;
|
|
369
|
+
/** The job entered `needs_review` in this write. */
|
|
370
|
+
parked?: boolean;
|
|
361
371
|
}
|
|
362
372
|
/**
|
|
363
373
|
* Record what a step cost and produced, and hand the lease back.
|
package/src/lib/jobs/ai-jobs.js
CHANGED
|
@@ -43,6 +43,8 @@ import { assistExchangeExpiry, assistUsageDay, assistUsageMonth, recordAssistCos
|
|
|
43
43
|
import { aiJobRefundCredits, refundJobCredits } from "../usage/assist-job-refund.js";
|
|
44
44
|
import { freeAssistAccount } from "../usage/assist-free-taste.js";
|
|
45
45
|
import { aiJobAutoConfirms } from "./ai-job-auto-confirm.js";
|
|
46
|
+
import { AI_SITE_GUIDED_BUILD_FAILED_COPY } from "../model/ai-job-failure-copy.js";
|
|
47
|
+
import { aiSiteStartInputsOf } from "../model/ai-site-start.js";
|
|
46
48
|
import { addAdminAudit } from "@aglyn/tenant-data-admin/server/admin-audit-write";
|
|
47
49
|
/**
|
|
48
50
|
* AI generation jobs — the Firestore state machine (AGL-2904).
|
|
@@ -426,8 +428,31 @@ export async function getAiJob(firestore, orgId, jobId) {
|
|
|
426
428
|
...options.statuses
|
|
427
429
|
]);
|
|
428
430
|
else if (options.hostId) query = query.where('hostId', '==', options.hostId);
|
|
429
|
-
const
|
|
430
|
-
|
|
431
|
+
const read = async (from, count)=>(await from.orderBy('createdAt', 'desc').limit(count).get()).docs.map((doc)=>jobFrom(doc)).filter((job)=>job !== null);
|
|
432
|
+
try {
|
|
433
|
+
return await read(query, limit);
|
|
434
|
+
} catch (error) {
|
|
435
|
+
var _options_statuses1;
|
|
436
|
+
// A site's list while its (hostId, createdAt) index is still building
|
|
437
|
+
// (AGL-3596): Firestore refuses the query with FAILED_PRECONDITION until
|
|
438
|
+
// the index is ready, which answered the site's AI jobs page with a 500.
|
|
439
|
+
// The org's own newest jobs — the index every org list already uses —
|
|
440
|
+
// filtered to the site in memory stand in, bounded to
|
|
441
|
+
// `AI_JOBS_HOST_FALLBACK_SCAN` jobs, so a site whose jobs are older than
|
|
442
|
+
// that lists fewer until the index is ready rather than none.
|
|
443
|
+
if (!options.hostId || options.status || ((_options_statuses1 = options.statuses) == null ? void 0 : _options_statuses1.length) || !isFailedPrecondition(error)) throw error;
|
|
444
|
+
console.warn('ai jobs: host index not ready; listing from the org', {
|
|
445
|
+
orgId,
|
|
446
|
+
hostId: options.hostId
|
|
447
|
+
});
|
|
448
|
+
const recent = await read(jobsCollection(firestore, orgId), AI_JOBS_HOST_FALLBACK_SCAN);
|
|
449
|
+
return recent.filter((job)=>job.hostId === options.hostId).slice(0, limit);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
/** The most of an org's newest jobs a site's list scans while its index builds (AGL-3596). */ export const AI_JOBS_HOST_FALLBACK_SCAN = 300;
|
|
453
|
+
/** Firestore's "this query needs an index that is not ready" (gRPC 9). */ function isFailedPrecondition(error) {
|
|
454
|
+
const code = error == null ? void 0 : error.code;
|
|
455
|
+
return code === 9 || code === 'failed-precondition' || code === 'FAILED_PRECONDITION';
|
|
431
456
|
}
|
|
432
457
|
/** The wire form. `running` is the lease, read against `now`. */ export function aiJobSummary(job, now = new Date()) {
|
|
433
458
|
var _job_hostId, _job_steps, _job_outputs, _job_creditsReserved, _job_creditsSpent, _job_refundedCredits, _toIso, _toIso1, _job_error, _job_plan_labels, _job_plan_confirmedBy, _job_review, _job_applied_versions, _job_applied_staged;
|
|
@@ -476,6 +501,8 @@ export async function getAiJob(firestore, orgId, jobId) {
|
|
|
476
501
|
} : null
|
|
477
502
|
}, job.sitePublish ? {
|
|
478
503
|
sitePublish: job.sitePublish
|
|
504
|
+
} : {}, job.kind === 'site' ? {
|
|
505
|
+
siteInputs: aiSiteStartInputsOf(job.inputs)
|
|
479
506
|
} : {});
|
|
480
507
|
}
|
|
481
508
|
function leaseIsLive(job, owner, nowMs) {
|
|
@@ -707,10 +734,11 @@ function nextStepIndex(job) {
|
|
|
707
734
|
parked: parksForReview && job.status !== 'needs_review'
|
|
708
735
|
};
|
|
709
736
|
});
|
|
710
|
-
if (recorded.parked) await announceAiJobTransition(recorded.job, 'needs-review');
|
|
737
|
+
if (recorded.parked && !input.quiet) await announceAiJobTransition(recorded.job, 'needs-review');
|
|
711
738
|
return {
|
|
712
739
|
job: recorded.job,
|
|
713
|
-
remaining: recorded.remaining
|
|
740
|
+
remaining: recorded.remaining,
|
|
741
|
+
parked: recorded.parked
|
|
714
742
|
};
|
|
715
743
|
}
|
|
716
744
|
async function transition(firestore, orgId, jobId, apply) {
|
|
@@ -868,6 +896,21 @@ async function transition(firestore, orgId, jobId, apply) {
|
|
|
868
896
|
return job;
|
|
869
897
|
}
|
|
870
898
|
}
|
|
899
|
+
/**
|
|
900
|
+
* A failure on our side, given back BEFORE the job is failed (AGL-3596): the
|
|
901
|
+
* failed state is the last one a follower of the job reads and the one its
|
|
902
|
+
* notice is written from, so it already carries `refundedCredits`. A job that
|
|
903
|
+
* ended meanwhile — a person's cancel, which pays for what ran — is left as it
|
|
904
|
+
* ended and given nothing back.
|
|
905
|
+
*/ async function failOurFailure(firestore, orgId, jobId, message, detail, refund, now) {
|
|
906
|
+
const current = await getAiJob(firestore, orgId, jobId);
|
|
907
|
+
if (current && !isAiJobTerminal(current.status)) {
|
|
908
|
+
await refundOurFailure(firestore, orgId, current, _extends({}, refund, {
|
|
909
|
+
now
|
|
910
|
+
}));
|
|
911
|
+
}
|
|
912
|
+
return failAiJob(firestore, orgId, jobId, message, detail, now);
|
|
913
|
+
}
|
|
871
914
|
/**
|
|
872
915
|
* Resume a job that stopped for a person (AGL-2935): confirm its plan, or
|
|
873
916
|
* try again the step whose answer broke a building rule. One transaction,
|
|
@@ -1005,7 +1048,7 @@ async function transition(firestore, orgId, jobId, apply) {
|
|
|
1005
1048
|
* round.
|
|
1006
1049
|
*/ export async function runAiJobStep(firestore, orgId, jobId, options) {
|
|
1007
1050
|
var _options_now, _step_attempts, _options_org, _data, _ref, _ref1, _outcome_effort, _step_passes, _options_actor;
|
|
1008
|
-
var _reservation_allotment, _reservation_allotment1;
|
|
1051
|
+
var _reservation_allotment, _reservation_allotment1, _outcome_review;
|
|
1009
1052
|
const now = (_options_now = options.now) != null ? _options_now : new Date();
|
|
1010
1053
|
// A reservation the caller took before the job existed is a message
|
|
1011
1054
|
// counted against the org; a step that fails before the provider is
|
|
@@ -1046,32 +1089,28 @@ async function transition(firestore, orgId, jobId, apply) {
|
|
|
1046
1089
|
const runner = aiJobRunnerForStep(job.kind, step.name);
|
|
1047
1090
|
if (!runner) {
|
|
1048
1091
|
await releaseHeld();
|
|
1049
|
-
const failed = await failAiJob(firestore, orgId, jobId, AI_JOB_NOT_AVAILABLE_COPY, {
|
|
1050
|
-
stepIndex,
|
|
1051
|
-
error: `no runner registered for kind ${job.kind}`
|
|
1052
|
-
}, now);
|
|
1053
1092
|
return {
|
|
1054
1093
|
outcome: 'failed',
|
|
1055
|
-
job: await
|
|
1094
|
+
job: await failOurFailure(firestore, orgId, jobId, AI_JOB_NOT_AVAILABLE_COPY, {
|
|
1095
|
+
stepIndex,
|
|
1096
|
+
error: `no runner registered for kind ${job.kind}`
|
|
1097
|
+
}, {
|
|
1056
1098
|
reason: 'unavailable',
|
|
1057
|
-
stepCredits: 0
|
|
1058
|
-
|
|
1059
|
-
})
|
|
1099
|
+
stepCredits: 0
|
|
1100
|
+
}, now)
|
|
1060
1101
|
};
|
|
1061
1102
|
}
|
|
1062
1103
|
if (((_step_attempts = step.attempts) != null ? _step_attempts : 0) > AI_JOB_STEP_MAX_ATTEMPTS) {
|
|
1063
1104
|
await releaseHeld();
|
|
1064
|
-
const failed = await failAiJob(firestore, orgId, jobId, AI_UPSTREAM_FAILURE_COPY, {
|
|
1065
|
-
stepIndex,
|
|
1066
|
-
error: `step ${step.name} exhausted ${AI_JOB_STEP_MAX_ATTEMPTS} attempts`
|
|
1067
|
-
}, now);
|
|
1068
1105
|
return {
|
|
1069
1106
|
outcome: 'failed',
|
|
1070
|
-
job: await
|
|
1107
|
+
job: await failOurFailure(firestore, orgId, jobId, AI_UPSTREAM_FAILURE_COPY, {
|
|
1108
|
+
stepIndex,
|
|
1109
|
+
error: `step ${step.name} exhausted ${AI_JOB_STEP_MAX_ATTEMPTS} attempts`
|
|
1110
|
+
}, {
|
|
1071
1111
|
reason: 'timeout',
|
|
1072
|
-
stepCredits: 0
|
|
1073
|
-
|
|
1074
|
-
})
|
|
1112
|
+
stepCredits: 0
|
|
1113
|
+
}, now)
|
|
1075
1114
|
};
|
|
1076
1115
|
}
|
|
1077
1116
|
const org = (_options_org = options.org) != null ? _options_org : (_data = (await firestore.collection('orgs').doc(orgId).get()).data()) != null ? _data : {};
|
|
@@ -1197,18 +1236,16 @@ async function transition(firestore, orgId, jobId, apply) {
|
|
|
1197
1236
|
job: requeued
|
|
1198
1237
|
};
|
|
1199
1238
|
}
|
|
1200
|
-
const failed = await failAiJob(firestore, orgId, jobId, AI_UPSTREAM_FAILURE_COPY, {
|
|
1201
|
-
stepIndex,
|
|
1202
|
-
error
|
|
1203
|
-
}, now);
|
|
1204
1239
|
return {
|
|
1205
1240
|
outcome: 'failed',
|
|
1206
|
-
job: await
|
|
1241
|
+
job: await failOurFailure(firestore, orgId, jobId, AI_UPSTREAM_FAILURE_COPY, {
|
|
1242
|
+
stepIndex,
|
|
1243
|
+
error
|
|
1244
|
+
}, {
|
|
1207
1245
|
reason: 'provider',
|
|
1208
1246
|
stepCredits: 0,
|
|
1209
|
-
free: (_reservation_free = reservation.free) != null ? _reservation_free : null
|
|
1210
|
-
|
|
1211
|
-
})
|
|
1247
|
+
free: (_reservation_free = reservation.free) != null ? _reservation_free : null
|
|
1248
|
+
}, now)
|
|
1212
1249
|
};
|
|
1213
1250
|
}
|
|
1214
1251
|
const latencyMs = Math.max(0, Date.now() - runStarted);
|
|
@@ -1228,18 +1265,16 @@ async function transition(firestore, orgId, jobId, apply) {
|
|
|
1228
1265
|
creditsSpent: 0,
|
|
1229
1266
|
error: outcome.failure
|
|
1230
1267
|
}, now);
|
|
1231
|
-
const failed = await failAiJob(firestore, orgId, jobId, outcome.failure, {
|
|
1232
|
-
stepIndex,
|
|
1233
|
-
error: `step failure before the provider: ${outcome.failure}`
|
|
1234
|
-
}, now);
|
|
1235
1268
|
return {
|
|
1236
1269
|
outcome: 'failed',
|
|
1237
|
-
job: await
|
|
1270
|
+
job: await failOurFailure(firestore, orgId, jobId, outcome.failure, {
|
|
1271
|
+
stepIndex,
|
|
1272
|
+
error: `step failure before the provider: ${outcome.failure}`
|
|
1273
|
+
}, {
|
|
1238
1274
|
reason: 'step-failure',
|
|
1239
1275
|
stepCredits: 0,
|
|
1240
|
-
free: (_reservation_free1 = reservation.free) != null ? _reservation_free1 : null
|
|
1241
|
-
|
|
1242
|
-
})
|
|
1276
|
+
free: (_reservation_free1 = reservation.free) != null ? _reservation_free1 : null
|
|
1277
|
+
}, now)
|
|
1243
1278
|
};
|
|
1244
1279
|
}
|
|
1245
1280
|
const tokens = {
|
|
@@ -1303,19 +1338,45 @@ async function transition(firestore, orgId, jobId, apply) {
|
|
|
1303
1338
|
error: message,
|
|
1304
1339
|
tokens
|
|
1305
1340
|
}, now);
|
|
1306
|
-
const
|
|
1341
|
+
const detail = {
|
|
1307
1342
|
stepIndex,
|
|
1308
1343
|
error: outcome.refused ? 'stop_reason refusal' : `step failure: ${message}`
|
|
1309
|
-
}
|
|
1344
|
+
};
|
|
1310
1345
|
// A model declining the brief is not our failure; a step's own is (AGL-3594).
|
|
1311
1346
|
return {
|
|
1312
1347
|
outcome: 'failed',
|
|
1313
|
-
job: outcome.refused ?
|
|
1348
|
+
job: outcome.refused ? await failAiJob(firestore, orgId, jobId, message, detail, now) : await failOurFailure(firestore, orgId, jobId, message, detail, {
|
|
1314
1349
|
reason: 'step-failure',
|
|
1315
1350
|
stepCredits: credits,
|
|
1316
|
-
free: (_reservation_free3 = reservation.free) != null ? _reservation_free3 : null
|
|
1317
|
-
|
|
1318
|
-
|
|
1351
|
+
free: (_reservation_free3 = reservation.free) != null ? _reservation_free3 : null
|
|
1352
|
+
}, now)
|
|
1353
|
+
};
|
|
1354
|
+
}
|
|
1355
|
+
// A guided site start whose step broke a building rule after its re-ask
|
|
1356
|
+
// fails, given back whole (AGL-3596): the person never saw the plan, so a
|
|
1357
|
+
// review asking them to look at it again is a review of nothing. The rule
|
|
1358
|
+
// and its findings go to the log for staff; the person reads one sentence,
|
|
1359
|
+
// and Try again starts a fresh job.
|
|
1360
|
+
if (((_outcome_review = outcome.review) == null ? void 0 : _outcome_review.reason) === 'doctrine' && aiJobAutoConfirms(job) && step.name !== AI_JOB_PLAN_STEP) {
|
|
1361
|
+
var _outcome_review_findings, _outcome_review_message, _reservation_free4;
|
|
1362
|
+
await recordStep(firestore, orgId, jobId, options.owner, stepIndex, _extends({
|
|
1363
|
+
status: 'failed',
|
|
1364
|
+
creditsSpent: credits,
|
|
1365
|
+
error: AI_SITE_GUIDED_BUILD_FAILED_COPY,
|
|
1366
|
+
outputs: outcome.outputs
|
|
1367
|
+
}, spentNothing ? {} : {
|
|
1368
|
+
tokens
|
|
1369
|
+
}), now);
|
|
1370
|
+
return {
|
|
1371
|
+
outcome: 'failed',
|
|
1372
|
+
job: await failOurFailure(firestore, orgId, jobId, AI_SITE_GUIDED_BUILD_FAILED_COPY, {
|
|
1373
|
+
stepIndex,
|
|
1374
|
+
error: `doctrine refusal after the re-ask: ${JSON.stringify((_outcome_review_findings = outcome.review.findings) != null ? _outcome_review_findings : [])} ${(_outcome_review_message = outcome.review.message) != null ? _outcome_review_message : ''}`
|
|
1375
|
+
}, {
|
|
1376
|
+
reason: 'doctrine-refused',
|
|
1377
|
+
stepCredits: credits,
|
|
1378
|
+
free: (_reservation_free4 = reservation.free) != null ? _reservation_free4 : null
|
|
1379
|
+
}, now)
|
|
1319
1380
|
};
|
|
1320
1381
|
}
|
|
1321
1382
|
// A step that stopped for a person parks the job in the same write that
|
|
@@ -1354,6 +1415,8 @@ async function transition(firestore, orgId, jobId, apply) {
|
|
|
1354
1415
|
review
|
|
1355
1416
|
} : {}, outcome.sitePublish ? {
|
|
1356
1417
|
sitePublish: outcome.sitePublish
|
|
1418
|
+
} : {}, review ? {
|
|
1419
|
+
quiet: true
|
|
1357
1420
|
} : {}), now);
|
|
1358
1421
|
const actor = (_options_actor = options.actor) != null ? _options_actor : {
|
|
1359
1422
|
uid: job.createdBy
|
|
@@ -1410,7 +1473,7 @@ async function transition(firestore, orgId, jobId, apply) {
|
|
|
1410
1473
|
};
|
|
1411
1474
|
}
|
|
1412
1475
|
if (review && recorded.job.status === 'needs_review') {
|
|
1413
|
-
var
|
|
1476
|
+
var _reservation_free5;
|
|
1414
1477
|
// Nobody parked the job but its own step, so the row names no actor, as
|
|
1415
1478
|
// the meter's park does.
|
|
1416
1479
|
await logAiJobNeedsInput(orgId, {
|
|
@@ -1420,15 +1483,22 @@ async function transition(firestore, orgId, jobId, apply) {
|
|
|
1420
1483
|
kind: job.kind,
|
|
1421
1484
|
reason: review.reason
|
|
1422
1485
|
});
|
|
1423
|
-
// A plan the plan rules still refused
|
|
1486
|
+
// A plan the plan rules still refused, and a step that still broke a
|
|
1487
|
+
// building rule after its re-ask, are our failures (AGL-3594, AGL-3596):
|
|
1488
|
+
// given back before the person is told, so the notice can say so. Trying
|
|
1489
|
+
// again spends anew, and a second refusal gives back again, within the
|
|
1490
|
+
// day's bound.
|
|
1491
|
+
const ours = outcome.uncredited ? 'plan-refused' : review.reason === 'doctrine' ? 'doctrine-refused' : null;
|
|
1492
|
+
const parked = ours ? await refundOurFailure(firestore, orgId, recorded.job, {
|
|
1493
|
+
reason: ours,
|
|
1494
|
+
stepCredits: credits,
|
|
1495
|
+
free: (_reservation_free5 = reservation.free) != null ? _reservation_free5 : null,
|
|
1496
|
+
now
|
|
1497
|
+
}) : recorded.job;
|
|
1498
|
+
if (recorded.parked) await announceAiJobTransition(parked, 'needs-review');
|
|
1424
1499
|
return {
|
|
1425
1500
|
outcome: 'needs_review',
|
|
1426
|
-
job:
|
|
1427
|
-
reason: 'plan-refused',
|
|
1428
|
-
stepCredits: credits,
|
|
1429
|
-
free: (_reservation_free4 = reservation.free) != null ? _reservation_free4 : null,
|
|
1430
|
-
now
|
|
1431
|
-
}) : recorded.job
|
|
1501
|
+
job: parked
|
|
1432
1502
|
};
|
|
1433
1503
|
}
|
|
1434
1504
|
if (recorded.remaining > 0 || isAiJobTerminal(recorded.job.status)) {
|