@aglyn/plugins-ai 1.0.0-beta.218 → 1.0.0-beta.219
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/README.md +2 -2
- package/package.json +17 -17
- package/src/index.d.ts +1 -0
- package/src/index.js.map +1 -1
- package/src/lib/activity/ai-activity-actions.js +8 -1
- package/src/lib/activity/ai-activity-actions.js.map +1 -1
- package/src/lib/components/ai-assist-provider.component.js +2 -2
- package/src/lib/components/ai-assist-provider.component.js.map +1 -1
- package/src/lib/components/ai-brief-dialog.component.js +3 -3
- package/src/lib/components/ai-brief-dialog.component.js.map +1 -1
- package/src/lib/components/ai-experiment-cards.component.js +1 -1
- package/src/lib/components/ai-experiment-cards.component.js.map +1 -1
- package/src/lib/components/ai-job-plan.component.d.ts +8 -0
- package/src/lib/components/ai-job-plan.component.js +21 -4
- package/src/lib/components/ai-job-plan.component.js.map +1 -1
- package/src/lib/components/ai-permissions-on-host.component.d.ts +12 -2
- package/src/lib/components/ai-permissions-on-host.component.js +12 -3
- package/src/lib/components/ai-permissions-on-host.component.js.map +1 -1
- package/src/lib/components/ai-product-copy-card.component.d.ts +1 -1
- package/src/lib/components/ai-product-copy-card.component.js.map +1 -1
- package/src/lib/components/ai-product-import-option.component.d.ts +1 -1
- package/src/lib/components/ai-product-import-option.component.js.map +1 -1
- package/src/lib/components/ai-product-zones.d.ts +124 -0
- package/src/lib/components/ai-product-zones.js +32 -0
- package/src/lib/components/ai-product-zones.js.map +1 -0
- package/src/lib/components/ai-products-hub-card.component.d.ts +1 -1
- package/src/lib/components/ai-products-hub-card.component.js.map +1 -1
- package/src/lib/components/ai-products-proposals.component.d.ts +1 -1
- package/src/lib/components/ai-products-proposals.component.js.map +1 -1
- package/src/lib/components/ai-seo-audit-card.component.js +35 -92
- package/src/lib/components/ai-seo-audit-card.component.js.map +1 -1
- package/src/lib/components/ai-seo-fields-card.component.js.map +1 -1
- package/src/lib/components/ai-theme-proposal-card.component.js.map +1 -1
- package/src/lib/components/assist-jobs-drawer.component.js +21 -15
- package/src/lib/components/assist-jobs-drawer.component.js.map +1 -1
- package/src/lib/components/assist-panel.component.js +2 -2
- package/src/lib/components/assist-panel.component.js.map +1 -1
- package/src/lib/components/assist-signals-page.component.js +2 -2
- package/src/lib/components/assist-signals-page.component.js.map +1 -1
- package/src/lib/jobs/ai-email-bindings.d.ts +28 -21
- package/src/lib/jobs/ai-email-bindings.js +75 -42
- package/src/lib/jobs/ai-email-bindings.js.map +1 -1
- package/src/lib/jobs/ai-job-campaign-step.d.ts +0 -2
- package/src/lib/jobs/ai-job-campaign-step.js +5 -10
- package/src/lib/jobs/ai-job-campaign-step.js.map +1 -1
- package/src/lib/jobs/ai-job-component-step.js +9 -3
- package/src/lib/jobs/ai-job-component-step.js.map +1 -1
- package/src/lib/jobs/ai-job-drafts.js +1 -1
- package/src/lib/jobs/ai-job-drafts.js.map +1 -1
- package/src/lib/jobs/ai-job-email-step.d.ts +0 -2
- package/src/lib/jobs/ai-job-email-step.js +1 -3
- package/src/lib/jobs/ai-job-email-step.js.map +1 -1
- package/src/lib/jobs/ai-job-experiment-step.d.ts +1 -1
- package/src/lib/jobs/ai-job-experiment-step.js +1 -1
- package/src/lib/jobs/ai-job-experiment-step.js.map +1 -1
- package/src/lib/jobs/ai-job-generation.d.ts +1 -1
- package/src/lib/jobs/ai-job-generation.js +10 -2
- package/src/lib/jobs/ai-job-generation.js.map +1 -1
- package/src/lib/jobs/ai-job-layout-step.js +1 -1
- package/src/lib/jobs/ai-job-layout-step.js.map +1 -1
- package/src/lib/jobs/ai-job-page-sections.d.ts +4 -2
- package/src/lib/jobs/ai-job-page-sections.js +7 -4
- package/src/lib/jobs/ai-job-page-sections.js.map +1 -1
- package/src/lib/jobs/ai-job-page-step.js +7 -2
- package/src/lib/jobs/ai-job-page-step.js.map +1 -1
- package/src/lib/jobs/ai-job-plan-conformance.js +33 -0
- package/src/lib/jobs/ai-job-plan-conformance.js.map +1 -1
- package/src/lib/jobs/ai-job-plan-step.d.ts +3 -0
- package/src/lib/jobs/ai-job-plan-step.js +29 -17
- package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
- package/src/lib/jobs/ai-job-plugin-drafts.d.ts +14 -6
- package/src/lib/jobs/ai-job-plugin-drafts.js +12 -4
- package/src/lib/jobs/ai-job-plugin-drafts.js.map +1 -1
- package/src/lib/jobs/ai-job-products-step.d.ts +7 -0
- package/src/lib/jobs/ai-job-products-step.js +54 -32
- package/src/lib/jobs/ai-job-products-step.js.map +1 -1
- package/src/lib/jobs/ai-job-seo-step.d.ts +3 -24
- package/src/lib/jobs/ai-job-seo-step.js +35 -108
- package/src/lib/jobs/ai-job-seo-step.js.map +1 -1
- package/src/lib/jobs/ai-job-template-step.js +1 -0
- package/src/lib/jobs/ai-job-template-step.js.map +1 -1
- package/src/lib/jobs/ai-job-theme-step.d.ts +1 -1
- package/src/lib/jobs/ai-job-theme-step.js +1 -1
- package/src/lib/jobs/ai-job-theme-step.js.map +1 -1
- package/src/lib/jobs/ai-job-workflow-step.js +17 -9
- package/src/lib/jobs/ai-job-workflow-step.js.map +1 -1
- package/src/lib/jobs/ai-workflow-records.d.ts +19 -11
- package/src/lib/jobs/ai-workflow-records.js +60 -27
- package/src/lib/jobs/ai-workflow-records.js.map +1 -1
- package/src/lib/model/ai-allotments.d.ts +11 -3
- package/src/lib/model/ai-allotments.js +21 -4
- package/src/lib/model/ai-allotments.js.map +1 -1
- package/src/lib/model/ai-automation-outline.d.ts +21 -2
- package/src/lib/model/ai-automation-outline.js.map +1 -1
- package/src/lib/model/ai-build-plan.d.ts +57 -0
- package/src/lib/model/ai-build-plan.js +116 -7
- package/src/lib/model/ai-build-plan.js.map +1 -1
- package/src/lib/model/ai-seo.d.ts +11 -51
- package/src/lib/model/ai-seo.js +0 -17
- package/src/lib/model/ai-seo.js.map +1 -1
- package/src/lib/model/ai-site-inventory.js +1 -1
- package/src/lib/model/ai-site-inventory.js.map +1 -1
- package/src/lib/model/ai-template-subjects.js +2 -2
- package/src/lib/model/ai-template-subjects.js.map +1 -1
- package/src/lib/model/ai-theme-proposal.d.ts +1 -1
- package/src/lib/model/ai-theme-proposal.js +1 -1
- package/src/lib/model/ai-theme-proposal.js.map +1 -1
- package/src/lib/model/ai-workflow-job.d.ts +1 -3
- package/src/lib/model/ai-workflow-job.js +1 -2
- package/src/lib/model/ai-workflow-job.js.map +1 -1
- package/src/lib/plan-entitlements.d.ts +149 -0
- package/src/lib/plan-entitlements.js +168 -0
- package/src/lib/plan-entitlements.js.map +1 -0
- package/src/lib/plugin.js +1 -1
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/runtime/ai-doctrine-validators.d.ts +25 -4
- package/src/lib/runtime/ai-doctrine-validators.js +178 -21
- package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
- package/src/lib/runtime/ai-doctrine.js +5 -5
- package/src/lib/runtime/ai-doctrine.js.map +1 -1
- package/src/lib/runtime/ai-eval-live.js +29 -14
- package/src/lib/runtime/ai-eval-live.js.map +1 -1
- package/src/lib/runtime/ai-eval.d.ts +8 -1
- package/src/lib/runtime/ai-eval.js +29 -2
- package/src/lib/runtime/ai-eval.js.map +1 -1
- package/src/lib/runtime/ai-node-tree.js +3 -3
- package/src/lib/runtime/ai-node-tree.js.map +1 -1
- package/src/lib/runtime/ai-palette.d.ts +6 -0
- package/src/lib/runtime/ai-palette.generated.js +15 -12
- package/src/lib/runtime/ai-palette.generated.js.map +1 -1
- package/src/lib/runtime/ai-palette.js +7 -1
- package/src/lib/runtime/ai-palette.js.map +1 -1
- package/src/lib/runtime/seo-audit.d.ts +13 -63
- package/src/lib/runtime/seo-audit.js +22 -207
- package/src/lib/runtime/seo-audit.js.map +1 -1
- package/src/lib/runtime/site-inventory.js +1 -1
- package/src/lib/runtime/site-inventory.js.map +1 -1
- package/src/lib/server/ai-assist-prompts.js +3 -3
- package/src/lib/server/ai-assist-prompts.js.map +1 -1
- package/src/lib/server/assist-chat.js +11 -10
- package/src/lib/server/assist-chat.js.map +1 -1
- package/src/lib/server/assist-view-context.js +23 -23
- package/src/lib/server/assist-view-context.js.map +1 -1
- package/src/lib/server.d.ts +1 -0
- package/src/lib/server.js.map +1 -1
- package/src/lib/testing/stand-in-automation-index.d.ts +20 -0
- package/src/lib/testing/stand-in-automation-index.js +102 -0
- package/src/lib/testing/stand-in-automation-index.js.map +1 -0
- package/src/lib/testing/stand-in-list-send-time.d.ts +4 -0
- package/src/lib/testing/stand-in-list-send-time.js +77 -0
- package/src/lib/testing/stand-in-list-send-time.js.map +1 -0
- package/src/lib/testing/stand-in-pipeline-index.d.ts +32 -0
- package/src/lib/testing/stand-in-pipeline-index.js +59 -0
- package/src/lib/testing/stand-in-pipeline-index.js.map +1 -0
- package/src/lib/testing/stand-in-product-index.d.ts +20 -0
- package/src/lib/testing/stand-in-product-index.js +105 -0
- package/src/lib/testing/stand-in-product-index.js.map +1 -0
- package/src/lib/tools/ai-component-tool.js +2 -2
- package/src/lib/tools/ai-component-tool.js.map +1 -1
- package/src/lib/tools/ai-duplicate-resource-tool.js +1 -1
- package/src/lib/tools/ai-duplicate-resource-tool.js.map +1 -1
- package/src/lib/tools/ai-seo-tool.d.ts +6 -18
- package/src/lib/tools/ai-seo-tool.js +7 -38
- package/src/lib/tools/ai-seo-tool.js.map +1 -1
- package/src/lib/tools/ai-workflow-tool.d.ts +1 -1
- package/src/lib/tools/ai-workflow-tool.js +1 -1
- package/src/lib/tools/ai-workflow-tool.js.map +1 -1
- package/src/lib/usage/ai-allotment-alerts.js +8 -1
- package/src/lib/usage/ai-allotment-alerts.js.map +1 -1
- package/src/lib/usage/assist-free-taste.js +2 -1
- package/src/lib/usage/assist-free-taste.js.map +1 -1
- package/src/lib/usage-axes.d.ts +39 -0
- package/src/lib/usage-axes.js +70 -0
- package/src/lib/usage-axes.js.map +1 -0
- package/src/lib/runtime/seo-page-facts.d.ts +0 -61
- package/src/lib/runtime/seo-page-facts.js +0 -150
- package/src/lib/runtime/seo-page-facts.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-build-plan.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { AiTool } from '../providers/contract'\n\n/**\n * The plan a generation job builds from (AGL-2935): what the job's plan step\n * (`jobs/ai-job-plan-step.ts`) returns, before a single node is generated.\n *\n * This module is the plan's MODEL — its shape, its tool schema and its\n * reader — and imports nothing at runtime, so the doctrine validators, the\n * plan step, the job document and the proposal can all read it without\n * importing each other.\n *\n * A plan is REFERENCES, never documents (AGL-2937). It names what the site\n * already has by inventory id, what the job will create by a short name, and\n * which screens it builds from which of those — so the doctrine can be held\n * against the plan while fixing it costs a sentence, not a regenerated page,\n * and the customer reads what the job will do before it spends a credit on\n * doing it.\n *\n * `new:<name>` is how an entry refers to something the plan itself creates;\n * any other reference is an id from the site inventory.\n */\n\nexport const AI_PLAN_NEW_REF_PREFIX = 'new:'\n\n/** Records a plan may reuse, by the inventory list they come from. */\nexport type AiBuildPlanReuseKind =\n | 'component'\n | 'layout'\n | 'template'\n | 'form'\n | 'dataset'\n | 'collection'\n | 'screen'\n | 'theme'\n\nexport const AI_BUILD_PLAN_REUSE_KINDS: readonly AiBuildPlanReuseKind[] = [\n 'component',\n 'layout',\n 'template',\n 'form',\n 'dataset',\n 'collection',\n 'screen',\n 'theme',\n]\n\n/** What a plan may create. A theme change is a draft through the theme door, never a literal. */\nexport type AiBuildPlanCreateKind =\n | 'component'\n | 'layout'\n | 'template'\n | 'form'\n | 'theme-change'\n | 'dataset'\n | 'email'\n\n/**\n * What each creation is called in a sentence, and where a member makes one.\n *\n * Total over the union deliberately: a refusal that cannot name a creation is\n * a refusal a member cannot act on, so widening `AiBuildPlanCreateKind`\n * without saying where the new thing is made does not compile.\n */\nexport const AI_BUILD_PLAN_CREATION_NOUNS: Record<\n AiBuildPlanCreateKind,\n { noun: string; where: string }\n> = {\n component: { noun: 'component', where: 'on the Components page' },\n form: { noun: 'form', where: 'on the Forms page' },\n layout: { noun: 'layout', where: 'on the Layouts page' },\n template: { noun: 'template', where: 'in the Templates library' },\n 'theme-change': { noun: 'theme change', where: 'in the Theme section' },\n dataset: { noun: 'dataset', where: 'on the Datasets page' },\n email: { noun: 'email design', where: 'in Emails → Templates' },\n}\n\nexport const AI_BUILD_PLAN_CREATE_KINDS: readonly AiBuildPlanCreateKind[] = [\n 'component',\n 'layout',\n 'template',\n 'form',\n 'theme-change',\n 'dataset',\n 'email',\n]\n\nexport interface AiBuildPlanReuse {\n kind: AiBuildPlanReuseKind\n /** The inventory id. */\n id: string\n /** What the plan uses it for, in a few words. */\n purpose: string\n}\n\nexport interface AiBuildPlanCreate {\n kind: AiBuildPlanCreateKind\n /** Unique within the plan; referenced as `new:<name>`. */\n name: string\n /** Why nothing the site has will do — shown in the proposal. */\n why: string\n /** The inventory id this starts from as a duplicate, or null. */\n duplicateOf: string | null\n /**\n * A component: its props as `name:type`, an icon as `name:icon`\n * (AGL-3054). A layout: the regions it has, from the closed vocabulary\n * `AI_LAYOUT_REGIONS` holds (AGL-3024) — a layout has no props, and a\n * region named here is what the built layout is held to. A template: the\n * binding tokens its page shows, from the subject's own closed catalog\n * (`AI_TEMPLATE_SUBJECT_DEFINITIONS[subject].tokens`, AGL-3143) — the\n * template's analogue of a layout's regions, and what the built template is\n * held to. A form or a dataset: its field names. A theme change: the\n * palette paths it adds or changes.\n */\n fields: string[]\n /**\n * The id the draft this creation becomes is written under, where a unit of\n * the job builds it (AGL-3079). Minted when the job keeps the plan, never\n * part of the model's answer.\n */\n id?: string\n}\n\nexport interface AiBuildPlanSection {\n /** A few words: \"hero\", \"services grid\", \"contact form\". */\n name: string\n /** What the section places: inventory ids or `new:<name>` references. */\n uses: string[]\n /** How many repeated items it shows (cards, rows, tiles); 0 when none. */\n items: number\n}\n\nexport interface AiBuildPlanScreen {\n title: string\n slug: string\n /** The layout it renders inside. */\n layout: string | null\n /** The template it applies, or null for a one-off page. */\n template: string | null\n /** The inventory screen it starts from as a duplicate, or null. */\n duplicateOf: string | null\n /** Whether the site navigation gains an entry for it. */\n nav: boolean\n seoTitle: string\n seoDescription: string\n sections: AiBuildPlanSection[]\n /**\n * The id the draft screen is written under, where a unit of the job builds\n * it (AGL-3079). Minted when the job keeps the plan, never part of the\n * model's answer.\n */\n id?: string\n}\n\nexport interface AiBuildPlan {\n reuse: AiBuildPlanReuse[]\n create: AiBuildPlanCreate[]\n screens: AiBuildPlanScreen[]\n}\n\n/**\n * How much one plan may hold. A job builds what one proposal can describe\n * and one person can read before confirming; a brief asking for more is\n * several jobs.\n */\nexport const AI_BUILD_PLAN_LIMITS = {\n reuse: 40,\n create: 20,\n screens: 12,\n sections: 16,\n uses: 8,\n fields: 30,\n text: 200,\n seoDescription: 320,\n items: 500,\n} as const\n\nconst string = (description: string) => ({ type: 'string', description })\nconst nullableString = (description: string) => ({\n anyOf: [{ type: 'string' }, { type: 'null' }],\n description,\n})\nconst strings = (description: string) => ({\n type: 'array',\n items: { type: 'string' },\n description,\n})\n\n/**\n * The ceiling `parseAiBuildPlan` cuts a field's text at, in words (AGL-3022).\n * The reader repairs an overlong value by cutting it, and a model never told\n * where the cut falls writes a rationale that stops mid-sentence there.\n */\nconst atMost = (limit: number = AI_BUILD_PLAN_LIMITS.text) => `at most ${limit} characters`\n\n/**\n * The plan as a strict tool's input schema. Only the keywords strict\n * structured output accepts: every object closes its properties and lists\n * every one as required, an optional value is a `null` branch, and no\n * length or count bound appears as a keyword — those are enforced by\n * `parseAiBuildPlan`, which every answer passes through anyway.\n *\n * A text ceiling is stated in words instead, on each field the model writes.\n * A reference is copied from the inventory or from a creation's name, so its\n * length is not the model's to choose. A search title and description are\n * held by rule 10 to ceilings far inside the reader's, which the doctrine\n * states, so naming the reader's here would contradict the rule.\n */\nexport const AI_BUILD_PLAN_TOOL: AiTool = {\n name: 'submit_build_plan',\n description:\n 'Submit the build plan: what the site already has that this job reuses, what it creates and why, and the screens it builds from them.',\n strict: true,\n inputSchema: {\n type: 'object',\n additionalProperties: false,\n required: ['reuse', 'create', 'screens'],\n properties: {\n reuse: {\n type: 'array',\n description: 'Records the site already has that this plan uses, by inventory id.',\n items: {\n type: 'object',\n additionalProperties: false,\n required: ['kind', 'id', 'purpose'],\n properties: {\n kind: { type: 'string', enum: [...AI_BUILD_PLAN_REUSE_KINDS] },\n id: string('The id from the site inventory.'),\n purpose: string(`What the plan uses it for, in a few words, ${atMost()}.`),\n },\n },\n },\n create: {\n type: 'array',\n description:\n 'What the plan creates. Creating is the exception: each entry says why nothing listed will do.',\n items: {\n type: 'object',\n additionalProperties: false,\n required: ['kind', 'name', 'why', 'duplicateOf', 'fields'],\n properties: {\n kind: { type: 'string', enum: [...AI_BUILD_PLAN_CREATE_KINDS] },\n name: string(\n `Unique within the plan, ${atMost()}; other entries refer to it as new:<name>.`,\n ),\n why: string(`Why nothing the site already has will do, in one sentence of ${atMost()}.`),\n duplicateOf: nullableString(\n 'The inventory id this starts from as a duplicate, or null.',\n ),\n fields: strings(\n `A component: its props as name:type, an icon as name:icon. A layout: the regions it has, each one of header, nav, sidebar, main, footer — list every region the finished layout has, not only the ones it gains. A form or dataset: its field names. A theme change: the palette paths it adds or changes. Each ${atMost()}.`,\n ),\n },\n },\n },\n screens: {\n type: 'array',\n description: 'The screens the job builds, each a new draft.',\n items: {\n type: 'object',\n additionalProperties: false,\n required: [\n 'title',\n 'slug',\n 'layout',\n 'template',\n 'duplicateOf',\n 'nav',\n 'seoTitle',\n 'seoDescription',\n 'sections',\n ],\n properties: {\n title: string(`The screen name, ${atMost()}.`),\n slug: string(\n `The path, ${atMost()}: lowercase words joined by hyphens, e.g. /services/roof-repair.`,\n ),\n layout: nullableString(\n 'The layout it renders inside: an inventory id or new:<name>.',\n ),\n template: nullableString(\n 'The template it applies (inventory id or new:<name>), or null for a one-off page.',\n ),\n duplicateOf: nullableString(\n 'The inventory screen it starts from as a duplicate, or null.',\n ),\n nav: {\n type: 'boolean',\n description: 'Whether the site navigation gains an entry for it.',\n },\n seoTitle: string('The search-result title.'),\n seoDescription: string('The search-result description.'),\n sections: {\n type: 'array',\n description: 'The sections, top to bottom.',\n items: {\n type: 'object',\n additionalProperties: false,\n required: ['name', 'uses', 'items'],\n properties: {\n name: string(`A few words, ${atMost()}: hero, services grid, contact form.`),\n uses: strings('What the section places: inventory ids or new:<name>.'),\n items: {\n type: 'integer',\n description: 'How many repeated items it shows; 0 when none.',\n },\n },\n },\n },\n },\n },\n },\n },\n },\n}\n\nexport type AiBuildPlanParse =\n | { ok: true; plan: AiBuildPlan; repairs: string[] }\n | { ok: false; error: string }\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\nclass PlanShapeError extends Error {}\n\n/**\n * Read a plan out of a tool call, or name what makes it unreadable.\n *\n * Structure is refused — a missing list, an unknown kind, more screens than\n * a job holds, two creations under one name — because a plan that reads\n * differently from what the model meant is worse than a re-ask. Length is\n * repaired: copy past a ceiling is cut and the cut is listed.\n */\nexport function parseAiBuildPlan(input: unknown): AiBuildPlanParse {\n const repairs: string[] = []\n const text = (\n value: unknown,\n path: string,\n limit: number = AI_BUILD_PLAN_LIMITS.text,\n ): string => {\n if (typeof value !== 'string') throw new PlanShapeError(`${path} is not text`)\n const trimmed = value.trim()\n if (trimmed.length > limit) {\n repairs.push(`${path} was over ${limit} characters; truncated`)\n // On the last word break inside the ceiling, not through a word: a\n // rationale is read by the member confirming the plan, and half a word\n // reads as a broken answer rather than a long one (AGL-3022). A run of\n // `limit` characters with no break in it still has to be cut where the\n // ceiling falls.\n const cut = trimmed.slice(0, limit)\n const brk = cut.search(/\\s\\S*$/)\n return (brk > 0 ? cut.slice(0, brk) : cut).trimEnd()\n }\n return trimmed\n }\n const nullable = (value: unknown, path: string): string | null =>\n value === null || value === undefined || value === '' ? null : text(value, path) || null\n const list = (value: unknown, path: string, limit: number): unknown[] => {\n if (!Array.isArray(value)) throw new PlanShapeError(`${path} is not a list`)\n if (value.length > limit) {\n throw new PlanShapeError(`${path} lists ${value.length}; one plan holds at most ${limit}`)\n }\n return value\n }\n const record = (value: unknown, path: string): Record<string, unknown> => {\n if (!isRecord(value)) throw new PlanShapeError(`${path} is not an object`)\n return value\n }\n const oneOf = <T extends string>(value: unknown, allowed: readonly T[], path: string): T => {\n if (typeof value !== 'string' || !(allowed as readonly string[]).includes(value)) {\n throw new PlanShapeError(`${path} is not one of ${allowed.join(', ')}`)\n }\n return value as T\n }\n\n try {\n const root = record(input, 'plan')\n const reuse = list(root['reuse'], 'reuse', AI_BUILD_PLAN_LIMITS.reuse).map((raw, index) => {\n const entry = record(raw, `reuse[${index}]`)\n const id = text(entry['id'], `reuse[${index}].id`)\n if (!id) throw new PlanShapeError(`reuse[${index}].id is empty`)\n return {\n kind: oneOf(entry['kind'], AI_BUILD_PLAN_REUSE_KINDS, `reuse[${index}].kind`),\n id,\n purpose: text(entry['purpose'], `reuse[${index}].purpose`),\n }\n })\n const names = new Set<string>()\n const create = list(root['create'], 'create', AI_BUILD_PLAN_LIMITS.create).map(\n (raw, index) => {\n const entry = record(raw, `create[${index}]`)\n const name = text(entry['name'], `create[${index}].name`)\n if (!name) throw new PlanShapeError(`create[${index}].name is empty`)\n const key = name.toLowerCase()\n if (names.has(key)) {\n throw new PlanShapeError(`create[${index}].name \"${name}\" is used twice`)\n }\n names.add(key)\n return {\n kind: oneOf(entry['kind'], AI_BUILD_PLAN_CREATE_KINDS, `create[${index}].kind`),\n name,\n why: text(entry['why'], `create[${index}].why`),\n duplicateOf: nullable(entry['duplicateOf'], `create[${index}].duplicateOf`),\n fields: list(entry['fields'] ?? [], `create[${index}].fields`, AI_BUILD_PLAN_LIMITS.fields)\n .map((field, fieldIndex) => text(field, `create[${index}].fields[${fieldIndex}]`))\n .filter(Boolean),\n }\n },\n )\n const screens = list(root['screens'], 'screens', AI_BUILD_PLAN_LIMITS.screens).map(\n (raw, index) => {\n const entry = record(raw, `screens[${index}]`)\n if (typeof entry['nav'] !== 'boolean') {\n throw new PlanShapeError(`screens[${index}].nav is not true or false`)\n }\n return {\n title: text(entry['title'], `screens[${index}].title`),\n slug: text(entry['slug'], `screens[${index}].slug`),\n layout: nullable(entry['layout'], `screens[${index}].layout`),\n template: nullable(entry['template'], `screens[${index}].template`),\n duplicateOf: nullable(entry['duplicateOf'], `screens[${index}].duplicateOf`),\n nav: entry['nav'],\n seoTitle: text(entry['seoTitle'], `screens[${index}].seoTitle`),\n seoDescription: text(\n entry['seoDescription'],\n `screens[${index}].seoDescription`,\n AI_BUILD_PLAN_LIMITS.seoDescription,\n ),\n sections: list(\n entry['sections'],\n `screens[${index}].sections`,\n AI_BUILD_PLAN_LIMITS.sections,\n ).map((rawSection, sectionIndex) => {\n const path = `screens[${index}].sections[${sectionIndex}]`\n const section = record(rawSection, path)\n const items = Number(section['items'] ?? 0)\n if (!Number.isInteger(items) || items < 0 || items > AI_BUILD_PLAN_LIMITS.items) {\n throw new PlanShapeError(`${path}.items is not a count`)\n }\n return {\n name: text(section['name'], `${path}.name`),\n uses: list(section['uses'] ?? [], `${path}.uses`, AI_BUILD_PLAN_LIMITS.uses)\n .map((ref, refIndex) => text(ref, `${path}.uses[${refIndex}]`))\n .filter(Boolean),\n items,\n }\n }),\n }\n },\n )\n return { ok: true, plan: { reuse, create, screens }, repairs }\n } catch (error) {\n if (error instanceof PlanShapeError) return { ok: false, error: error.message }\n throw error\n }\n}\n\n/** Whether a reference names something the plan creates. */\nexport function isAiPlanNewRef(ref: string | null | undefined): ref is string {\n return typeof ref === 'string' && ref.startsWith(AI_PLAN_NEW_REF_PREFIX)\n}\n\n/** The creation a `new:<name>` reference names, case-insensitively. */\nexport function aiPlanCreateFor(\n plan: AiBuildPlan,\n ref: string,\n): AiBuildPlanCreate | undefined {\n if (!isAiPlanNewRef(ref)) return undefined\n const name = ref.slice(AI_PLAN_NEW_REF_PREFIX.length).trim().toLowerCase()\n return plan.create.find((entry) => entry.name.toLowerCase() === name)\n}\n\n/**\n * A `new:<name>` reference that names no entry of the plan's create list: a\n * screen's own layout or template, or what one of its sections places, with\n * that section's index.\n */\nexport type AiPlanUndeclaredRef = {\n /** The name the reference gives the creation, as written after `new:`. */\n name: string\n /** Where the plan writes it: `screens[0].template`, `screens[0].sections[4].uses[0]`. */\n path: string\n /** The screen that writes it, by index. */\n screenIndex: number\n} & ({ field: 'layout' | 'template'; sectionIndex: null } | { field: 'uses'; sectionIndex: number })\n\n/**\n * Every `new:<name>` reference the plan's create list does not carry, in plan\n * order: each screen's layout, then its template, then what its sections\n * place. A plan that places a creation it never declares has nothing to build\n * it from, so the plan rules refuse each (AGL-3040), and the page and\n * scaffold doors still name one in a plan confirmed before they did.\n */\nexport function aiPlanUndeclaredRefs(plan: AiBuildPlan): AiPlanUndeclaredRef[] {\n const found: AiPlanUndeclaredRef[] = []\n const undeclared = (ref: string | null): ref is string =>\n isAiPlanNewRef(ref) && !aiPlanCreateFor(plan, ref)\n const nameOf = (ref: string) => ref.slice(AI_PLAN_NEW_REF_PREFIX.length).trim()\n plan.screens.forEach((screen, screenIndex) => {\n for (const field of ['layout', 'template'] as const) {\n const ref = screen[field]\n if (!undeclared(ref)) continue\n found.push({\n name: nameOf(ref),\n path: `screens[${screenIndex}].${field}`,\n screenIndex,\n field,\n sectionIndex: null,\n })\n }\n screen.sections.forEach((section, sectionIndex) => {\n section.uses.forEach((ref, useIndex) => {\n if (!undeclared(ref)) return\n found.push({\n name: nameOf(ref),\n path: `screens[${screenIndex}].sections[${sectionIndex}].uses[${useIndex}]`,\n screenIndex,\n field: 'uses',\n sectionIndex,\n })\n })\n })\n })\n return found\n}\n"],"names":["AI_PLAN_NEW_REF_PREFIX","AI_BUILD_PLAN_REUSE_KINDS","AI_BUILD_PLAN_CREATION_NOUNS","component","noun","where","form","layout","template","dataset","email","AI_BUILD_PLAN_CREATE_KINDS","AI_BUILD_PLAN_LIMITS","reuse","create","screens","sections","uses","fields","text","seoDescription","items","string","description","type","nullableString","anyOf","strings","atMost","limit","AI_BUILD_PLAN_TOOL","name","strict","inputSchema","additionalProperties","required","properties","kind","enum","id","purpose","why","duplicateOf","title","slug","nav","seoTitle","isRecord","value","Array","isArray","PlanShapeError","Error","parseAiBuildPlan","input","repairs","path","trimmed","trim","length","push","cut","slice","brk","search","trimEnd","nullable","undefined","list","record","oneOf","allowed","includes","join","root","map","raw","index","entry","names","Set","key","toLowerCase","has","add","field","fieldIndex","filter","Boolean","rawSection","sectionIndex","section","Number","isInteger","ref","refIndex","ok","plan","error","message","isAiPlanNewRef","startsWith","aiPlanCreateFor","find","aiPlanUndeclaredRefs","found","undeclared","nameOf","forEach","screen","screenIndex","useIndex"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-build-plan.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { AiTool } from '../providers/contract'\n\n/**\n * The plan a generation job builds from (AGL-2935): what the job's plan step\n * (`jobs/ai-job-plan-step.ts`) returns, before a single node is generated.\n *\n * This module is the plan's MODEL — its shape, its tool schema and its\n * reader — and imports nothing at runtime, so the doctrine validators, the\n * plan step, the job document and the proposal can all read it without\n * importing each other.\n *\n * A plan is REFERENCES, never documents (AGL-2937). It names what the site\n * already has by inventory id, what the job will create by a short name, and\n * which screens it builds from which of those — so the doctrine can be held\n * against the plan while fixing it costs a sentence, not a regenerated page,\n * and the customer reads what the job will do before it spends a credit on\n * doing it.\n *\n * `new:<name>` is how an entry refers to something the plan itself creates;\n * any other reference is an id from the site inventory.\n */\n\nexport const AI_PLAN_NEW_REF_PREFIX = 'new:'\n\n/** Records a plan may reuse, by the inventory list they come from. */\nexport type AiBuildPlanReuseKind =\n | 'component'\n | 'layout'\n | 'template'\n | 'form'\n | 'dataset'\n | 'collection'\n | 'screen'\n | 'theme'\n\nexport const AI_BUILD_PLAN_REUSE_KINDS: readonly AiBuildPlanReuseKind[] = [\n 'component',\n 'layout',\n 'template',\n 'form',\n 'dataset',\n 'collection',\n 'screen',\n 'theme',\n]\n\n/** What a plan may create. A theme change is a draft through the theme door, never a literal. */\nexport type AiBuildPlanCreateKind =\n | 'component'\n | 'layout'\n | 'template'\n | 'form'\n | 'theme-change'\n | 'dataset'\n | 'email'\n\n/**\n * What each creation is called in a sentence, and where a member makes one.\n *\n * Total over the union deliberately: a refusal that cannot name a creation is\n * a refusal a member cannot act on, so widening `AiBuildPlanCreateKind`\n * without saying where the new thing is made does not compile.\n */\nexport const AI_BUILD_PLAN_CREATION_NOUNS: Record<\n AiBuildPlanCreateKind,\n { noun: string; where: string }\n> = {\n component: { noun: 'component', where: 'on the Components page' },\n form: { noun: 'form', where: 'on the Forms page' },\n layout: { noun: 'layout', where: 'on the Layouts page' },\n template: { noun: 'template', where: 'in the Templates library' },\n 'theme-change': { noun: 'theme change', where: 'in the Theme section' },\n dataset: { noun: 'dataset', where: 'on the Datasets page' },\n email: { noun: 'email design', where: 'in Emails → Templates' },\n}\n\nexport const AI_BUILD_PLAN_CREATE_KINDS: readonly AiBuildPlanCreateKind[] = [\n 'component',\n 'layout',\n 'template',\n 'form',\n 'theme-change',\n 'dataset',\n 'email',\n]\n\nexport interface AiBuildPlanReuse {\n kind: AiBuildPlanReuseKind\n /** The inventory id. */\n id: string\n /** What the plan uses it for, in a few words. */\n purpose: string\n}\n\nexport interface AiBuildPlanCreate {\n kind: AiBuildPlanCreateKind\n /** Unique within the plan; referenced as `new:<name>`. */\n name: string\n /** Why nothing the site has will do — shown in the proposal. */\n why: string\n /** The inventory id this starts from as a duplicate, or null. */\n duplicateOf: string | null\n /**\n * A component: its props as `name:type`, an icon as `name:icon`\n * (AGL-3054). A layout: the regions it has, from the closed vocabulary\n * `AI_LAYOUT_REGIONS` holds (AGL-3024) — a layout has no props, and a\n * region named here is what the built layout is held to. A template: the\n * binding tokens its page shows, from the subject's own closed catalog\n * (`AI_TEMPLATE_SUBJECT_DEFINITIONS[subject].tokens`, AGL-3143) — the\n * template's analogue of a layout's regions, and what the built template is\n * held to. A form or a dataset: its field names. A theme change: the\n * palette paths it adds or changes.\n */\n fields: string[]\n /**\n * The id the draft this creation becomes is written under, where a unit of\n * the job builds it (AGL-3079). Minted when the job keeps the plan, never\n * part of the model's answer.\n */\n id?: string\n}\n\nexport interface AiBuildPlanSection {\n /** A few words: \"hero\", \"services grid\", \"contact form\". */\n name: string\n /** What the section places: inventory ids or `new:<name>` references. */\n uses: string[]\n /** How many repeated items it shows (cards, rows, tiles); 0 when none. */\n items: number\n}\n\nexport interface AiBuildPlanScreen {\n title: string\n slug: string\n /** The layout it renders inside. */\n layout: string | null\n /** The template it applies, or null for a one-off page. */\n template: string | null\n /** The inventory screen it starts from as a duplicate, or null. */\n duplicateOf: string | null\n /** Whether the site navigation gains an entry for it. */\n nav: boolean\n seoTitle: string\n seoDescription: string\n sections: AiBuildPlanSection[]\n /**\n * The id the draft screen is written under, where a unit of the job builds\n * it (AGL-3079). Minted when the job keeps the plan, never part of the\n * model's answer.\n */\n id?: string\n}\n\n/** The third-party players a plan may embed: the hosts the Video embed element plays. */\nexport type AiBuildPlanEmbedHost = 'youtube' | 'vimeo'\n\nexport const AI_BUILD_PLAN_EMBED_HOSTS: readonly AiBuildPlanEmbedHost[] = ['youtube', 'vimeo']\n\n/** What each host is called in a sentence. */\nexport const AI_BUILD_PLAN_EMBED_HOST_NAMES: Record<AiBuildPlanEmbedHost, string> = {\n youtube: 'YouTube',\n vimeo: 'Vimeo',\n}\n\n/**\n * A third-party player the brief asks for (rule 16, AGL-3433). It loads its\n * host's code on the page that carries it, so it is planned, where the member\n * reads that cost before confirming, and a build admits only what the\n * confirmed plan lists.\n */\nexport interface AiBuildPlanEmbed {\n host: AiBuildPlanEmbedHost\n /** Where it plays: a screen of the plan by its slug, or `new:<name>` for a component the plan creates. */\n where: string\n /** The brief's own words asking for it, which the plan step finds in the brief. */\n asked: string\n /** The video's link as the brief gives it, or null for the site owner to paste. */\n url: string | null\n}\n\nexport interface AiBuildPlan {\n reuse: AiBuildPlanReuse[]\n create: AiBuildPlanCreate[]\n screens: AiBuildPlanScreen[]\n /**\n * The third-party players the brief asks for. Absent is none: a plan kept\n * before the list existed, and every plan whose brief asks for no player.\n */\n embeds?: AiBuildPlanEmbed[]\n}\n\n/**\n * How much one plan may hold. A job builds what one proposal can describe\n * and one person can read before confirming; a brief asking for more is\n * several jobs.\n */\nexport const AI_BUILD_PLAN_LIMITS = {\n reuse: 40,\n create: 20,\n screens: 12,\n sections: 16,\n uses: 8,\n fields: 30,\n embeds: 4,\n text: 200,\n seoDescription: 320,\n items: 500,\n} as const\n\nconst string = (description: string) => ({ type: 'string', description })\nconst nullableString = (description: string) => ({\n anyOf: [{ type: 'string' }, { type: 'null' }],\n description,\n})\nconst strings = (description: string) => ({\n type: 'array',\n items: { type: 'string' },\n description,\n})\n\n/**\n * The ceiling `parseAiBuildPlan` cuts a field's text at, in words (AGL-3022).\n * The reader repairs an overlong value by cutting it, and a model never told\n * where the cut falls writes a rationale that stops mid-sentence there.\n */\nconst atMost = (limit: number = AI_BUILD_PLAN_LIMITS.text) => `at most ${limit} characters`\n\n/**\n * The plan as a strict tool's input schema. Only the keywords strict\n * structured output accepts: every object closes its properties and lists\n * every one as required, an optional value is a `null` branch, and no\n * length or count bound appears as a keyword — those are enforced by\n * `parseAiBuildPlan`, which every answer passes through anyway.\n *\n * A text ceiling is stated in words instead, on each field the model writes.\n * A reference is copied from the inventory or from a creation's name, so its\n * length is not the model's to choose. A search title and description are\n * held by rule 10 to ceilings far inside the reader's, which the doctrine\n * states, so naming the reader's here would contradict the rule.\n */\nexport const AI_BUILD_PLAN_TOOL: AiTool = {\n name: 'submit_build_plan',\n description:\n 'Submit the build plan: what the site already has that this job reuses, what it creates and why, and the pages it builds from them.',\n strict: true,\n inputSchema: {\n type: 'object',\n additionalProperties: false,\n required: ['reuse', 'create', 'screens'],\n properties: {\n reuse: {\n type: 'array',\n description: 'Records the site already has that this plan uses, by inventory id.',\n items: {\n type: 'object',\n additionalProperties: false,\n required: ['kind', 'id', 'purpose'],\n properties: {\n kind: { type: 'string', enum: [...AI_BUILD_PLAN_REUSE_KINDS] },\n id: string('The id from the site inventory.'),\n purpose: string(`What the plan uses it for, in a few words, ${atMost()}.`),\n },\n },\n },\n create: {\n type: 'array',\n description:\n 'What the plan creates. Creating is the exception: each entry says why nothing listed will do.',\n items: {\n type: 'object',\n additionalProperties: false,\n required: ['kind', 'name', 'why', 'duplicateOf', 'fields'],\n properties: {\n kind: { type: 'string', enum: [...AI_BUILD_PLAN_CREATE_KINDS] },\n name: string(\n `Unique within the plan, ${atMost()}; other entries refer to it as new:<name>.`,\n ),\n why: string(`Why nothing the site already has will do, in one sentence of ${atMost()}.`),\n duplicateOf: nullableString(\n 'The inventory id this starts from as a duplicate, or null.',\n ),\n fields: strings(\n `A component: its props as name:type, an icon as name:icon. A layout: the regions it has, each one of header, nav, sidebar, main, footer — list every region the finished layout has, not only the ones it gains. A form or dataset: its field names. A theme change: the palette paths it adds or changes. Each ${atMost()}.`,\n ),\n },\n },\n },\n screens: {\n type: 'array',\n description: 'The pages the job builds, each a new draft.',\n items: {\n type: 'object',\n additionalProperties: false,\n required: [\n 'title',\n 'slug',\n 'layout',\n 'template',\n 'duplicateOf',\n 'nav',\n 'seoTitle',\n 'seoDescription',\n 'sections',\n ],\n properties: {\n title: string(`The page name, ${atMost()}.`),\n slug: string(\n `The path, ${atMost()}: lowercase words joined by hyphens, e.g. /services/roof-repair.`,\n ),\n layout: nullableString(\n 'The layout it renders inside: an inventory id or new:<name>.',\n ),\n template: nullableString(\n 'The template it applies (inventory id or new:<name>), or null for a one-off page.',\n ),\n duplicateOf: nullableString(\n 'The inventory page it starts from as a duplicate, or null.',\n ),\n nav: {\n type: 'boolean',\n description: 'Whether the site navigation gains an entry for it.',\n },\n seoTitle: string('The search-result title.'),\n seoDescription: string('The search-result description.'),\n sections: {\n type: 'array',\n description: 'The sections, top to bottom.',\n items: {\n type: 'object',\n additionalProperties: false,\n required: ['name', 'uses', 'items'],\n properties: {\n name: string(`A few words, ${atMost()}: hero, services grid, contact form.`),\n uses: strings('What the section places: inventory ids or new:<name>.'),\n items: {\n type: 'integer',\n description: 'How many repeated items it shows; 0 when none.',\n },\n },\n },\n },\n },\n },\n },\n },\n },\n}\n\n/** Words that ask for a video, or a link to one. */\nexport const AI_PLAN_ASKS_FOR_VIDEO = /\\b(?:videos?|films?|clips?|youtube|vimeo|player)\\b|youtu\\.be/i\n\nconst EMBEDS_SCHEMA = {\n type: 'array',\n description:\n 'A YouTube or Vimeo player the brief asks for, on a screen or a created component. Empty unless the brief asks; a library film is a Video.',\n items: {\n type: 'object',\n additionalProperties: false,\n required: ['host', 'where', 'asked', 'url'],\n properties: {\n host: { type: 'string', enum: [...AI_BUILD_PLAN_EMBED_HOSTS] },\n where: string('A screen slug, or new:<name> of a component.'),\n asked: string('The words of the brief that ask for it, copied exactly.'),\n url: nullableString('The video link the brief gives, or null.'),\n },\n },\n}\n\n/**\n * The plan tool with its list of third-party players, offered only to a brief\n * that asks for a video (AGL-3433). Every other plan's request is the plain\n * tool, byte for byte, so the list costs nothing on the plans that cannot use\n * it, and a model answering a brief that names no video has nowhere to plan a\n * player it was never asked for.\n */\nexport const AI_BUILD_PLAN_EMBEDS_TOOL: AiTool = {\n ...AI_BUILD_PLAN_TOOL,\n inputSchema: {\n ...AI_BUILD_PLAN_TOOL.inputSchema,\n required: [...(AI_BUILD_PLAN_TOOL.inputSchema['required'] as string[]), 'embeds'],\n properties: {\n ...(AI_BUILD_PLAN_TOOL.inputSchema['properties'] as Record<string, unknown>),\n embeds: EMBEDS_SCHEMA,\n },\n },\n}\n\n/** The plan tool a brief is answered with. */\nexport function aiBuildPlanToolFor(brief: string): AiTool {\n return AI_PLAN_ASKS_FOR_VIDEO.test(brief) ? AI_BUILD_PLAN_EMBEDS_TOOL : AI_BUILD_PLAN_TOOL\n}\n\nexport type AiBuildPlanParse =\n | { ok: true; plan: AiBuildPlan; repairs: string[] }\n | { ok: false; error: string }\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\nclass PlanShapeError extends Error {}\n\n/**\n * Read a plan out of a tool call, or name what makes it unreadable.\n *\n * Structure is refused — a missing list, an unknown kind, more screens than\n * a job holds, two creations under one name — because a plan that reads\n * differently from what the model meant is worse than a re-ask. Length is\n * repaired: copy past a ceiling is cut and the cut is listed.\n */\nexport function parseAiBuildPlan(input: unknown): AiBuildPlanParse {\n const repairs: string[] = []\n const text = (\n value: unknown,\n path: string,\n limit: number = AI_BUILD_PLAN_LIMITS.text,\n ): string => {\n if (typeof value !== 'string') throw new PlanShapeError(`${path} is not text`)\n const trimmed = value.trim()\n if (trimmed.length > limit) {\n repairs.push(`${path} was over ${limit} characters; truncated`)\n // On the last word break inside the ceiling, not through a word: a\n // rationale is read by the member confirming the plan, and half a word\n // reads as a broken answer rather than a long one (AGL-3022). A run of\n // `limit` characters with no break in it still has to be cut where the\n // ceiling falls.\n const cut = trimmed.slice(0, limit)\n const brk = cut.search(/\\s\\S*$/)\n return (brk > 0 ? cut.slice(0, brk) : cut).trimEnd()\n }\n return trimmed\n }\n const nullable = (value: unknown, path: string): string | null =>\n value === null || value === undefined || value === '' ? null : text(value, path) || null\n const list = (value: unknown, path: string, limit: number): unknown[] => {\n if (!Array.isArray(value)) throw new PlanShapeError(`${path} is not a list`)\n if (value.length > limit) {\n throw new PlanShapeError(`${path} lists ${value.length}; one plan holds at most ${limit}`)\n }\n return value\n }\n const record = (value: unknown, path: string): Record<string, unknown> => {\n if (!isRecord(value)) throw new PlanShapeError(`${path} is not an object`)\n return value\n }\n const oneOf = <T extends string>(value: unknown, allowed: readonly T[], path: string): T => {\n if (typeof value !== 'string' || !(allowed as readonly string[]).includes(value)) {\n throw new PlanShapeError(`${path} is not one of ${allowed.join(', ')}`)\n }\n return value as T\n }\n\n try {\n const root = record(input, 'plan')\n const reuse = list(root['reuse'], 'reuse', AI_BUILD_PLAN_LIMITS.reuse).map((raw, index) => {\n const entry = record(raw, `reuse[${index}]`)\n const id = text(entry['id'], `reuse[${index}].id`)\n if (!id) throw new PlanShapeError(`reuse[${index}].id is empty`)\n return {\n kind: oneOf(entry['kind'], AI_BUILD_PLAN_REUSE_KINDS, `reuse[${index}].kind`),\n id,\n purpose: text(entry['purpose'], `reuse[${index}].purpose`),\n }\n })\n const names = new Set<string>()\n const create = list(root['create'], 'create', AI_BUILD_PLAN_LIMITS.create).map(\n (raw, index) => {\n const entry = record(raw, `create[${index}]`)\n const name = text(entry['name'], `create[${index}].name`)\n if (!name) throw new PlanShapeError(`create[${index}].name is empty`)\n const key = name.toLowerCase()\n if (names.has(key)) {\n throw new PlanShapeError(`create[${index}].name \"${name}\" is used twice`)\n }\n names.add(key)\n return {\n kind: oneOf(entry['kind'], AI_BUILD_PLAN_CREATE_KINDS, `create[${index}].kind`),\n name,\n why: text(entry['why'], `create[${index}].why`),\n duplicateOf: nullable(entry['duplicateOf'], `create[${index}].duplicateOf`),\n fields: list(entry['fields'] ?? [], `create[${index}].fields`, AI_BUILD_PLAN_LIMITS.fields)\n .map((field, fieldIndex) => text(field, `create[${index}].fields[${fieldIndex}]`))\n .filter(Boolean),\n }\n },\n )\n const screens = list(root['screens'], 'screens', AI_BUILD_PLAN_LIMITS.screens).map(\n (raw, index) => {\n const entry = record(raw, `screens[${index}]`)\n if (typeof entry['nav'] !== 'boolean') {\n throw new PlanShapeError(`screens[${index}].nav is not true or false`)\n }\n return {\n title: text(entry['title'], `screens[${index}].title`),\n slug: text(entry['slug'], `screens[${index}].slug`),\n layout: nullable(entry['layout'], `screens[${index}].layout`),\n template: nullable(entry['template'], `screens[${index}].template`),\n duplicateOf: nullable(entry['duplicateOf'], `screens[${index}].duplicateOf`),\n nav: entry['nav'],\n seoTitle: text(entry['seoTitle'], `screens[${index}].seoTitle`),\n seoDescription: text(\n entry['seoDescription'],\n `screens[${index}].seoDescription`,\n AI_BUILD_PLAN_LIMITS.seoDescription,\n ),\n sections: list(\n entry['sections'],\n `screens[${index}].sections`,\n AI_BUILD_PLAN_LIMITS.sections,\n ).map((rawSection, sectionIndex) => {\n const path = `screens[${index}].sections[${sectionIndex}]`\n const section = record(rawSection, path)\n const items = Number(section['items'] ?? 0)\n if (!Number.isInteger(items) || items < 0 || items > AI_BUILD_PLAN_LIMITS.items) {\n throw new PlanShapeError(`${path}.items is not a count`)\n }\n return {\n name: text(section['name'], `${path}.name`),\n uses: list(section['uses'] ?? [], `${path}.uses`, AI_BUILD_PLAN_LIMITS.uses)\n .map((ref, refIndex) => text(ref, `${path}.uses[${refIndex}]`))\n .filter(Boolean),\n items,\n }\n }),\n }\n },\n )\n const embeds = list(root['embeds'] ?? [], 'embeds', AI_BUILD_PLAN_LIMITS.embeds).map(\n (raw, index) => {\n const entry = record(raw, `embeds[${index}]`)\n const where = text(entry['where'], `embeds[${index}].where`)\n if (!where) throw new PlanShapeError(`embeds[${index}].where is empty`)\n return {\n host: oneOf(entry['host'], AI_BUILD_PLAN_EMBED_HOSTS, `embeds[${index}].host`),\n where,\n asked: text(entry['asked'], `embeds[${index}].asked`),\n url: nullable(entry['url'], `embeds[${index}].url`),\n }\n },\n )\n // A plan whose brief asks for no player carries no list, as it did before the list existed.\n return {\n ok: true,\n plan: { reuse, create, screens, ...(embeds.length ? { embeds } : {}) },\n repairs,\n }\n } catch (error) {\n if (error instanceof PlanShapeError) return { ok: false, error: error.message }\n throw error\n }\n}\n\n/** Whether a reference names something the plan creates. */\nexport function isAiPlanNewRef(ref: string | null | undefined): ref is string {\n return typeof ref === 'string' && ref.startsWith(AI_PLAN_NEW_REF_PREFIX)\n}\n\n/** The creation a `new:<name>` reference names, case-insensitively. */\nexport function aiPlanCreateFor(\n plan: AiBuildPlan,\n ref: string,\n): AiBuildPlanCreate | undefined {\n if (!isAiPlanNewRef(ref)) return undefined\n const name = ref.slice(AI_PLAN_NEW_REF_PREFIX.length).trim().toLowerCase()\n return plan.create.find((entry) => entry.name.toLowerCase() === name)\n}\n\n/** A slug as two spellings of one path compare: lowercase, one leading slash, no trailing one. */\nexport function aiPlanSlugKey(slug: string): string {\n const path = slug.trim().toLowerCase().replace(/^\\/*/, '/').replace(/\\/+$/, '')\n return path || '/'\n}\n\n/**\n * The players the plan lists for one output (AGL-3433): a screen by its slug,\n * or a creation by its name. A build admits these and no other.\n */\nexport function aiPlanEmbedsFor(\n plan: Pick<AiBuildPlan, 'embeds'> | null | undefined,\n target: { slug: string } | { create: string },\n): AiBuildPlanEmbed[] {\n const key =\n 'slug' in target\n ? aiPlanSlugKey(target.slug)\n : `${AI_PLAN_NEW_REF_PREFIX}${target.create.trim().toLowerCase()}`\n return (plan?.embeds ?? []).filter((embed) =>\n 'slug' in target\n ? !isAiPlanNewRef(embed.where) && aiPlanSlugKey(embed.where) === key\n : embed.where.trim().toLowerCase() === key,\n )\n}\n\n/** The host a video link plays from, or null for a link neither host serves. */\nexport function aiEmbedHostOf(url: string): AiBuildPlanEmbedHost | null {\n try {\n const parsed = new URL(url.trim())\n if (parsed.protocol !== 'https:') return null\n const host = parsed.hostname.replace(/^(?:www|m)\\./, '')\n if (host === 'youtu.be' || host === 'youtube.com' || host === 'youtube-nocookie.com') {\n return 'youtube'\n }\n if (host === 'vimeo.com' || host === 'player.vimeo.com') return 'vimeo'\n } catch {\n /* not a link */\n }\n return null\n}\n\n/**\n * One video however its link is spelled — `youtu.be/<id>`, a watch link, an\n * embed or a Shorts link — as `<host>:<id>`, the way the Video embed element\n * reads a link; null for a link it cannot play.\n */\nexport function aiEmbedVideoKey(url: string): string | null {\n const host = aiEmbedHostOf(url)\n if (!host) return null\n const parsed = new URL(url.trim())\n const parts = parsed.pathname.split('/').filter(Boolean)\n const id =\n host === 'vimeo'\n ? parts.find((part) => /^\\d{6,}$/.test(part))\n : parsed.hostname.endsWith('youtu.be')\n ? parts[0]\n : (parsed.searchParams.get('v') ??\n (parts[0] === 'embed' || parts[0] === 'shorts' ? parts[1] : undefined))\n return id && /^[\\w-]{6,20}$/.test(id) ? `${host}:${id}` : null\n}\n\n/**\n * A `new:<name>` reference that names no entry of the plan's create list: a\n * screen's own layout or template, or what one of its sections places, with\n * that section's index.\n */\nexport type AiPlanUndeclaredRef = {\n /** The name the reference gives the creation, as written after `new:`. */\n name: string\n /** Where the plan writes it: `screens[0].template`, `screens[0].sections[4].uses[0]`. */\n path: string\n /** The screen that writes it, by index. */\n screenIndex: number\n} & ({ field: 'layout' | 'template'; sectionIndex: null } | { field: 'uses'; sectionIndex: number })\n\n/**\n * Every `new:<name>` reference the plan's create list does not carry, in plan\n * order: each screen's layout, then its template, then what its sections\n * place. A plan that places a creation it never declares has nothing to build\n * it from, so the plan rules refuse each (AGL-3040), and the page and\n * scaffold doors still name one in a plan confirmed before they did.\n */\nexport function aiPlanUndeclaredRefs(plan: AiBuildPlan): AiPlanUndeclaredRef[] {\n const found: AiPlanUndeclaredRef[] = []\n const undeclared = (ref: string | null): ref is string =>\n isAiPlanNewRef(ref) && !aiPlanCreateFor(plan, ref)\n const nameOf = (ref: string) => ref.slice(AI_PLAN_NEW_REF_PREFIX.length).trim()\n plan.screens.forEach((screen, screenIndex) => {\n for (const field of ['layout', 'template'] as const) {\n const ref = screen[field]\n if (!undeclared(ref)) continue\n found.push({\n name: nameOf(ref),\n path: `screens[${screenIndex}].${field}`,\n screenIndex,\n field,\n sectionIndex: null,\n })\n }\n screen.sections.forEach((section, sectionIndex) => {\n section.uses.forEach((ref, useIndex) => {\n if (!undeclared(ref)) return\n found.push({\n name: nameOf(ref),\n path: `screens[${screenIndex}].sections[${sectionIndex}].uses[${useIndex}]`,\n screenIndex,\n field: 'uses',\n sectionIndex,\n })\n })\n })\n })\n return found\n}\n"],"names":["AI_PLAN_NEW_REF_PREFIX","AI_BUILD_PLAN_REUSE_KINDS","AI_BUILD_PLAN_CREATION_NOUNS","component","noun","where","form","layout","template","dataset","email","AI_BUILD_PLAN_CREATE_KINDS","AI_BUILD_PLAN_EMBED_HOSTS","AI_BUILD_PLAN_EMBED_HOST_NAMES","youtube","vimeo","AI_BUILD_PLAN_LIMITS","reuse","create","screens","sections","uses","fields","embeds","text","seoDescription","items","string","description","type","nullableString","anyOf","strings","atMost","limit","AI_BUILD_PLAN_TOOL","name","strict","inputSchema","additionalProperties","required","properties","kind","enum","id","purpose","why","duplicateOf","title","slug","nav","seoTitle","AI_PLAN_ASKS_FOR_VIDEO","EMBEDS_SCHEMA","host","asked","url","AI_BUILD_PLAN_EMBEDS_TOOL","aiBuildPlanToolFor","brief","test","isRecord","value","Array","isArray","PlanShapeError","Error","parseAiBuildPlan","input","repairs","path","trimmed","trim","length","push","cut","slice","brk","search","trimEnd","nullable","undefined","list","record","oneOf","allowed","includes","join","root","map","raw","index","entry","names","Set","key","toLowerCase","has","add","field","fieldIndex","filter","Boolean","rawSection","sectionIndex","section","Number","isInteger","ref","refIndex","ok","plan","error","message","isAiPlanNewRef","startsWith","aiPlanCreateFor","find","aiPlanSlugKey","replace","aiPlanEmbedsFor","target","embed","aiEmbedHostOf","parsed","URL","protocol","hostname","aiEmbedVideoKey","parts","pathname","split","part","endsWith","searchParams","get","aiPlanUndeclaredRefs","found","undeclared","nameOf","forEach","screen","screenIndex","useIndex"],"mappings":"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"}
|
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
|
+
import type { SeoAuditReport } from '@aglyn/aglyn/app-utils/seo-audit';
|
|
18
|
+
import type { SeoKeywordCoverage } from '@aglyn/aglyn/app-utils/seo-keywords';
|
|
17
19
|
import { type SeoListingFieldKey } from '@aglyn/aglyn/app-utils/seo-listing-fields';
|
|
18
20
|
import type { AiJobOutput, AiJobSummary } from './ai-jobs.types';
|
|
19
21
|
/**
|
|
@@ -34,8 +36,9 @@ import type { AiJobOutput, AiJobSummary } from './ai-jobs.types';
|
|
|
34
36
|
* ## The outputs of a job
|
|
35
37
|
*
|
|
36
38
|
* - `fields:{screen|product}:{id}` — one listing's values ({@link AiSeoFieldsProposal}).
|
|
37
|
-
* - `audit:report` — the site audit:
|
|
38
|
-
* its score
|
|
39
|
+
* - `audit:report` — the site audit: the platform SEO check's report (every
|
|
40
|
+
* audited page, its findings and its score) and which pages need generated
|
|
41
|
+
* fixes ({@link AiSeoAuditReport}).
|
|
39
42
|
* - `audit:site` — structured data for the site's entity and the agent
|
|
40
43
|
* guidance `/llms.txt` leads with ({@link AiSeoSiteBatch}).
|
|
41
44
|
* - `audit:fixes:{n}` — one batch of page fixes ({@link AiSeoFixesBatch}).
|
|
@@ -47,14 +50,6 @@ export declare const AI_SEO_TARGETS: readonly AiSeoTarget[];
|
|
|
47
50
|
export declare function aiSeoJobTarget(inputs: Record<string, unknown> | null | undefined): AiSeoTarget | null;
|
|
48
51
|
/** Proposed values for one listing, by field. */
|
|
49
52
|
export type AiSeoFieldValues = Partial<Record<SeoListingFieldKey, string>>;
|
|
50
|
-
/** Whether, and where, a page already says a target keyword. */
|
|
51
|
-
export interface AiSeoKeywordCoverage {
|
|
52
|
-
keyword: string;
|
|
53
|
-
inTitle: boolean;
|
|
54
|
-
inDescription: boolean;
|
|
55
|
-
inH1: boolean;
|
|
56
|
-
inBody: boolean;
|
|
57
|
-
}
|
|
58
53
|
/** A proposal for one page's or one product's search listing. */
|
|
59
54
|
export interface AiSeoFieldsProposal {
|
|
60
55
|
kind: 'fields';
|
|
@@ -68,55 +63,22 @@ export interface AiSeoFieldsProposal {
|
|
|
68
63
|
};
|
|
69
64
|
values: AiSeoFieldValues;
|
|
70
65
|
/** The target keywords the listing was asked to cover, and where it now does. */
|
|
71
|
-
keywords:
|
|
66
|
+
keywords: SeoKeywordCoverage[];
|
|
72
67
|
/** Customer-safe lines worth knowing (a field left out, and why). */
|
|
73
68
|
notes: string[];
|
|
74
69
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
/** One customer-safe sentence. */
|
|
81
|
-
message: string;
|
|
82
|
-
/** The elements it is about: the images without a description, the extra headings. */
|
|
83
|
-
nodeIds?: string[];
|
|
84
|
-
/** The other pages it involves: a duplicate's twins. */
|
|
85
|
-
screenIds?: string[];
|
|
86
|
-
/** The target keyword, for `keyword-missing`. */
|
|
87
|
-
keyword?: string;
|
|
88
|
-
}
|
|
89
|
-
/** One audited page. */
|
|
90
|
-
export interface AiSeoPageReport {
|
|
91
|
-
screenId: string;
|
|
92
|
-
/** The public path, `/` for the home page. */
|
|
93
|
-
path: string;
|
|
94
|
-
name: string;
|
|
95
|
-
/** The published version the page was audited on. */
|
|
96
|
-
versionId: string | null;
|
|
97
|
-
/** 100 with nothing found, less for each finding by its severity. */
|
|
98
|
-
score: number;
|
|
99
|
-
findings: AiSeoFinding[];
|
|
100
|
-
keywords: AiSeoKeywordCoverage[];
|
|
101
|
-
}
|
|
102
|
-
/** The site audit: every audited page, what was found, and what still needs generating. */
|
|
103
|
-
export interface AiSeoAuditReport {
|
|
70
|
+
/**
|
|
71
|
+
* The site audit a job records: the platform's SEO check, whose findings any
|
|
72
|
+
* site owner sees without this plugin, plus what the job owes on top of it.
|
|
73
|
+
*/
|
|
74
|
+
export interface AiSeoAuditReport extends SeoAuditReport {
|
|
104
75
|
kind: 'audit';
|
|
105
|
-
pages: AiSeoPageReport[];
|
|
106
|
-
/** Published pages past the audit's page cap, not audited. */
|
|
107
|
-
skipped: number;
|
|
108
|
-
/** Findings about the site rather than a page. */
|
|
109
|
-
site: AiSeoFinding[];
|
|
110
76
|
/** Whether the site-wide proposal (structured data, agent guidance) is owed. */
|
|
111
77
|
siteProposal: boolean;
|
|
112
78
|
/** Pages whose fixes need generated text, in the order the batches take them. */
|
|
113
79
|
queue: string[];
|
|
114
80
|
/** How many pages one batch of fixes carries. */
|
|
115
81
|
batchSize: number;
|
|
116
|
-
/** The average page score. */
|
|
117
|
-
score: number;
|
|
118
|
-
/** Customer-safe lines about the audit itself: a keyword line naming no page, say. */
|
|
119
|
-
notes: string[];
|
|
120
82
|
}
|
|
121
83
|
/** A content fix: applied into a NEW version of the page, never the published one. */
|
|
122
84
|
export type AiSeoContentFix =
|
|
@@ -182,8 +144,6 @@ export declare const AI_SEO_OUTPUT_IDS: {
|
|
|
182
144
|
readonly fixes: (batch: number) => string;
|
|
183
145
|
readonly fields: (kind: "screen" | "product", id: string | null) => string;
|
|
184
146
|
};
|
|
185
|
-
/** What each finding is called where it is listed. */
|
|
186
|
-
export declare const AI_SEO_FINDING_LABELS: Record<AiSeoFindingCode, string>;
|
|
187
147
|
/** A stored proposal, or `null` for anything that is not one. */
|
|
188
148
|
export declare function readAiSeoProposal(value: unknown): AiSeoProposal | null;
|
|
189
149
|
/** One site audit, put back together from a job's outputs. */
|
package/src/lib/model/ai-seo.js
CHANGED
|
@@ -41,23 +41,6 @@ export const AI_SEO_TARGETS = [
|
|
|
41
41
|
fixes: (batch)=>`audit:fixes:${batch}`,
|
|
42
42
|
fields: (kind, id)=>`fields:${kind}:${id != null ? id : 'new'}`
|
|
43
43
|
};
|
|
44
|
-
/** What each finding is called where it is listed. */ export const AI_SEO_FINDING_LABELS = {
|
|
45
|
-
'title-missing': 'No search title',
|
|
46
|
-
'title-too-long': 'Title too long',
|
|
47
|
-
'title-duplicate': 'Title used on another page',
|
|
48
|
-
'description-missing': 'No search description',
|
|
49
|
-
'description-too-long': 'Description too long',
|
|
50
|
-
'description-duplicate': 'Description used on another page',
|
|
51
|
-
'h1-missing': 'No main heading',
|
|
52
|
-
'h1-multiple': 'More than one main heading',
|
|
53
|
-
'h1-thin': 'Main heading says little',
|
|
54
|
-
'image-alt-missing': 'Images without a description',
|
|
55
|
-
orphan: 'No page links here',
|
|
56
|
-
'keyword-missing': 'Target keyword not covered',
|
|
57
|
-
'search-discouraged': 'Search engines are discouraged',
|
|
58
|
-
'entity-incomplete': 'Structured data incomplete',
|
|
59
|
-
'llms-guidance-missing': 'No guidance for AI agents'
|
|
60
|
-
};
|
|
61
44
|
const isRecord = (value)=>typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
62
45
|
const strings = (value)=>Array.isArray(value) ? value.filter((entry)=>typeof entry === 'string') : [];
|
|
63
46
|
function readValues(value) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-seo.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n isSeoListingFieldKey,\n type SeoListingFieldKey,\n} from '@aglyn/aglyn/app-utils/seo-listing-fields'\nimport type { AiJobOutput, AiJobSummary } from './ai-jobs.types'\n\n/**\n * SEO by AI (AGL-2910): the proposals an `seo` job produces, in the shape\n * both the job step and the console cards read. Pure data and readers — no\n * server import — so a card can type what it renders from the same module\n * the step writes it with.\n *\n * ## Proposals, never writes\n *\n * Every value here is a PROPOSAL. A page's search listing is served from the\n * screen document, so a job never writes it: the values are staged in the\n * page's SEO card and saved there. A site audit's content fixes — an image\n * description, a page's one `h1` — become a new unpublished version only\n * when a person applies them, and the site-wide proposals land in the SEO\n * form unsaved. Nothing a job produces changes what a published site serves.\n *\n * ## The outputs of a job\n *\n * - `fields:{screen|product}:{id}` — one listing's values ({@link AiSeoFieldsProposal}).\n * - `audit:report` — the site audit: every audited page, its findings and\n * its score, and which pages need generated fixes ({@link AiSeoAuditReport}).\n * - `audit:site` — structured data for the site's entity and the agent\n * guidance `/llms.txt` leads with ({@link AiSeoSiteBatch}).\n * - `audit:fixes:{n}` — one batch of page fixes ({@link AiSeoFixesBatch}).\n */\n\n/** What an `seo` job is about, as its `inputs.target` names it. */\nexport type AiSeoTarget = 'screen' | 'product' | 'site'\n\nexport const AI_SEO_TARGETS: readonly AiSeoTarget[] = ['screen', 'product', 'site']\n\n/** What an `seo` job is about, or `null` when its inputs name nothing an SEO job writes. */\nexport function aiSeoJobTarget(inputs: Record<string, unknown> | null | undefined): AiSeoTarget | null {\n const target = inputs?.['target']\n return typeof target === 'string' && (AI_SEO_TARGETS as readonly string[]).includes(target)\n ? (target as AiSeoTarget)\n : null\n}\n\n/** Proposed values for one listing, by field. */\nexport type AiSeoFieldValues = Partial<Record<SeoListingFieldKey, string>>\n\n/** Whether, and where, a page already says a target keyword. */\nexport interface AiSeoKeywordCoverage {\n keyword: string\n inTitle: boolean\n inDescription: boolean\n inH1: boolean\n inBody: boolean\n}\n\n/** A proposal for one page's or one product's search listing. */\nexport interface AiSeoFieldsProposal {\n kind: 'fields'\n subject: {\n kind: 'screen' | 'product'\n /** `null` for a product not saved yet. */\n id: string | null\n name: string\n /** The page's public path, for a screen. */\n path?: string | null\n }\n values: AiSeoFieldValues\n /** The target keywords the listing was asked to cover, and where it now does. */\n keywords: AiSeoKeywordCoverage[]\n /** Customer-safe lines worth knowing (a field left out, and why). */\n notes: string[]\n}\n\nexport type AiSeoFindingCode =\n | 'title-missing'\n | 'title-too-long'\n | 'title-duplicate'\n | 'description-missing'\n | 'description-too-long'\n | 'description-duplicate'\n | 'h1-missing'\n | 'h1-multiple'\n | 'h1-thin'\n | 'image-alt-missing'\n | 'orphan'\n | 'keyword-missing'\n | 'search-discouraged'\n | 'entity-incomplete'\n | 'llms-guidance-missing'\n\nexport type AiSeoSeverity = 'high' | 'medium' | 'low'\n\nexport interface AiSeoFinding {\n code: AiSeoFindingCode\n severity: AiSeoSeverity\n /** One customer-safe sentence. */\n message: string\n /** The elements it is about: the images without a description, the extra headings. */\n nodeIds?: string[]\n /** The other pages it involves: a duplicate's twins. */\n screenIds?: string[]\n /** The target keyword, for `keyword-missing`. */\n keyword?: string\n}\n\n/** One audited page. */\nexport interface AiSeoPageReport {\n screenId: string\n /** The public path, `/` for the home page. */\n path: string\n name: string\n /** The published version the page was audited on. */\n versionId: string | null\n /** 100 with nothing found, less for each finding by its severity. */\n score: number\n findings: AiSeoFinding[]\n keywords: AiSeoKeywordCoverage[]\n}\n\n/** The site audit: every audited page, what was found, and what still needs generating. */\nexport interface AiSeoAuditReport {\n kind: 'audit'\n pages: AiSeoPageReport[]\n /** Published pages past the audit's page cap, not audited. */\n skipped: number\n /** Findings about the site rather than a page. */\n site: AiSeoFinding[]\n /** Whether the site-wide proposal (structured data, agent guidance) is owed. */\n siteProposal: boolean\n /** Pages whose fixes need generated text, in the order the batches take them. */\n queue: string[]\n /** How many pages one batch of fixes carries. */\n batchSize: number\n /** The average page score. */\n score: number\n /** Customer-safe lines about the audit itself: a keyword line naming no page, say. */\n notes: string[]\n}\n\n/** A content fix: applied into a NEW version of the page, never the published one. */\nexport type AiSeoContentFix =\n /** An image with no description gets one. */\n | { kind: 'image-alt'; nodeId: string; alt: string }\n /** An existing heading becomes the page's `h1`, with this text. */\n | { kind: 'h1-set'; nodeId: string; text: string }\n /** A page with no heading to promote gets a new `h1` at the top of its content. */\n | { kind: 'h1-insert'; text: string }\n /** An extra `h1` becomes an `h2`, so the page keeps one. */\n | { kind: 'h1-demote'; nodeId: string }\n\n/** Everything proposed for one page. */\nexport interface AiSeoPageFix {\n screenId: string\n /** Listing values, staged in the page's SEO card and saved there. */\n values: AiSeoFieldValues\n content: AiSeoContentFix[]\n /** Findings with no fix a job can make — an orphan needs a person's link. */\n guidance: string[]\n}\n\nexport interface AiSeoFixesBatch {\n kind: 'fixes'\n /** 1-based. */\n batch: number\n fixes: AiSeoPageFix[]\n notes: string[]\n}\n\n/** The site's structured-data fields a proposal may fill, by form field name. */\nexport const AI_SEO_ENTITY_FORM_FIELDS = [\n 'seo.entity.type',\n 'seo.entity.name',\n 'seo.entity.description',\n 'seo.entity.email',\n 'seo.entity.telephone',\n 'seo.entity.contactType',\n] as const\n\n/** The agent guidance fields `/llms.txt` leads with, by form field name. */\nexport const AI_SEO_AGENT_FORM_FIELDS = ['seo.agent.whenToUse', 'seo.agent.howToUse'] as const\n\nexport type AiSeoSiteFormField =\n | (typeof AI_SEO_ENTITY_FORM_FIELDS)[number]\n | (typeof AI_SEO_AGENT_FORM_FIELDS)[number]\n\nexport interface AiSeoSiteProposal {\n /** Values for the site SEO form, by field name; only what the site's pages support. */\n values: Partial<Record<AiSeoSiteFormField, string>>\n /** `/llms.txt` as it would read once the guidance is saved. */\n llmsPreview: string\n notes: string[]\n}\n\nexport interface AiSeoSiteBatch {\n kind: 'site'\n site: AiSeoSiteProposal\n}\n\nexport type AiSeoProposal =\n | AiSeoFieldsProposal\n | AiSeoAuditReport\n | AiSeoFixesBatch\n | AiSeoSiteBatch\n\n/** Output ids, one place. */\nexport const AI_SEO_OUTPUT_IDS = {\n report: 'audit:report',\n site: 'audit:site',\n fixes: (batch: number) => `audit:fixes:${batch}`,\n fields: (kind: 'screen' | 'product', id: string | null) => `fields:${kind}:${id ?? 'new'}`,\n} as const\n\n/** What each finding is called where it is listed. */\nexport const AI_SEO_FINDING_LABELS: Record<AiSeoFindingCode, string> = {\n 'title-missing': 'No search title',\n 'title-too-long': 'Title too long',\n 'title-duplicate': 'Title used on another page',\n 'description-missing': 'No search description',\n 'description-too-long': 'Description too long',\n 'description-duplicate': 'Description used on another page',\n 'h1-missing': 'No main heading',\n 'h1-multiple': 'More than one main heading',\n 'h1-thin': 'Main heading says little',\n 'image-alt-missing': 'Images without a description',\n orphan: 'No page links here',\n 'keyword-missing': 'Target keyword not covered',\n 'search-discouraged': 'Search engines are discouraged',\n 'entity-incomplete': 'Structured data incomplete',\n 'llms-guidance-missing': 'No guidance for AI agents',\n}\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value)\n\nconst strings = (value: unknown): string[] =>\n Array.isArray(value) ? value.filter((entry): entry is string => typeof entry === 'string') : []\n\nfunction readValues(value: unknown): AiSeoFieldValues {\n const out: AiSeoFieldValues = {}\n if (!isRecord(value)) return out\n for (const [key, entry] of Object.entries(value)) {\n if (isSeoListingFieldKey(key) && typeof entry === 'string') out[key] = entry\n }\n return out\n}\n\n/** A stored proposal, or `null` for anything that is not one. */\nexport function readAiSeoProposal(value: unknown): AiSeoProposal | null {\n if (!isRecord(value)) return null\n switch (value['kind']) {\n case 'fields': {\n const subject = isRecord(value['subject']) ? value['subject'] : null\n if (!subject || (subject['kind'] !== 'screen' && subject['kind'] !== 'product')) return null\n return {\n kind: 'fields',\n subject: {\n kind: subject['kind'],\n id: typeof subject['id'] === 'string' ? subject['id'] : null,\n name: String(subject['name'] ?? ''),\n path: typeof subject['path'] === 'string' ? subject['path'] : null,\n },\n values: readValues(value['values']),\n keywords: Array.isArray(value['keywords']) ? (value['keywords'] as AiSeoKeywordCoverage[]) : [],\n notes: strings(value['notes']),\n }\n }\n case 'audit':\n return Array.isArray(value['pages']) ? (value as unknown as AiSeoAuditReport) : null\n case 'fixes':\n return Array.isArray(value['fixes']) ? (value as unknown as AiSeoFixesBatch) : null\n case 'site':\n return isRecord(value['site']) ? (value as unknown as AiSeoSiteBatch) : null\n default:\n return null\n }\n}\n\n/** One site audit, put back together from a job's outputs. */\nexport interface AiSeoAuditView {\n report: AiSeoAuditReport\n /** Every page fix proposed so far, by screen id. */\n fixes: Record<string, AiSeoPageFix>\n site: AiSeoSiteProposal | null\n /** Batches recorded so far. */\n batches: number\n /** Whether every batch the report queued has been recorded. */\n complete: boolean\n}\n\n/** The audit a job's outputs describe, or `null` when they describe none. */\nexport function aiSeoAuditView(outputs: readonly AiJobOutput[]): AiSeoAuditView | null {\n let report: AiSeoAuditReport | null = null\n let site: AiSeoSiteProposal | null = null\n const fixes: Record<string, AiSeoPageFix> = {}\n let batches = 0\n for (const output of outputs) {\n if (output.resource !== 'seo') continue\n const proposal = readAiSeoProposal(output.proposal)\n if (!proposal) continue\n if (proposal.kind === 'audit') report = proposal\n else if (proposal.kind === 'site') site = proposal.site\n else if (proposal.kind === 'fixes') {\n batches += 1\n for (const fix of proposal.fixes) fixes[fix.screenId] = fix\n }\n }\n if (!report) return null\n const owedBatches = Math.ceil(report.queue.length / Math.max(1, report.batchSize))\n return {\n report,\n fixes,\n site,\n batches,\n complete: batches >= owedBatches && (!report.siteProposal || site !== null),\n }\n}\n\n/** The listing proposal a job produced, when it produced one. */\nexport function aiSeoFieldsProposalOf(job: Pick<AiJobSummary, 'outputs'> | null): AiSeoFieldsProposal | null {\n for (const output of job?.outputs ?? []) {\n if (output.resource !== 'seo') continue\n const proposal = readAiSeoProposal(output.proposal)\n if (proposal?.kind === 'fields') return proposal\n }\n return null\n}\n\n/** How many content fixes, and how many staged listings, an audit view would apply. */\nexport function aiSeoApplyCounts(view: AiSeoAuditView): { pagesWithContent: number; pagesWithValues: number } {\n let pagesWithContent = 0\n let pagesWithValues = 0\n for (const fix of Object.values(view.fixes)) {\n if (fix.content.length) pagesWithContent += 1\n if (Object.keys(fix.values).length) pagesWithValues += 1\n }\n return { pagesWithContent, pagesWithValues }\n}\n"],"names":["isSeoListingFieldKey","AI_SEO_TARGETS","aiSeoJobTarget","inputs","target","includes","AI_SEO_ENTITY_FORM_FIELDS","AI_SEO_AGENT_FORM_FIELDS","AI_SEO_OUTPUT_IDS","report","site","fixes","batch","fields","kind","id","AI_SEO_FINDING_LABELS","orphan","isRecord","value","Array","isArray","strings","filter","entry","readValues","out","key","Object","entries","readAiSeoProposal","subject","name","String","path","values","keywords","notes","aiSeoAuditView","outputs","batches","output","resource","proposal","fix","screenId","owedBatches","Math","ceil","queue","length","max","batchSize","complete","siteProposal","aiSeoFieldsProposalOf","job","aiSeoApplyCounts","view","pagesWithContent","pagesWithValues","content","keys"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-seo.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 { SeoAuditReport } from '@aglyn/aglyn/app-utils/seo-audit'\nimport type { SeoKeywordCoverage } from '@aglyn/aglyn/app-utils/seo-keywords'\nimport {\n isSeoListingFieldKey,\n type SeoListingFieldKey,\n} from '@aglyn/aglyn/app-utils/seo-listing-fields'\nimport type { AiJobOutput, AiJobSummary } from './ai-jobs.types'\n\n/**\n * SEO by AI (AGL-2910): the proposals an `seo` job produces, in the shape\n * both the job step and the console cards read. Pure data and readers — no\n * server import — so a card can type what it renders from the same module\n * the step writes it with.\n *\n * ## Proposals, never writes\n *\n * Every value here is a PROPOSAL. A page's search listing is served from the\n * screen document, so a job never writes it: the values are staged in the\n * page's SEO card and saved there. A site audit's content fixes — an image\n * description, a page's one `h1` — become a new unpublished version only\n * when a person applies them, and the site-wide proposals land in the SEO\n * form unsaved. Nothing a job produces changes what a published site serves.\n *\n * ## The outputs of a job\n *\n * - `fields:{screen|product}:{id}` — one listing's values ({@link AiSeoFieldsProposal}).\n * - `audit:report` — the site audit: the platform SEO check's report (every\n * audited page, its findings and its score) and which pages need generated\n * fixes ({@link AiSeoAuditReport}).\n * - `audit:site` — structured data for the site's entity and the agent\n * guidance `/llms.txt` leads with ({@link AiSeoSiteBatch}).\n * - `audit:fixes:{n}` — one batch of page fixes ({@link AiSeoFixesBatch}).\n */\n\n/** What an `seo` job is about, as its `inputs.target` names it. */\nexport type AiSeoTarget = 'screen' | 'product' | 'site'\n\nexport const AI_SEO_TARGETS: readonly AiSeoTarget[] = ['screen', 'product', 'site']\n\n/** What an `seo` job is about, or `null` when its inputs name nothing an SEO job writes. */\nexport function aiSeoJobTarget(inputs: Record<string, unknown> | null | undefined): AiSeoTarget | null {\n const target = inputs?.['target']\n return typeof target === 'string' && (AI_SEO_TARGETS as readonly string[]).includes(target)\n ? (target as AiSeoTarget)\n : null\n}\n\n/** Proposed values for one listing, by field. */\nexport type AiSeoFieldValues = Partial<Record<SeoListingFieldKey, string>>\n\n/** A proposal for one page's or one product's search listing. */\nexport interface AiSeoFieldsProposal {\n kind: 'fields'\n subject: {\n kind: 'screen' | 'product'\n /** `null` for a product not saved yet. */\n id: string | null\n name: string\n /** The page's public path, for a screen. */\n path?: string | null\n }\n values: AiSeoFieldValues\n /** The target keywords the listing was asked to cover, and where it now does. */\n keywords: SeoKeywordCoverage[]\n /** Customer-safe lines worth knowing (a field left out, and why). */\n notes: string[]\n}\n\n/**\n * The site audit a job records: the platform's SEO check, whose findings any\n * site owner sees without this plugin, plus what the job owes on top of it.\n */\nexport interface AiSeoAuditReport extends SeoAuditReport {\n kind: 'audit'\n /** Whether the site-wide proposal (structured data, agent guidance) is owed. */\n siteProposal: boolean\n /** Pages whose fixes need generated text, in the order the batches take them. */\n queue: string[]\n /** How many pages one batch of fixes carries. */\n batchSize: number\n}\n\n/** A content fix: applied into a NEW version of the page, never the published one. */\nexport type AiSeoContentFix =\n /** An image with no description gets one. */\n | { kind: 'image-alt'; nodeId: string; alt: string }\n /** An existing heading becomes the page's `h1`, with this text. */\n | { kind: 'h1-set'; nodeId: string; text: string }\n /** A page with no heading to promote gets a new `h1` at the top of its content. */\n | { kind: 'h1-insert'; text: string }\n /** An extra `h1` becomes an `h2`, so the page keeps one. */\n | { kind: 'h1-demote'; nodeId: string }\n\n/** Everything proposed for one page. */\nexport interface AiSeoPageFix {\n screenId: string\n /** Listing values, staged in the page's SEO card and saved there. */\n values: AiSeoFieldValues\n content: AiSeoContentFix[]\n /** Findings with no fix a job can make — an orphan needs a person's link. */\n guidance: string[]\n}\n\nexport interface AiSeoFixesBatch {\n kind: 'fixes'\n /** 1-based. */\n batch: number\n fixes: AiSeoPageFix[]\n notes: string[]\n}\n\n/** The site's structured-data fields a proposal may fill, by form field name. */\nexport const AI_SEO_ENTITY_FORM_FIELDS = [\n 'seo.entity.type',\n 'seo.entity.name',\n 'seo.entity.description',\n 'seo.entity.email',\n 'seo.entity.telephone',\n 'seo.entity.contactType',\n] as const\n\n/** The agent guidance fields `/llms.txt` leads with, by form field name. */\nexport const AI_SEO_AGENT_FORM_FIELDS = ['seo.agent.whenToUse', 'seo.agent.howToUse'] as const\n\nexport type AiSeoSiteFormField =\n | (typeof AI_SEO_ENTITY_FORM_FIELDS)[number]\n | (typeof AI_SEO_AGENT_FORM_FIELDS)[number]\n\nexport interface AiSeoSiteProposal {\n /** Values for the site SEO form, by field name; only what the site's pages support. */\n values: Partial<Record<AiSeoSiteFormField, string>>\n /** `/llms.txt` as it would read once the guidance is saved. */\n llmsPreview: string\n notes: string[]\n}\n\nexport interface AiSeoSiteBatch {\n kind: 'site'\n site: AiSeoSiteProposal\n}\n\nexport type AiSeoProposal =\n | AiSeoFieldsProposal\n | AiSeoAuditReport\n | AiSeoFixesBatch\n | AiSeoSiteBatch\n\n/** Output ids, one place. */\nexport const AI_SEO_OUTPUT_IDS = {\n report: 'audit:report',\n site: 'audit:site',\n fixes: (batch: number) => `audit:fixes:${batch}`,\n fields: (kind: 'screen' | 'product', id: string | null) => `fields:${kind}:${id ?? 'new'}`,\n} as const\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value)\n\nconst strings = (value: unknown): string[] =>\n Array.isArray(value) ? value.filter((entry): entry is string => typeof entry === 'string') : []\n\nfunction readValues(value: unknown): AiSeoFieldValues {\n const out: AiSeoFieldValues = {}\n if (!isRecord(value)) return out\n for (const [key, entry] of Object.entries(value)) {\n if (isSeoListingFieldKey(key) && typeof entry === 'string') out[key] = entry\n }\n return out\n}\n\n/** A stored proposal, or `null` for anything that is not one. */\nexport function readAiSeoProposal(value: unknown): AiSeoProposal | null {\n if (!isRecord(value)) return null\n switch (value['kind']) {\n case 'fields': {\n const subject = isRecord(value['subject']) ? value['subject'] : null\n if (!subject || (subject['kind'] !== 'screen' && subject['kind'] !== 'product')) return null\n return {\n kind: 'fields',\n subject: {\n kind: subject['kind'],\n id: typeof subject['id'] === 'string' ? subject['id'] : null,\n name: String(subject['name'] ?? ''),\n path: typeof subject['path'] === 'string' ? subject['path'] : null,\n },\n values: readValues(value['values']),\n keywords: Array.isArray(value['keywords']) ? (value['keywords'] as SeoKeywordCoverage[]) : [],\n notes: strings(value['notes']),\n }\n }\n case 'audit':\n return Array.isArray(value['pages']) ? (value as unknown as AiSeoAuditReport) : null\n case 'fixes':\n return Array.isArray(value['fixes']) ? (value as unknown as AiSeoFixesBatch) : null\n case 'site':\n return isRecord(value['site']) ? (value as unknown as AiSeoSiteBatch) : null\n default:\n return null\n }\n}\n\n/** One site audit, put back together from a job's outputs. */\nexport interface AiSeoAuditView {\n report: AiSeoAuditReport\n /** Every page fix proposed so far, by screen id. */\n fixes: Record<string, AiSeoPageFix>\n site: AiSeoSiteProposal | null\n /** Batches recorded so far. */\n batches: number\n /** Whether every batch the report queued has been recorded. */\n complete: boolean\n}\n\n/** The audit a job's outputs describe, or `null` when they describe none. */\nexport function aiSeoAuditView(outputs: readonly AiJobOutput[]): AiSeoAuditView | null {\n let report: AiSeoAuditReport | null = null\n let site: AiSeoSiteProposal | null = null\n const fixes: Record<string, AiSeoPageFix> = {}\n let batches = 0\n for (const output of outputs) {\n if (output.resource !== 'seo') continue\n const proposal = readAiSeoProposal(output.proposal)\n if (!proposal) continue\n if (proposal.kind === 'audit') report = proposal\n else if (proposal.kind === 'site') site = proposal.site\n else if (proposal.kind === 'fixes') {\n batches += 1\n for (const fix of proposal.fixes) fixes[fix.screenId] = fix\n }\n }\n if (!report) return null\n const owedBatches = Math.ceil(report.queue.length / Math.max(1, report.batchSize))\n return {\n report,\n fixes,\n site,\n batches,\n complete: batches >= owedBatches && (!report.siteProposal || site !== null),\n }\n}\n\n/** The listing proposal a job produced, when it produced one. */\nexport function aiSeoFieldsProposalOf(job: Pick<AiJobSummary, 'outputs'> | null): AiSeoFieldsProposal | null {\n for (const output of job?.outputs ?? []) {\n if (output.resource !== 'seo') continue\n const proposal = readAiSeoProposal(output.proposal)\n if (proposal?.kind === 'fields') return proposal\n }\n return null\n}\n\n/** How many content fixes, and how many staged listings, an audit view would apply. */\nexport function aiSeoApplyCounts(view: AiSeoAuditView): { pagesWithContent: number; pagesWithValues: number } {\n let pagesWithContent = 0\n let pagesWithValues = 0\n for (const fix of Object.values(view.fixes)) {\n if (fix.content.length) pagesWithContent += 1\n if (Object.keys(fix.values).length) pagesWithValues += 1\n }\n return { pagesWithContent, pagesWithValues }\n}\n"],"names":["isSeoListingFieldKey","AI_SEO_TARGETS","aiSeoJobTarget","inputs","target","includes","AI_SEO_ENTITY_FORM_FIELDS","AI_SEO_AGENT_FORM_FIELDS","AI_SEO_OUTPUT_IDS","report","site","fixes","batch","fields","kind","id","isRecord","value","Array","isArray","strings","filter","entry","readValues","out","key","Object","entries","readAiSeoProposal","subject","name","String","path","values","keywords","notes","aiSeoAuditView","outputs","batches","output","resource","proposal","fix","screenId","owedBatches","Math","ceil","queue","length","max","batchSize","complete","siteProposal","aiSeoFieldsProposalOf","job","aiSeoApplyCounts","view","pagesWithContent","pagesWithValues","content","keys"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAID,SACEA,oBAAoB,QAEf,4CAA2C;AAgClD,OAAO,MAAMC,iBAAyC;IAAC;IAAU;IAAW;CAAO,CAAA;AAEnF,0FAA0F,GAC1F,OAAO,SAASC,eAAeC,MAAkD;IAC/E,MAAMC,SAASD,0BAAAA,MAAQ,CAAC,SAAS;IACjC,OAAO,OAAOC,WAAW,YAAY,AAACH,eAAqCI,QAAQ,CAACD,UAC/EA,SACD;AACN;AAkEA,+EAA+E,GAC/E,OAAO,MAAME,4BAA4B;IACvC;IACA;IACA;IACA;IACA;IACA;CACD,CAAS;AAEV,0EAA0E,GAC1E,OAAO,MAAMC,2BAA2B;IAAC;IAAuB;CAAqB,CAAS;AAyB9F,2BAA2B,GAC3B,OAAO,MAAMC,oBAAoB;IAC/BC,QAAQ;IACRC,MAAM;IACNC,OAAO,CAACC,QAAkB,CAAC,YAAY,EAAEA,OAAO;IAChDC,QAAQ,CAACC,MAA4BC,KAAsB,CAAC,OAAO,EAAED,KAAK,CAAC,EAAEC,aAAAA,KAAM,OAAO;AAC5F,EAAU;AAEV,MAAMC,WAAW,CAACC,QAChB,OAAOA,UAAU,YAAYA,UAAU,QAAQ,CAACC,MAAMC,OAAO,CAACF;AAEhE,MAAMG,UAAU,CAACH,QACfC,MAAMC,OAAO,CAACF,SAASA,MAAMI,MAAM,CAAC,CAACC,QAA2B,OAAOA,UAAU,YAAY,EAAE;AAEjG,SAASC,WAAWN,KAAc;IAChC,MAAMO,MAAwB,CAAC;IAC/B,IAAI,CAACR,SAASC,QAAQ,OAAOO;IAC7B,KAAK,MAAM,CAACC,KAAKH,MAAM,IAAII,OAAOC,OAAO,CAACV,OAAQ;QAChD,IAAIjB,qBAAqByB,QAAQ,OAAOH,UAAU,UAAUE,GAAG,CAACC,IAAI,GAAGH;IACzE;IACA,OAAOE;AACT;AAEA,+DAA+D,GAC/D,OAAO,SAASI,kBAAkBX,KAAc;IAC9C,IAAI,CAACD,SAASC,QAAQ,OAAO;IAC7B,OAAQA,KAAK,CAAC,OAAO;QACnB,KAAK;YAAU;oBAQIY;gBAPjB,MAAMA,UAAUb,SAASC,KAAK,CAAC,UAAU,IAAIA,KAAK,CAAC,UAAU,GAAG;gBAChE,IAAI,CAACY,WAAYA,OAAO,CAAC,OAAO,KAAK,YAAYA,OAAO,CAAC,OAAO,KAAK,WAAY,OAAO;gBACxF,OAAO;oBACLf,MAAM;oBACNe,SAAS;wBACPf,MAAMe,OAAO,CAAC,OAAO;wBACrBd,IAAI,OAAOc,OAAO,CAAC,KAAK,KAAK,WAAWA,OAAO,CAAC,KAAK,GAAG;wBACxDC,MAAMC,QAAOF,gBAAAA,OAAO,CAAC,OAAO,YAAfA,gBAAmB;wBAChCG,MAAM,OAAOH,OAAO,CAAC,OAAO,KAAK,WAAWA,OAAO,CAAC,OAAO,GAAG;oBAChE;oBACAI,QAAQV,WAAWN,KAAK,CAAC,SAAS;oBAClCiB,UAAUhB,MAAMC,OAAO,CAACF,KAAK,CAAC,WAAW,IAAKA,KAAK,CAAC,WAAW,GAA4B,EAAE;oBAC7FkB,OAAOf,QAAQH,KAAK,CAAC,QAAQ;gBAC/B;YACF;QACA,KAAK;YACH,OAAOC,MAAMC,OAAO,CAACF,KAAK,CAAC,QAAQ,IAAKA,QAAwC;QAClF,KAAK;YACH,OAAOC,MAAMC,OAAO,CAACF,KAAK,CAAC,QAAQ,IAAKA,QAAuC;QACjF,KAAK;YACH,OAAOD,SAASC,KAAK,CAAC,OAAO,IAAKA,QAAsC;QAC1E;YACE,OAAO;IACX;AACF;AAcA,2EAA2E,GAC3E,OAAO,SAASmB,eAAeC,OAA+B;IAC5D,IAAI5B,SAAkC;IACtC,IAAIC,OAAiC;IACrC,MAAMC,QAAsC,CAAC;IAC7C,IAAI2B,UAAU;IACd,KAAK,MAAMC,UAAUF,QAAS;QAC5B,IAAIE,OAAOC,QAAQ,KAAK,OAAO;QAC/B,MAAMC,WAAWb,kBAAkBW,OAAOE,QAAQ;QAClD,IAAI,CAACA,UAAU;QACf,IAAIA,SAAS3B,IAAI,KAAK,SAASL,SAASgC;aACnC,IAAIA,SAAS3B,IAAI,KAAK,QAAQJ,OAAO+B,SAAS/B,IAAI;aAClD,IAAI+B,SAAS3B,IAAI,KAAK,SAAS;YAClCwB,WAAW;YACX,KAAK,MAAMI,OAAOD,SAAS9B,KAAK,CAAEA,KAAK,CAAC+B,IAAIC,QAAQ,CAAC,GAAGD;QAC1D;IACF;IACA,IAAI,CAACjC,QAAQ,OAAO;IACpB,MAAMmC,cAAcC,KAAKC,IAAI,CAACrC,OAAOsC,KAAK,CAACC,MAAM,GAAGH,KAAKI,GAAG,CAAC,GAAGxC,OAAOyC,SAAS;IAChF,OAAO;QACLzC;QACAE;QACAD;QACA4B;QACAa,UAAUb,WAAWM,eAAgB,CAAA,CAACnC,OAAO2C,YAAY,IAAI1C,SAAS,IAAG;IAC3E;AACF;AAEA,+DAA+D,GAC/D,OAAO,SAAS2C,sBAAsBC,GAAyC;;IAC7E,KAAK,MAAMf,kBAAUe,uBAAAA,IAAKjB,OAAO,mBAAI,EAAE,CAAE;QACvC,IAAIE,OAAOC,QAAQ,KAAK,OAAO;QAC/B,MAAMC,WAAWb,kBAAkBW,OAAOE,QAAQ;QAClD,IAAIA,CAAAA,4BAAAA,SAAU3B,IAAI,MAAK,UAAU,OAAO2B;IAC1C;IACA,OAAO;AACT;AAEA,qFAAqF,GACrF,OAAO,SAASc,iBAAiBC,IAAoB;IACnD,IAAIC,mBAAmB;IACvB,IAAIC,kBAAkB;IACtB,KAAK,MAAMhB,OAAOhB,OAAOO,MAAM,CAACuB,KAAK7C,KAAK,EAAG;QAC3C,IAAI+B,IAAIiB,OAAO,CAACX,MAAM,EAAES,oBAAoB;QAC5C,IAAI/B,OAAOkC,IAAI,CAAClB,IAAIT,MAAM,EAAEe,MAAM,EAAEU,mBAAmB;IACzD;IACA,OAAO;QAAED;QAAkBC;IAAgB;AAC7C"}
|
|
@@ -74,7 +74,7 @@ export const AI_INVENTORY_KINDS = [
|
|
|
74
74
|
forms: 'Forms (id \u00b7 name \u00b7 fields)',
|
|
75
75
|
datasets: 'Datasets (id \u00b7 name \u00b7 fields)',
|
|
76
76
|
collections: 'Content collections (id \u00b7 name \u00b7 slug)',
|
|
77
|
-
screens: '
|
|
77
|
+
screens: 'Pages (id \u00b7 name \u00b7 slug)'
|
|
78
78
|
};
|
|
79
79
|
/**
|
|
80
80
|
* One record as a line, in the order its heading names the columns. The one
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-site-inventory.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * The site's inventory as a model sees it (AGL-2935): what the site already\n * has, so a plan reuses before it creates (doctrine rule 7).\n *\n * COMPACT by construction (AGL-2937): ids, names, prop shapes and slugs —\n * never a node tree, never a record's content. A component is its name and\n * the props an instance fills in; a form is its field names; a dataset its\n * field names. That is everything a planner needs to decide \"reuse this\"\n * and a generator needs to place a reference, and it is a few hundred\n * tokens where the trees would be tens of thousands.\n *\n * CAPPED three times, and the three are different numbers on purpose\n * (AGL-2937). The reader holds each kind to `AI_SITE_INVENTORY_MAX_PER_KIND`;\n * the prompt block LISTS only `AI_SITE_INVENTORY_LISTED_PER_KIND` of them and\n * is held to `AI_SITE_INVENTORY_MAX_CHARS` besides; and the rest are answered\n * on request, through the lookup tool the generation loop offers beside every\n * door's own (`tools/ai-inventory-lookup-tool.ts`).\n *\n * The listing cap is what a prompt can afford: the block is volatile, so it\n * is billed at full input rate on every request and every re-ask, and a site\n * with two hundred components would spend most of a plan's prompt on rows it\n * will not use. The read cap is what a LOOKUP can answer from, which is a\n * different question — a search for \"pricing card\" is worth nothing if the\n * card was never read. A kind cut by either is named in `truncated`, so the\n * model is told more exist rather than concluding they do not.\n *\n * The shapes live here, apart from the reader (`runtime/site-inventory.ts`),\n * so the prompt block, the validators and their specs can hold an inventory\n * without loading the Admin SDK the reader needs.\n */\n\n/**\n * Rows per kind the reader holds. Above the listing cap on purpose: the rows\n * past it are what the lookup tool searches, and a row never read cannot be\n * found. One projection of a few fields each, so the read is small even at\n * this cap.\n */\nexport const AI_SITE_INVENTORY_MAX_PER_KIND = 200\n\n/** Rows per kind the prompt block lists; the rest are found by lookup. */\nexport const AI_SITE_INVENTORY_LISTED_PER_KIND = 40\n\n/** The rendered block's ceiling: 2,000 tokens at four characters a token. */\nexport const AI_SITE_INVENTORY_MAX_CHARS = 8_000\n\nexport type AiInventoryKind =\n | 'components'\n | 'layouts'\n | 'templates'\n | 'forms'\n | 'datasets'\n | 'collections'\n | 'screens'\n\nexport const AI_INVENTORY_KINDS: readonly AiInventoryKind[] = [\n 'components',\n 'layouts',\n 'templates',\n 'forms',\n 'datasets',\n 'collections',\n 'screens',\n]\n\n/**\n * A reusable component's declared props as an instance may fill them: prop\n * name → its declared type (`text`, `image`, `href`, `number`, `boolean`, …).\n */\nexport type AiComponentPropTypes = Record<string, string>\n\nexport interface AiInventoryComponent {\n id: string\n name: string\n /** The props an instance fills in: name → declared type. */\n props: AiComponentPropTypes\n /**\n * The facts in square brackets each prop's default shows until a page sets\n * its own, by prop name (AGL-3056): what a page placing the component still\n * asks the member to fill. Read for the member's note, never listed in a\n * prompt; absent when no default holds one.\n */\n bracketedDefaults?: Record<string, string[]>\n}\n\nexport interface AiInventoryLayout {\n id: string\n name: string\n /** The layout this one nests inside, when it nests. */\n parentId: string | null\n}\n\nexport interface AiInventoryTemplate {\n id: string\n name: string\n kind: string\n}\n\nexport interface AiInventoryForm {\n id: string\n name: string\n fields: string[]\n}\n\nexport interface AiInventoryDataset {\n id: string\n name: string\n fields: string[]\n}\n\nexport interface AiInventoryCollection {\n id: string\n name: string\n slug: string\n}\n\nexport interface AiInventoryScreen {\n id: string\n name: string\n slug: string\n layoutId: string | null\n /** A collection's entry template rather than a page of its own. */\n template: boolean\n}\n\n/**\n * The site's one theme, as a planner and the email rule read it: a short\n * description, the light scheme's colors by palette path, and the families\n * the theme loads.\n */\nexport interface AiInventoryTheme {\n summary: string[]\n colors: Record<string, string>\n fonts: string[]\n}\n\nexport interface AiSiteInventory {\n hostId: string\n components: AiInventoryComponent[]\n layouts: AiInventoryLayout[]\n templates: AiInventoryTemplate[]\n forms: AiInventoryForm[]\n datasets: AiInventoryDataset[]\n collections: AiInventoryCollection[]\n screens: AiInventoryScreen[]\n theme: AiInventoryTheme | null\n /** Kinds with more records than are listed. */\n truncated: AiInventoryKind[]\n}\n\n/** One inventory record, whatever kind it is. */\nexport type AiInventoryRow =\n | AiInventoryComponent\n | AiInventoryLayout\n | AiInventoryTemplate\n | AiInventoryForm\n | AiInventoryDataset\n | AiInventoryCollection\n | AiInventoryScreen\n\n/**\n * The rows of one kind. The inventory's fields are named after their kinds,\n * so this is a lookup rather than a switch, and it is the one place that\n * relies on the correspondence: the prompt block, the lookup tool and their\n * specs all read a kind through it.\n */\nexport function aiInventoryRows(\n inventory: AiSiteInventory,\n kind: AiInventoryKind,\n): readonly AiInventoryRow[] {\n return inventory[kind]\n}\n\n/** The heading one kind's lines are listed under, naming its columns in order. */\nexport const AI_INVENTORY_KIND_HEADINGS: Readonly<Record<AiInventoryKind, string>> = {\n components: 'Reusable components (id \\u00b7 name \\u00b7 props an instance fills)',\n layouts: 'Layouts (id \\u00b7 name)',\n templates: 'Templates (id \\u00b7 name \\u00b7 kind)',\n forms: 'Forms (id \\u00b7 name \\u00b7 fields)',\n datasets: 'Datasets (id \\u00b7 name \\u00b7 fields)',\n collections: 'Content collections (id \\u00b7 name \\u00b7 slug)',\n screens: 'Screens (id \\u00b7 name \\u00b7 slug)',\n}\n\n/**\n * One record as a line, in the order its heading names the columns. The one\n * renderer: the prompt block and the lookup tool's answer read alike, so a\n * row found by lookup is the same shape as a row that was listed.\n */\nexport function aiInventoryLine(kind: AiInventoryKind, row: AiInventoryRow): string {\n const head = `${row.id} \\u00b7 ${row.name}`\n switch (kind) {\n case 'components': {\n const props = Object.entries((row as AiInventoryComponent).props)\n .map(([name, type]) => `${name}:${type}`)\n .join(', ')\n return `${head} \\u00b7 ${props || 'no props'}`\n }\n case 'layouts':\n return head\n case 'templates':\n return `${head} \\u00b7 ${(row as AiInventoryTemplate).kind}`\n case 'forms':\n return `${head} \\u00b7 ${(row as AiInventoryForm).fields.join(', ')}`\n case 'datasets':\n return `${head} \\u00b7 ${(row as AiInventoryDataset).fields.join(', ')}`\n case 'collections':\n return `${head} \\u00b7 ${(row as AiInventoryCollection).slug}`\n case 'screens': {\n const screen = row as AiInventoryScreen\n return `${head} \\u00b7 ${screen.slug}${screen.template ? ' \\u00b7 entry template' : ''}`\n }\n }\n}\n\n/** Every word of a record a lookup searches: its line, which holds them all. */\nexport function aiInventoryHaystack(kind: AiInventoryKind, row: AiInventoryRow): string {\n return aiInventoryLine(kind, row).toLowerCase()\n}\n\n/** The addresses a site's home screen is stored under. */\nconst HOME_SLUGS = new Set(['', 'home', 'index'])\n\n/**\n * The site's home screens, by id (AGL-3056): a screen at the root or at\n * `home`, as a seeded site stores its first screen, or one named Home. A link\n * with nowhere to go is sent there, which is why the doctrine names them.\n */\nexport function aiHomeScreenIds(inventory: AiSiteInventory | null): string[] {\n return (inventory?.screens ?? [])\n .filter(\n (screen) =>\n !screen.template &&\n (HOME_SLUGS.has(screen.slug.trim().replace(/^\\/+|\\/+$/g, '').toLowerCase()) ||\n /^home(?:\\s?page)?$/i.test(screen.name.trim())),\n )\n .map((screen) => screen.id)\n}\n\n/** A site the org does not own; the inventory is never read for it. */\nexport class AiInventoryScopeError extends Error {\n constructor(hostId: string) {\n super(`host ${hostId} is not a site of the workspace that asked`)\n this.name = 'AiInventoryScopeError'\n }\n}\n\nexport function emptyAiSiteInventory(hostId: string): AiSiteInventory {\n return {\n hostId,\n components: [],\n layouts: [],\n templates: [],\n forms: [],\n datasets: [],\n collections: [],\n screens: [],\n theme: null,\n truncated: [],\n }\n}\n"],"names":["AI_SITE_INVENTORY_MAX_PER_KIND","AI_SITE_INVENTORY_LISTED_PER_KIND","AI_SITE_INVENTORY_MAX_CHARS","AI_INVENTORY_KINDS","aiInventoryRows","inventory","kind","AI_INVENTORY_KIND_HEADINGS","components","layouts","templates","forms","datasets","collections","screens","aiInventoryLine","row","head","id","name","props","Object","entries","map","type","join","fields","slug","screen","template","aiInventoryHaystack","toLowerCase","HOME_SLUGS","Set","aiHomeScreenIds","filter","has","trim","replace","test","AiInventoryScopeError","Error","hostId","emptyAiSiteInventory","theme","truncated"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-site-inventory.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * The site's inventory as a model sees it (AGL-2935): what the site already\n * has, so a plan reuses before it creates (doctrine rule 7).\n *\n * COMPACT by construction (AGL-2937): ids, names, prop shapes and slugs —\n * never a node tree, never a record's content. A component is its name and\n * the props an instance fills in; a form is its field names; a dataset its\n * field names. That is everything a planner needs to decide \"reuse this\"\n * and a generator needs to place a reference, and it is a few hundred\n * tokens where the trees would be tens of thousands.\n *\n * CAPPED three times, and the three are different numbers on purpose\n * (AGL-2937). The reader holds each kind to `AI_SITE_INVENTORY_MAX_PER_KIND`;\n * the prompt block LISTS only `AI_SITE_INVENTORY_LISTED_PER_KIND` of them and\n * is held to `AI_SITE_INVENTORY_MAX_CHARS` besides; and the rest are answered\n * on request, through the lookup tool the generation loop offers beside every\n * door's own (`tools/ai-inventory-lookup-tool.ts`).\n *\n * The listing cap is what a prompt can afford: the block is volatile, so it\n * is billed at full input rate on every request and every re-ask, and a site\n * with two hundred components would spend most of a plan's prompt on rows it\n * will not use. The read cap is what a LOOKUP can answer from, which is a\n * different question — a search for \"pricing card\" is worth nothing if the\n * card was never read. A kind cut by either is named in `truncated`, so the\n * model is told more exist rather than concluding they do not.\n *\n * The shapes live here, apart from the reader (`runtime/site-inventory.ts`),\n * so the prompt block, the validators and their specs can hold an inventory\n * without loading the Admin SDK the reader needs.\n */\n\n/**\n * Rows per kind the reader holds. Above the listing cap on purpose: the rows\n * past it are what the lookup tool searches, and a row never read cannot be\n * found. One projection of a few fields each, so the read is small even at\n * this cap.\n */\nexport const AI_SITE_INVENTORY_MAX_PER_KIND = 200\n\n/** Rows per kind the prompt block lists; the rest are found by lookup. */\nexport const AI_SITE_INVENTORY_LISTED_PER_KIND = 40\n\n/** The rendered block's ceiling: 2,000 tokens at four characters a token. */\nexport const AI_SITE_INVENTORY_MAX_CHARS = 8_000\n\nexport type AiInventoryKind =\n | 'components'\n | 'layouts'\n | 'templates'\n | 'forms'\n | 'datasets'\n | 'collections'\n | 'screens'\n\nexport const AI_INVENTORY_KINDS: readonly AiInventoryKind[] = [\n 'components',\n 'layouts',\n 'templates',\n 'forms',\n 'datasets',\n 'collections',\n 'screens',\n]\n\n/**\n * A reusable component's declared props as an instance may fill them: prop\n * name → its declared type (`text`, `image`, `href`, `number`, `boolean`, …).\n */\nexport type AiComponentPropTypes = Record<string, string>\n\nexport interface AiInventoryComponent {\n id: string\n name: string\n /** The props an instance fills in: name → declared type. */\n props: AiComponentPropTypes\n /**\n * The facts in square brackets each prop's default shows until a page sets\n * its own, by prop name (AGL-3056): what a page placing the component still\n * asks the member to fill. Read for the member's note, never listed in a\n * prompt; absent when no default holds one.\n */\n bracketedDefaults?: Record<string, string[]>\n}\n\nexport interface AiInventoryLayout {\n id: string\n name: string\n /** The layout this one nests inside, when it nests. */\n parentId: string | null\n}\n\nexport interface AiInventoryTemplate {\n id: string\n name: string\n kind: string\n}\n\nexport interface AiInventoryForm {\n id: string\n name: string\n fields: string[]\n}\n\nexport interface AiInventoryDataset {\n id: string\n name: string\n fields: string[]\n}\n\nexport interface AiInventoryCollection {\n id: string\n name: string\n slug: string\n}\n\nexport interface AiInventoryScreen {\n id: string\n name: string\n slug: string\n layoutId: string | null\n /** A collection's entry template rather than a page of its own. */\n template: boolean\n}\n\n/**\n * The site's one theme, as a planner and the email rule read it: a short\n * description, the light scheme's colors by palette path, and the families\n * the theme loads.\n */\nexport interface AiInventoryTheme {\n summary: string[]\n colors: Record<string, string>\n fonts: string[]\n}\n\nexport interface AiSiteInventory {\n hostId: string\n components: AiInventoryComponent[]\n layouts: AiInventoryLayout[]\n templates: AiInventoryTemplate[]\n forms: AiInventoryForm[]\n datasets: AiInventoryDataset[]\n collections: AiInventoryCollection[]\n screens: AiInventoryScreen[]\n theme: AiInventoryTheme | null\n /** Kinds with more records than are listed. */\n truncated: AiInventoryKind[]\n}\n\n/** One inventory record, whatever kind it is. */\nexport type AiInventoryRow =\n | AiInventoryComponent\n | AiInventoryLayout\n | AiInventoryTemplate\n | AiInventoryForm\n | AiInventoryDataset\n | AiInventoryCollection\n | AiInventoryScreen\n\n/**\n * The rows of one kind. The inventory's fields are named after their kinds,\n * so this is a lookup rather than a switch, and it is the one place that\n * relies on the correspondence: the prompt block, the lookup tool and their\n * specs all read a kind through it.\n */\nexport function aiInventoryRows(\n inventory: AiSiteInventory,\n kind: AiInventoryKind,\n): readonly AiInventoryRow[] {\n return inventory[kind]\n}\n\n/** The heading one kind's lines are listed under, naming its columns in order. */\nexport const AI_INVENTORY_KIND_HEADINGS: Readonly<Record<AiInventoryKind, string>> = {\n components: 'Reusable components (id \\u00b7 name \\u00b7 props an instance fills)',\n layouts: 'Layouts (id \\u00b7 name)',\n templates: 'Templates (id \\u00b7 name \\u00b7 kind)',\n forms: 'Forms (id \\u00b7 name \\u00b7 fields)',\n datasets: 'Datasets (id \\u00b7 name \\u00b7 fields)',\n collections: 'Content collections (id \\u00b7 name \\u00b7 slug)',\n screens: 'Pages (id \\u00b7 name \\u00b7 slug)',\n}\n\n/**\n * One record as a line, in the order its heading names the columns. The one\n * renderer: the prompt block and the lookup tool's answer read alike, so a\n * row found by lookup is the same shape as a row that was listed.\n */\nexport function aiInventoryLine(kind: AiInventoryKind, row: AiInventoryRow): string {\n const head = `${row.id} \\u00b7 ${row.name}`\n switch (kind) {\n case 'components': {\n const props = Object.entries((row as AiInventoryComponent).props)\n .map(([name, type]) => `${name}:${type}`)\n .join(', ')\n return `${head} \\u00b7 ${props || 'no props'}`\n }\n case 'layouts':\n return head\n case 'templates':\n return `${head} \\u00b7 ${(row as AiInventoryTemplate).kind}`\n case 'forms':\n return `${head} \\u00b7 ${(row as AiInventoryForm).fields.join(', ')}`\n case 'datasets':\n return `${head} \\u00b7 ${(row as AiInventoryDataset).fields.join(', ')}`\n case 'collections':\n return `${head} \\u00b7 ${(row as AiInventoryCollection).slug}`\n case 'screens': {\n const screen = row as AiInventoryScreen\n return `${head} \\u00b7 ${screen.slug}${screen.template ? ' \\u00b7 entry template' : ''}`\n }\n }\n}\n\n/** Every word of a record a lookup searches: its line, which holds them all. */\nexport function aiInventoryHaystack(kind: AiInventoryKind, row: AiInventoryRow): string {\n return aiInventoryLine(kind, row).toLowerCase()\n}\n\n/** The addresses a site's home screen is stored under. */\nconst HOME_SLUGS = new Set(['', 'home', 'index'])\n\n/**\n * The site's home screens, by id (AGL-3056): a screen at the root or at\n * `home`, as a seeded site stores its first screen, or one named Home. A link\n * with nowhere to go is sent there, which is why the doctrine names them.\n */\nexport function aiHomeScreenIds(inventory: AiSiteInventory | null): string[] {\n return (inventory?.screens ?? [])\n .filter(\n (screen) =>\n !screen.template &&\n (HOME_SLUGS.has(screen.slug.trim().replace(/^\\/+|\\/+$/g, '').toLowerCase()) ||\n /^home(?:\\s?page)?$/i.test(screen.name.trim())),\n )\n .map((screen) => screen.id)\n}\n\n/** A site the org does not own; the inventory is never read for it. */\nexport class AiInventoryScopeError extends Error {\n constructor(hostId: string) {\n super(`host ${hostId} is not a site of the workspace that asked`)\n this.name = 'AiInventoryScopeError'\n }\n}\n\nexport function emptyAiSiteInventory(hostId: string): AiSiteInventory {\n return {\n hostId,\n components: [],\n layouts: [],\n templates: [],\n forms: [],\n datasets: [],\n collections: [],\n screens: [],\n theme: null,\n truncated: [],\n }\n}\n"],"names":["AI_SITE_INVENTORY_MAX_PER_KIND","AI_SITE_INVENTORY_LISTED_PER_KIND","AI_SITE_INVENTORY_MAX_CHARS","AI_INVENTORY_KINDS","aiInventoryRows","inventory","kind","AI_INVENTORY_KIND_HEADINGS","components","layouts","templates","forms","datasets","collections","screens","aiInventoryLine","row","head","id","name","props","Object","entries","map","type","join","fields","slug","screen","template","aiInventoryHaystack","toLowerCase","HOME_SLUGS","Set","aiHomeScreenIds","filter","has","trim","replace","test","AiInventoryScopeError","Error","hostId","emptyAiSiteInventory","theme","truncated"],"mappings":"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"}
|
|
@@ -68,7 +68,7 @@ export const AI_TEMPLATE_SUBJECT_DEFINITIONS = {
|
|
|
68
68
|
'collectionRelated'
|
|
69
69
|
],
|
|
70
70
|
defaultName: 'Entry page template',
|
|
71
|
-
appliedIn: 'the collection’s entry template
|
|
71
|
+
appliedIn: 'the collection’s entry template page, under Template pages in Content'
|
|
72
72
|
},
|
|
73
73
|
product: {
|
|
74
74
|
subject: 'product',
|
|
@@ -91,7 +91,7 @@ export const AI_TEMPLATE_SUBJECT_DEFINITIONS = {
|
|
|
91
91
|
'contentAuthorProfile'
|
|
92
92
|
],
|
|
93
93
|
defaultName: 'Author page template',
|
|
94
|
-
appliedIn: 'the author page
|
|
94
|
+
appliedIn: 'the author page on the Authors tab'
|
|
95
95
|
}
|
|
96
96
|
};
|
|
97
97
|
/** Every `{{…}}` token a string holds, spelled without inner whitespace. */ export function aiBindingTokensIn(value) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-template-subjects.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n AUTHOR_TOKEN_CATALOG,\n COLLECTION_TOKEN_CATALOG,\n ENTRY_TOKEN_CATALOG,\n type BindingTokenCatalogEntry,\n} from '@aglyn/aglyn/app-utils/binding-token-catalog'\n\n/**\n * What a generated page template renders for (AGL-2909): one page drawn once\n * per record the site already keeps.\n *\n * - `entry` — a content collection's entry page: a blog post, an event, a\n * case study. Picked as the collection's entry template screen.\n * - `product` — the store's product page. Picked as the product template in\n * Store settings.\n * - `author` — an author's page. Picked as the author page screen.\n *\n * Everything that differs per record is a binding token, and the tokens a\n * template may bind are the ones the platform fills on that page. For an\n * entry and an author those are the besigner's own insert-picker catalogs, so\n * what a model may bind and what an author can pick are one list; a product's\n * are the commerce product page's, which no catalog names yet (below).\n *\n * Model only: shapes, the vocabularies and the inputs reader, no runtime.\n */\n\nexport type AiTemplateSubject = 'entry' | 'product' | 'author'\n\nexport const AI_TEMPLATE_SUBJECTS: readonly AiTemplateSubject[] = ['entry', 'product', 'author']\n\n/**\n * The tokens the commerce product page fills. The commerce plugin's page\n * resolver is their definition of record; a spec reads its source and holds\n * this list to it in both directions, because a plugin never imports another\n * plugin's internals.\n */\nexport const AI_PRODUCT_TEMPLATE_TOKENS: readonly BindingTokenCatalogEntry[] = [\n { token: '{{product.name}}', label: 'Name', description: 'The product’s name.' },\n {\n token: '{{product.description}}',\n label: 'Description',\n description: 'The product’s description.',\n },\n {\n token: '{{product.price}}',\n label: 'Price',\n description: 'The price, or “From” the lowest price when variants differ.',\n },\n { token: '{{product.image}}', label: 'Image', description: 'The product’s first image.' },\n { token: '{{product.slug}}', label: 'Slug', description: 'The product’s URL segment.' },\n]\n\n/** The pager tokens, which resolve on an author's page over their own archive. */\nconst PAGINATION_TOKENS = COLLECTION_TOKEN_CATALOG.filter((entry) =>\n entry.token.startsWith('{{pagination.'),\n)\n\nexport interface AiTemplateSubjectDefinition {\n subject: AiTemplateSubject\n /** One rendered page's record, as a sentence names it. */\n noun: string\n /** The token the page's one h1 binds, so every record shows its own title. */\n titleToken: string\n /** Every token the page may bind. */\n tokens: readonly BindingTokenCatalogEntry[]\n /**\n * Palette blocks that fill themselves from this subject on its page, and\n * render nothing on another subject's. Inside a Collection Entries card each\n * fills from that card's entry instead, so a card is exempt.\n */\n blocks: readonly string[]\n /** The template's name when the plan names none. */\n defaultName: string\n /** Where a member puts the page to work, for the proposal and the docs. */\n appliedIn: string\n}\n\nexport const AI_TEMPLATE_SUBJECT_DEFINITIONS: Readonly<\n Record<AiTemplateSubject, AiTemplateSubjectDefinition>\n> = {\n entry: {\n subject: 'entry',\n noun: 'a collection entry',\n titleToken: '{{entry.title}}',\n tokens: [...ENTRY_TOKEN_CATALOG, ...COLLECTION_TOKEN_CATALOG],\n blocks: ['collectionEntryMeta', 'collectionEntryBody', 'collectionEntryAuthor', 'collectionRelated'],\n defaultName: 'Entry page template',\n appliedIn: 'the collection’s entry template
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-template-subjects.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n AUTHOR_TOKEN_CATALOG,\n COLLECTION_TOKEN_CATALOG,\n ENTRY_TOKEN_CATALOG,\n type BindingTokenCatalogEntry,\n} from '@aglyn/aglyn/app-utils/binding-token-catalog'\n\n/**\n * What a generated page template renders for (AGL-2909): one page drawn once\n * per record the site already keeps.\n *\n * - `entry` — a content collection's entry page: a blog post, an event, a\n * case study. Picked as the collection's entry template screen.\n * - `product` — the store's product page. Picked as the product template in\n * Store settings.\n * - `author` — an author's page. Picked as the author page screen.\n *\n * Everything that differs per record is a binding token, and the tokens a\n * template may bind are the ones the platform fills on that page. For an\n * entry and an author those are the besigner's own insert-picker catalogs, so\n * what a model may bind and what an author can pick are one list; a product's\n * are the commerce product page's, which no catalog names yet (below).\n *\n * Model only: shapes, the vocabularies and the inputs reader, no runtime.\n */\n\nexport type AiTemplateSubject = 'entry' | 'product' | 'author'\n\nexport const AI_TEMPLATE_SUBJECTS: readonly AiTemplateSubject[] = ['entry', 'product', 'author']\n\n/**\n * The tokens the commerce product page fills. The commerce plugin's page\n * resolver is their definition of record; a spec reads its source and holds\n * this list to it in both directions, because a plugin never imports another\n * plugin's internals.\n */\nexport const AI_PRODUCT_TEMPLATE_TOKENS: readonly BindingTokenCatalogEntry[] = [\n { token: '{{product.name}}', label: 'Name', description: 'The product’s name.' },\n {\n token: '{{product.description}}',\n label: 'Description',\n description: 'The product’s description.',\n },\n {\n token: '{{product.price}}',\n label: 'Price',\n description: 'The price, or “From” the lowest price when variants differ.',\n },\n { token: '{{product.image}}', label: 'Image', description: 'The product’s first image.' },\n { token: '{{product.slug}}', label: 'Slug', description: 'The product’s URL segment.' },\n]\n\n/** The pager tokens, which resolve on an author's page over their own archive. */\nconst PAGINATION_TOKENS = COLLECTION_TOKEN_CATALOG.filter((entry) =>\n entry.token.startsWith('{{pagination.'),\n)\n\nexport interface AiTemplateSubjectDefinition {\n subject: AiTemplateSubject\n /** One rendered page's record, as a sentence names it. */\n noun: string\n /** The token the page's one h1 binds, so every record shows its own title. */\n titleToken: string\n /** Every token the page may bind. */\n tokens: readonly BindingTokenCatalogEntry[]\n /**\n * Palette blocks that fill themselves from this subject on its page, and\n * render nothing on another subject's. Inside a Collection Entries card each\n * fills from that card's entry instead, so a card is exempt.\n */\n blocks: readonly string[]\n /** The template's name when the plan names none. */\n defaultName: string\n /** Where a member puts the page to work, for the proposal and the docs. */\n appliedIn: string\n}\n\nexport const AI_TEMPLATE_SUBJECT_DEFINITIONS: Readonly<\n Record<AiTemplateSubject, AiTemplateSubjectDefinition>\n> = {\n entry: {\n subject: 'entry',\n noun: 'a collection entry',\n titleToken: '{{entry.title}}',\n tokens: [...ENTRY_TOKEN_CATALOG, ...COLLECTION_TOKEN_CATALOG],\n blocks: ['collectionEntryMeta', 'collectionEntryBody', 'collectionEntryAuthor', 'collectionRelated'],\n defaultName: 'Entry page template',\n appliedIn: 'the collection’s entry template page, under Template pages in Content',\n },\n product: {\n subject: 'product',\n noun: 'a product',\n titleToken: '{{product.name}}',\n tokens: AI_PRODUCT_TEMPLATE_TOKENS,\n blocks: [],\n defaultName: 'Product page template',\n appliedIn: 'the product page template in Store settings',\n },\n author: {\n subject: 'author',\n noun: 'an author',\n titleToken: '{{author.name}}',\n tokens: [...AUTHOR_TOKEN_CATALOG, ...PAGINATION_TOKENS],\n blocks: ['contentAuthorProfile'],\n defaultName: 'Author page template',\n appliedIn: 'the author page on the Authors tab',\n },\n}\n\n/** Every `{{…}}` token a string holds, spelled without inner whitespace. */\nexport function aiBindingTokensIn(value: string): string[] {\n return [...value.matchAll(/\\{\\{\\s*([^{}\\s]+)\\s*\\}\\}/g)].map((match) => `{{${match[1]}}}`)\n}\n\n/**\n * Whether a token may fill a link or media prop whole: its field is an\n * address or a picture (`{{entry.url}}`, `{{entry.coverImage}}`,\n * `{{author.image}}`), never copy.\n */\nexport function isAiAddressToken(token: string): boolean {\n const field = token.replace(/^\\{\\{|\\}\\}$/g, '').split('.').pop() ?? ''\n return /^(?:url|image)$|(?:Url|Image|Video)$/.test(field)\n}\n\n/** The tokens of a subject a link or media prop may hold. */\nexport function aiTemplateAddressTokens(definition: AiTemplateSubjectDefinition): string[] {\n return definition.tokens.map((entry) => entry.token).filter(isAiAddressToken)\n}\n\n/** What a template job reads from its inputs. */\nexport interface AiTemplateJobInputs {\n subject: AiTemplateSubject\n /** The content collection an entry template is for; `null` for a product or author page. */\n collectionId: string | null\n}\n\nconst RECORD_ID = /^[A-Za-z0-9_-]{1,100}$/\n\n/** The inputs of a template job, or a sentence naming what is missing. */\nexport function parseAiTemplateJobInputs(\n inputs: Readonly<Record<string, unknown>> | null | undefined,\n): AiTemplateJobInputs | string {\n const subject = String(inputs?.['subject'] ?? '')\n if (!(AI_TEMPLATE_SUBJECTS as readonly string[]).includes(subject)) {\n return `Name what the template is for: inputs.subject is one of ${AI_TEMPLATE_SUBJECTS.join(', ')}`\n }\n if (subject !== 'entry') return { subject: subject as AiTemplateSubject, collectionId: null }\n const raw = inputs?.['collectionId']\n const collectionId = typeof raw === 'string' ? raw.trim() : ''\n if (!RECORD_ID.test(collectionId)) {\n return 'An entry page template names its content collection in inputs.collectionId'\n }\n return { subject: 'entry', collectionId }\n}\n"],"names":["AUTHOR_TOKEN_CATALOG","COLLECTION_TOKEN_CATALOG","ENTRY_TOKEN_CATALOG","AI_TEMPLATE_SUBJECTS","AI_PRODUCT_TEMPLATE_TOKENS","token","label","description","PAGINATION_TOKENS","filter","entry","startsWith","AI_TEMPLATE_SUBJECT_DEFINITIONS","subject","noun","titleToken","tokens","blocks","defaultName","appliedIn","product","author","aiBindingTokensIn","value","matchAll","map","match","isAiAddressToken","field","replace","split","pop","test","aiTemplateAddressTokens","definition","RECORD_ID","parseAiTemplateJobInputs","inputs","String","includes","join","collectionId","raw","trim"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,oBAAoB,EACpBC,wBAAwB,EACxBC,mBAAmB,QAEd,+CAA8C;AAuBrD,OAAO,MAAMC,uBAAqD;IAAC;IAAS;IAAW;CAAS,CAAA;AAEhG;;;;;CAKC,GACD,OAAO,MAAMC,6BAAkE;IAC7E;QAAEC,OAAO;QAAoBC,OAAO;QAAQC,aAAa;IAAsB;IAC/E;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QAAEF,OAAO;QAAqBC,OAAO;QAASC,aAAa;IAA6B;IACxF;QAAEF,OAAO;QAAoBC,OAAO;QAAQC,aAAa;IAA6B;CACvF,CAAA;AAED,gFAAgF,GAChF,MAAMC,oBAAoBP,yBAAyBQ,MAAM,CAAC,CAACC,QACzDA,MAAML,KAAK,CAACM,UAAU,CAAC;AAuBzB,OAAO,MAAMC,kCAET;IACFF,OAAO;QACLG,SAAS;QACTC,MAAM;QACNC,YAAY;QACZC,QAAQ;eAAId;eAAwBD;SAAyB;QAC7DgB,QAAQ;YAAC;YAAuB;YAAuB;YAAyB;SAAoB;QACpGC,aAAa;QACbC,WAAW;IACb;IACAC,SAAS;QACPP,SAAS;QACTC,MAAM;QACNC,YAAY;QACZC,QAAQZ;QACRa,QAAQ,EAAE;QACVC,aAAa;QACbC,WAAW;IACb;IACAE,QAAQ;QACNR,SAAS;QACTC,MAAM;QACNC,YAAY;QACZC,QAAQ;eAAIhB;eAAyBQ;SAAkB;QACvDS,QAAQ;YAAC;SAAuB;QAChCC,aAAa;QACbC,WAAW;IACb;AACF,EAAC;AAED,0EAA0E,GAC1E,OAAO,SAASG,kBAAkBC,KAAa;IAC7C,OAAO;WAAIA,MAAMC,QAAQ,CAAC;KAA6B,CAACC,GAAG,CAAC,CAACC,QAAU,CAAC,EAAE,EAAEA,KAAK,CAAC,EAAE,CAAC,EAAE,CAAC;AAC1F;AAEA;;;;CAIC,GACD,OAAO,SAASC,iBAAiBtB,KAAa;QAC9BA;IAAd,MAAMuB,SAAQvB,2BAAAA,MAAMwB,OAAO,CAAC,gBAAgB,IAAIC,KAAK,CAAC,KAAKC,GAAG,cAAhD1B,2BAAsD;IACpE,OAAO,uCAAuC2B,IAAI,CAACJ;AACrD;AAEA,2DAA2D,GAC3D,OAAO,SAASK,wBAAwBC,UAAuC;IAC7E,OAAOA,WAAWlB,MAAM,CAACS,GAAG,CAAC,CAACf,QAAUA,MAAML,KAAK,EAAEI,MAAM,CAACkB;AAC9D;AASA,MAAMQ,YAAY;AAElB,wEAAwE,GACxE,OAAO,SAASC,yBACdC,MAA4D;;IAE5D,MAAMxB,UAAUyB,eAAOD,0BAAAA,MAAQ,CAAC,UAAU,mBAAI;IAC9C,IAAI,CAAC,AAAClC,qBAA2CoC,QAAQ,CAAC1B,UAAU;QAClE,OAAO,CAAC,wDAAwD,EAAEV,qBAAqBqC,IAAI,CAAC,OAAO;IACrG;IACA,IAAI3B,YAAY,SAAS,OAAO;QAAEA,SAASA;QAA8B4B,cAAc;IAAK;IAC5F,MAAMC,MAAML,0BAAAA,MAAQ,CAAC,eAAe;IACpC,MAAMI,eAAe,OAAOC,QAAQ,WAAWA,IAAIC,IAAI,KAAK;IAC5D,IAAI,CAACR,UAAUH,IAAI,CAACS,eAAe;QACjC,OAAO;IACT;IACA,OAAO;QAAE5B,SAAS;QAAS4B;IAAa;AAC1C"}
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
|
-
import { type HostThemeSource } from '@aglyn/aglyn/app-utils/
|
|
17
|
+
import { type HostThemeSource } from '@aglyn/aglyn/app-utils/site-theme';
|
|
18
18
|
import type { HostTheme, HostThemeScheme } from '@aglyn/shared-data-types';
|
|
19
19
|
import { type ThemeColorToken, type ThemeComponentOverrideLeaf, type ThemeEditorControlId } from '@aglyn/shared-ui-theme/util/theme-editor-fields';
|
|
20
20
|
/**
|
|
@@ -14,7 +14,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
14
14
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
15
|
* See the License for the specific language governing permissions and
|
|
16
16
|
* limitations under the License.
|
|
17
|
-
*/ import { CONTRAST_AA, CONTRAST_AA_LARGE, contrastRatio } from "@aglyn/aglyn/app-utils/
|
|
17
|
+
*/ import { CONTRAST_AA, CONTRAST_AA_LARGE, contrastRatio } from "@aglyn/aglyn/app-utils/site-theme";
|
|
18
18
|
import { accessibleShade } from "@aglyn/shared-ui-theme/util/accessible-shade";
|
|
19
19
|
import { inheritedThemeColor } from "@aglyn/shared-ui-theme/util/theme-editor-defaults";
|
|
20
20
|
import { readDarkScheme, readFontFamily, readThemeColor, readToolbarHeight, resetComponentOverrides, SYSTEM_FONT_VALUE, THEME_COLOR_FIELDS, THEME_EDITOR_CONTROLS, THEME_EDITOR_MEDIA_QUERIES, THEME_EDITOR_SCHEMES, writeBorderRadius, writeComponentOverride, writeDarkScheme, writeFontFamily, writeSpacing, writeThemeColor, writeToolbarHeight } from "@aglyn/shared-ui-theme/util/theme-editor-fields";
|