@aglyn/plugins-ai 1.0.0-beta.292 → 1.0.0-beta.296
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +17 -17
- package/src/lib/components/ai-job-links.d.ts +15 -2
- package/src/lib/components/ai-job-links.js +24 -3
- package/src/lib/components/ai-job-links.js.map +1 -1
- package/src/lib/components/ai-job-requests.d.ts +9 -0
- package/src/lib/components/ai-job-requests.js +34 -0
- package/src/lib/components/ai-job-requests.js.map +1 -1
- package/src/lib/components/ai-site-build-page.component.js +28 -2
- package/src/lib/components/ai-site-build-page.component.js.map +1 -1
- package/src/lib/components/ai-site-import-job.component.js +5 -4
- package/src/lib/components/ai-site-import-job.component.js.map +1 -1
- package/src/lib/components/ai-site-import-start.component.js +3 -3
- package/src/lib/components/ai-site-import-start.component.js.map +1 -1
- package/src/lib/components/ai-site-start-card.component.d.ts +1 -1
- package/src/lib/components/ai-site-start-card.component.js +129 -29
- package/src/lib/components/ai-site-start-card.component.js.map +1 -1
- package/src/lib/components/ai-site-start-steps.component.d.ts +15 -3
- package/src/lib/components/ai-site-start-steps.component.js +42 -25
- package/src/lib/components/ai-site-start-steps.component.js.map +1 -1
- package/src/lib/components/ai-site-style-previews.d.ts +17 -0
- package/src/lib/components/ai-site-style-previews.js +26 -95
- package/src/lib/components/ai-site-style-previews.js.map +1 -1
- package/src/lib/components/assist-jobs-drawer.component.d.ts +7 -1
- package/src/lib/components/assist-jobs-drawer.component.js +20 -6
- package/src/lib/components/assist-jobs-drawer.component.js.map +1 -1
- package/src/lib/components/assist-page-change-card.component.d.ts +44 -0
- package/src/lib/components/assist-page-change-card.component.js +166 -0
- package/src/lib/components/assist-page-change-card.component.js.map +1 -0
- package/src/lib/components/assist-panel.component.d.ts +105 -1
- package/src/lib/components/assist-panel.component.js +224 -29
- package/src/lib/components/assist-panel.component.js.map +1 -1
- package/src/lib/declarations.server.js +22 -0
- package/src/lib/declarations.server.js.map +1 -1
- package/src/lib/jobs/ai-job-edit-step.d.ts +62 -16
- package/src/lib/jobs/ai-job-edit-step.js +172 -61
- package/src/lib/jobs/ai-job-edit-step.js.map +1 -1
- package/src/lib/jobs/ai-job-import-step.d.ts +21 -2
- package/src/lib/jobs/ai-job-import-step.js +179 -49
- package/src/lib/jobs/ai-job-import-step.js.map +1 -1
- package/src/lib/jobs/ai-job-layout-language.d.ts +27 -3
- package/src/lib/jobs/ai-job-layout-language.js +156 -113
- package/src/lib/jobs/ai-job-layout-language.js.map +1 -1
- package/src/lib/jobs/ai-job-layout-step.js +56 -17
- package/src/lib/jobs/ai-job-layout-step.js.map +1 -1
- package/src/lib/jobs/ai-job-navigation-step.js +4 -2
- package/src/lib/jobs/ai-job-navigation-step.js.map +1 -1
- package/src/lib/jobs/ai-job-page-language.d.ts +25 -15
- package/src/lib/jobs/ai-job-page-language.js +120 -66
- package/src/lib/jobs/ai-job-page-language.js.map +1 -1
- package/src/lib/jobs/ai-job-page-step.js +27 -7
- package/src/lib/jobs/ai-job-page-step.js.map +1 -1
- package/src/lib/jobs/ai-job-site-datasets.d.ts +12 -4
- package/src/lib/jobs/ai-job-site-datasets.js +36 -16
- package/src/lib/jobs/ai-job-site-datasets.js.map +1 -1
- package/src/lib/jobs/ai-jobs.js +6 -2
- package/src/lib/jobs/ai-jobs.js.map +1 -1
- package/src/lib/jobs/ai-layout-stock-photos.d.ts +50 -1
- package/src/lib/jobs/ai-layout-stock-photos.js +586 -35
- package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
- package/src/lib/jobs/ai-site-import-compile.d.ts +147 -0
- package/src/lib/jobs/ai-site-import-compile.js +1143 -0
- package/src/lib/jobs/ai-site-import-compile.js.map +1 -0
- package/src/lib/jobs/ai-site-import-document.d.ts +9 -36
- package/src/lib/jobs/ai-site-import-document.js +23 -489
- package/src/lib/jobs/ai-site-import-document.js.map +1 -1
- package/src/lib/jobs/ai-site-import-look.d.ts +54 -7
- package/src/lib/jobs/ai-site-import-look.js +163 -52
- package/src/lib/jobs/ai-site-import-look.js.map +1 -1
- package/src/lib/jobs/ai-site-import-publish.d.ts +7 -1
- package/src/lib/jobs/ai-site-import-publish.js +19 -5
- package/src/lib/jobs/ai-site-import-publish.js.map +1 -1
- package/src/lib/jobs/ai-site-late-photos.js +4 -2
- package/src/lib/jobs/ai-site-late-photos.js.map +1 -1
- package/src/lib/jobs/ai-site-publish.d.ts +10 -1
- package/src/lib/jobs/ai-site-publish.js +8 -3
- package/src/lib/jobs/ai-site-publish.js.map +1 -1
- package/src/lib/jobs/fixtures/ai-stock-photo-agl4092-1029-recordings.d.ts +23 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-agl4092-1029-recordings.js +677 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-agl4092-1029-recordings.js.map +1 -0
- package/src/lib/layout-language/ai-layout-compiler.d.ts +26 -0
- package/src/lib/layout-language/ai-layout-compiler.js +677 -33
- package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
- package/src/lib/layout-language/ai-layout-frame-hollows.js +5 -1
- package/src/lib/layout-language/ai-layout-frame-hollows.js.map +1 -1
- package/src/lib/layout-language/ai-layout-frame-source.d.ts +196 -0
- package/src/lib/layout-language/ai-layout-frame-source.js +726 -0
- package/src/lib/layout-language/ai-layout-frame-source.js.map +1 -0
- package/src/lib/layout-language/ai-layout-frame.d.ts +22 -0
- package/src/lib/layout-language/ai-layout-frame.js +121 -29
- package/src/lib/layout-language/ai-layout-frame.js.map +1 -1
- package/src/lib/layout-language/ai-layout-language.d.ts +103 -37
- package/src/lib/layout-language/ai-layout-language.js +85 -120
- package/src/lib/layout-language/ai-layout-language.js.map +1 -1
- package/src/lib/model/ai-credit-estimate.d.ts +11 -7
- package/src/lib/model/ai-credit-estimate.js +11 -7
- package/src/lib/model/ai-credit-estimate.js.map +1 -1
- package/src/lib/model/ai-edit-job.d.ts +33 -16
- package/src/lib/model/ai-edit-job.js +50 -34
- package/src/lib/model/ai-edit-job.js.map +1 -1
- package/src/lib/model/ai-job-activity.d.ts +6 -11
- package/src/lib/model/ai-job-activity.js +8 -15
- package/src/lib/model/ai-job-activity.js.map +1 -1
- package/src/lib/model/ai-jobs.types.d.ts +6 -0
- package/src/lib/model/ai-jobs.types.js.map +1 -1
- package/src/lib/model/ai-site-import.d.ts +307 -13
- package/src/lib/model/ai-site-import.js +227 -19
- package/src/lib/model/ai-site-import.js.map +1 -1
- package/src/lib/model/ai-site-inventory.d.ts +6 -0
- package/src/lib/model/ai-site-inventory.js.map +1 -1
- package/src/lib/model/ai-site-look.d.ts +65 -1
- package/src/lib/model/ai-site-look.js +141 -23
- package/src/lib/model/ai-site-look.js.map +1 -1
- package/src/lib/model/ai-site-photos.d.ts +2 -0
- package/src/lib/model/ai-site-photos.js.map +1 -1
- package/src/lib/model/ai-site-start-steps.d.ts +4 -3
- package/src/lib/model/ai-site-start-steps.js +11 -3
- package/src/lib/model/ai-site-start-steps.js.map +1 -1
- package/src/lib/model/ai-site-start.d.ts +23 -2
- package/src/lib/model/ai-site-start.js +25 -3
- package/src/lib/model/ai-site-start.js.map +1 -1
- package/src/lib/model/assist-edit.d.ts +18 -1
- package/src/lib/model/assist-edit.js +7 -0
- package/src/lib/model/assist-edit.js.map +1 -1
- package/src/lib/model/assist-page-change.d.ts +86 -0
- package/src/lib/model/assist-page-change.js +86 -0
- package/src/lib/model/assist-page-change.js.map +1 -0
- package/src/lib/providers/routing.d.ts +11 -0
- package/src/lib/providers/routing.js +23 -0
- package/src/lib/providers/routing.js.map +1 -1
- package/src/lib/runtime/ai-doctrine-validators.d.ts +4 -2
- package/src/lib/runtime/ai-doctrine-validators.js +7 -2
- package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
- package/src/lib/runtime/ai-eval.js +3 -1
- package/src/lib/runtime/ai-eval.js.map +1 -1
- package/src/lib/runtime/ai-icon-library.d.ts +4 -0
- package/src/lib/runtime/ai-icon-library.js +4 -0
- package/src/lib/runtime/ai-icon-library.js.map +1 -1
- package/src/lib/runtime/ai-node-tree.d.ts +5 -0
- package/src/lib/runtime/ai-node-tree.js +23 -4
- package/src/lib/runtime/ai-node-tree.js.map +1 -1
- package/src/lib/runtime/ai-palette.d.ts +2 -0
- package/src/lib/runtime/ai-palette.generated.js +19 -3
- package/src/lib/runtime/ai-palette.generated.js.map +1 -1
- package/src/lib/runtime/ai-palette.js +2 -1
- package/src/lib/runtime/ai-palette.js.map +1 -1
- package/src/lib/runtime/site-inventory.d.ts +8 -1
- package/src/lib/runtime/site-inventory.js +18 -0
- package/src/lib/runtime/site-inventory.js.map +1 -1
- package/src/lib/server/assist-build.js +2 -0
- package/src/lib/server/assist-build.js.map +1 -1
- package/src/lib/server/assist-chat.js +51 -2
- package/src/lib/server/assist-chat.js.map +1 -1
- package/src/lib/server/assist-edit.d.ts +54 -2
- package/src/lib/server/assist-edit.js +219 -30
- package/src/lib/server/assist-edit.js.map +1 -1
- package/src/lib/server/assist-page-change.d.ts +62 -0
- package/src/lib/server/assist-page-change.js +140 -0
- package/src/lib/server/assist-page-change.js.map +1 -0
- package/src/lib/site-import/fixtures/site-layout-recordings.d.ts +50 -0
- package/src/lib/site-import/fixtures/site-layout-recordings.js +86 -0
- package/src/lib/site-import/fixtures/site-layout-recordings.js.map +1 -0
- package/src/lib/site-import/site-import-crawl.d.ts +18 -1
- package/src/lib/site-import/site-import-crawl.js +70 -10
- package/src/lib/site-import/site-import-crawl.js.map +1 -1
- package/src/lib/site-import/site-import-css.d.ts +110 -0
- package/src/lib/site-import/site-import-css.js +826 -0
- package/src/lib/site-import/site-import-css.js.map +1 -0
- package/src/lib/site-import/site-import-html.d.ts +116 -1
- package/src/lib/site-import/site-import-html.js +538 -46
- package/src/lib/site-import/site-import-html.js.map +1 -1
- package/src/lib/site-import/site-import-layout.d.ts +79 -0
- package/src/lib/site-import/site-import-layout.js +1608 -0
- package/src/lib/site-import/site-import-layout.js.map +1 -0
- package/src/lib/site-import/site-import-nav.d.ts +40 -0
- package/src/lib/site-import/site-import-nav.js +437 -0
- package/src/lib/site-import/site-import-nav.js.map +1 -0
- package/src/lib/site-import/site-import-page.d.ts +8 -1
- package/src/lib/site-import/site-import-page.js +24 -2
- package/src/lib/site-import/site-import-page.js.map +1 -1
- package/src/lib/assets/site-styles/beauty.webp +0 -0
- package/src/lib/assets/site-styles/blog.webp +0 -0
- package/src/lib/assets/site-styles/business.webp +0 -0
- package/src/lib/assets/site-styles/coming-soon.webp +0 -0
- package/src/lib/assets/site-styles/education.webp +0 -0
- package/src/lib/assets/site-styles/events.webp +0 -0
- package/src/lib/assets/site-styles/fitness.webp +0 -0
- package/src/lib/assets/site-styles/landing.webp +0 -0
- package/src/lib/assets/site-styles/music.webp +0 -0
- package/src/lib/assets/site-styles/nonprofit.webp +0 -0
- package/src/lib/assets/site-styles/personal.webp +0 -0
- package/src/lib/assets/site-styles/photography.webp +0 -0
- package/src/lib/assets/site-styles/portfolio.webp +0 -0
- package/src/lib/assets/site-styles/professional.webp +0 -0
- package/src/lib/assets/site-styles/realestate.webp +0 -0
- package/src/lib/assets/site-styles/restaurant.webp +0 -0
- package/src/lib/assets/site-styles/store.webp +0 -0
- package/src/lib/assets/site-styles/studio.webp +0 -0
- package/src/lib/assets/site-styles/trades.webp +0 -0
- package/src/lib/assets/site-styles/wellness.webp +0 -0
- package/src/lib/assets/site-styles/yoga.webp +0 -0
- package/src/lib/site-import/site-import-review.d.ts +0 -40
- package/src/lib/site-import/site-import-review.js +0 -149
- package/src/lib/site-import/site-import-review.js.map +0 -1
|
@@ -94,7 +94,8 @@
|
|
|
94
94
|
headline: 120,
|
|
95
95
|
body: 2000,
|
|
96
96
|
button: 40,
|
|
97
|
-
/** A short label, title or caption. */ label: 200
|
|
97
|
+
/** A short label, title or caption. */ label: 200,
|
|
98
|
+
/** A table's grid, every cell's words (AGL-4107): a page brought in carries its source's tables whole. */ table: 20000
|
|
98
99
|
};
|
|
99
100
|
/**
|
|
100
101
|
* Prompt budget per surface catalog, in characters at four per token.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/runtime/ai-palette.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 { JsonLinealOrder } from '@aglyn/aglyn/app-utils/lineal-order'\n\n/**\n * The element palette as a model sees it (AGL-2905).\n *\n * `ai-palette.generated.ts` is produced from the registered plugin bundles by\n * `libs/plugins/ai/scripts/generate-ai-palette.mts` and holds the data; this module\n * holds the shapes that data is typed against and the small readers over it,\n * so a regeneration never rewrites a type.\n */\n\n/** The document kinds a model is asked to compose. */\nexport type AiSurface = 'screen' | 'email' | 'form' | 'layout' | 'component'\n\nexport const AI_SURFACE_NAMES: readonly AiSurface[] = [\n 'screen',\n 'email',\n 'form',\n 'layout',\n 'component',\n]\n\n/**\n * The subset of JSON Schema a prop declaration is expressed in. Enough to\n * validate what an attribute field can persist; nothing the field editor\n * cannot itself model.\n */\nexport interface AiPropSchema {\n type?: 'string' | 'number' | 'integer' | 'boolean'\n enum?: readonly string[]\n minLength?: number\n maxLength?: number\n minimum?: number\n maximum?: number\n pattern?: string\n description?: string\n}\n\nexport interface AiPropsSchema {\n type: 'object'\n properties: Record<string, AiPropSchema>\n required: readonly string[]\n additionalProperties: false\n}\n\n/**\n * What a string prop MEANS, which decides how the validator checks it.\n *\n * - `text` — copy the visitor reads; held to a length ceiling.\n * - `url` — a navigable address: an `https:` URL or a root-relative path.\n * - `screen` — a screen id, checked against the screens the host has.\n * - `media` — an image or video source: a DAM reference or an `https:` URL.\n */\nexport type AiPropRole = 'text' | 'url' | 'screen' | 'media'\n\n/**\n * A `restrictChildren` / `restrictParent` directive as generated data: the\n * same tuple the schema declares, with the flag spelled as the string the\n * enum holds so a JSON emitter can write it.\n */\nexport type AiLinealOrder = JsonLinealOrder\n\nexport interface AiPaletteEntry {\n pluginId: string\n kind: 'element' | 'plaintext' | 'markdown'\n category: string\n displayName: string\n /** One sentence, from the schema's own description. */\n summary: string\n /** Whether the editor lets anything be dropped inside this element. */\n acceptsChildren: boolean\n restrictChildren?: AiLinealOrder\n restrictParent?: AiLinealOrder\n propsSchema: AiPropsSchema\n /** Roles of the string props the validator treats specially. */\n propRoles: Record<string, AiPropRole>\n /**\n * The attribute field kind each declared prop is edited with, as its\n * `FieldComponentType` value (`switch`, `select`, `screen-select`, …): what\n * the editor reads to decide which component properties a field can be\n * bound to (AGL-2908).\n */\n propFields: Record<string, string>\n /** Length ceilings for `text` props, in characters. */\n textLimits: Record<string, number>\n /** Names of the presets that place this component. */\n presets: readonly string[]\n}\n\n/**\n * What every NODE can do, as against what one component declares (AGL-3156).\n *\n * Generated from the node capabilities core declares, beside the per-component\n * schemas rather than out of them: repeating is not a Stack's feature, so no\n * component schema names it and a palette built from schemas alone advertises\n * it on nothing. Declared ONCE for the whole palette — a model may write these\n * props on any element the surface allows.\n */\nexport interface AiNodeCapability {\n /** Stable id, as core declares it. */\n id: string\n displayName: string\n /** One sentence, for the prompt catalog. */\n summary: string\n propsSchema: AiPropsSchema\n propRoles: Record<string, AiPropRole>\n propFields: Record<string, string>\n textLimits: Record<string, number>\n /** Props offered only on a node that holds a child list. */\n childrenOnlyProps: readonly string[]\n /**\n * Declared attributes the palette deliberately does not offer: a picker\n * that chooses a record the site holds, which a model cannot name from a\n * description. Recorded so a field left off on purpose reads differently\n * from one that went missing.\n */\n omittedProps: readonly string[]\n}\n\n/**\n * The capability props a node with this child contract may carry, merged into\n * one map the prop sanitizer can read beside the element's own declarations.\n */\nexport function nodeCapabilityProps(\n capabilities: Readonly<Record<string, AiNodeCapability>>,\n acceptsChildren: boolean,\n): {\n properties: Record<string, AiPropSchema>\n propRoles: Record<string, AiPropRole>\n textLimits: Record<string, number>\n} {\n const properties: Record<string, AiPropSchema> = {}\n const propRoles: Record<string, AiPropRole> = {}\n const textLimits: Record<string, number> = {}\n for (const capability of Object.values(capabilities)) {\n for (const [name, schema] of Object.entries(\n capability.propsSchema.properties,\n )) {\n if (!acceptsChildren && capability.childrenOnlyProps.includes(name))\n continue\n properties[name] = schema\n const role = capability.propRoles[name]\n if (role) propRoles[name] = role\n const limit = capability.textLimits[name]\n if (limit !== undefined) textLimits[name] = limit\n }\n }\n return { properties, propRoles, textLimits }\n}\n\n/**\n * What a node capability declared in core and one carried by the palette\n * disagree about (AGL-3156).\n *\n * `check:ai-palette` pins the generated file to the generator's output for its\n * inputs, which cannot notice a capability leaving those inputs: when repeat\n * stopped being a component's attribute, the generator regenerated cleanly and\n * the palette simply stopped mentioning it. This reads the core declarations\n * instead and asks whether the palette still accounts for each one, so the\n * next generic capability cannot vanish the same way.\n *\n * `offeredFieldKinds` is every field kind the palette DOES carry somewhere,\n * derived from the generated entries rather than restated here: a capability\n * attribute drawn with one of those kinds has no excuse for being omitted.\n */\nexport function auditNodeCapabilities(\n declared: ReadonlyArray<{\n id: string\n label: string\n attributes: ReadonlyArray<{ name: string; component: string }>\n }>,\n carried: Readonly<Record<string, AiNodeCapability>>,\n offeredFieldKinds: ReadonlySet<string>,\n): string[] {\n const problems: string[] = []\n for (const capability of declared) {\n const entry = carried[capability.id]\n if (!entry) {\n problems.push(\n `the palette carries no node capability \"${capability.id}\": a model cannot use it at all`,\n )\n continue\n }\n if (!Object.keys(entry.propsSchema.properties).length) {\n problems.push(\n `node capability \"${capability.id}\" is carried with no prop a model can write`,\n )\n }\n for (const attribute of capability.attributes) {\n if (entry.propsSchema.properties[attribute.name]) continue\n if (!entry.omittedProps.includes(attribute.name)) {\n problems.push(\n `node capability \"${capability.id}\" declares \"${attribute.name}\", which the palette neither offers nor records as omitted`,\n )\n continue\n }\n if (offeredFieldKinds.has(attribute.component)) {\n problems.push(\n `node capability \"${capability.id}\" omits \"${attribute.name}\", whose \"${attribute.component}\" field the palette offers elsewhere`,\n )\n }\n }\n }\n for (const id of Object.keys(carried)) {\n if (!declared.some((capability) => capability.id === id)) {\n problems.push(\n `the palette carries node capability \"${id}\", which core no longer declares`,\n )\n }\n }\n return problems\n}\n\nexport interface AiSurfaceDefinition {\n /** The component id at the top of a tree for this surface. */\n root: string\n /** Every component id a node in this surface may carry. */\n allow: readonly string[]\n /**\n * Component ids admitted only in a tree the platform's own code composed\n * (AGL-3660), never listed in the surface's catalog: see\n * `AiNodeTreeContext.codeBuilt`.\n */\n codeOnly?: readonly string[]\n}\n\n/**\n * Theme vocabulary an `sx` value may name, read off the theme sources so the\n * validator's allowlist is derived rather than typed by hand.\n */\nexport interface AiSxTokens {\n /** Dotted palette paths such as `primary.main`. */\n palette: readonly string[]\n /** Responsive object keys. */\n breakpoints: readonly string[]\n /** Values `typography` may take. */\n typographyVariants: readonly string[]\n}\n\n/**\n * Text ceilings per role of copy (AGL-2905). A headline is one line of a\n * hero, body copy is a paragraph or two, and a button label has to fit a\n * button.\n */\nexport const AI_TEXT_LIMITS = {\n headline: 120,\n body: 2_000,\n button: 40,\n /** A short label, title or caption. */\n label: 200,\n} as const\n\n/**\n * Prompt budget per surface catalog, in characters at four per token.\n * Asserted by a spec so a component that grows its description cannot\n * silently push a system prompt past what the route pays for.\n */\nexport const AI_PALETTE_CATALOG_MAX_TOKENS = 2_500\nexport const AI_PALETTE_CATALOG_MAX_CHARS = AI_PALETTE_CATALOG_MAX_TOKENS * 4\n\n/** The model's own token estimate: characters over four. */\nexport function estimateCatalogTokens(catalog: string): number {\n return Math.ceil(catalog.length / 4)\n}\n\n/**\n * The documents a generator emits (AGL-2935). Each is composed on one\n * surface of the palette and held to its own budget below.\n */\nexport type AiOutputKind =\n | 'page'\n | 'template'\n | 'component'\n | 'layout'\n | 'form'\n | 'email'\n\nexport const AI_OUTPUT_KINDS: readonly AiOutputKind[] = [\n 'page',\n 'template',\n 'component',\n 'layout',\n 'form',\n 'email',\n]\n\n/** The surface each output kind is composed on. */\nexport const AI_OUTPUT_SURFACE: Record<AiOutputKind, AiSurface> = {\n page: 'screen',\n template: 'screen',\n component: 'component',\n layout: 'layout',\n form: 'form',\n email: 'email',\n}\n\n/**\n * What the rule-17 scorer measures on an emitted tree:\n *\n * - `nodes` — how many elements the tree carries;\n * - `bytes` — the stored node map, in the unit the save ceiling uses\n * (`nodeMapBytes`, msgpack);\n * - `imageBytes` — what the placed library images request at the slot's\n * variant, from each asset's recorded size;\n * - `embeds` — third-party players and frames the output loads;\n * - `fontFamilies` — families named beyond the theme's own;\n * - `emailHtmlBytes` — an email's rendered HTML, which is what a mail\n * client clips.\n */\nexport type AiBudgetMetric =\n | 'nodes'\n | 'bytes'\n | 'imageBytes'\n | 'embeds'\n | 'fontFamilies'\n | 'emailHtmlBytes'\n\nexport type AiOutputBudget = Record<Exclude<AiBudgetMetric, 'emailHtmlBytes'>, number> & {\n emailHtmlBytes?: number\n}\n\n/**\n * The size past which Gmail clips a message and hides the rest behind a\n * \"View entire message\" link — the unsubscribe footer included.\n */\nexport const AI_EMAIL_CLIP_BYTES = 102_000\n\n/**\n * The budget per output kind (AGL-2935, rule 17), beside the catalog budget\n * above for the same reason: a generator that grows its output past what\n * its kind is for is caught by a number, not noticed by a reader.\n *\n * A page is a few hundred elements and tens of kilobytes of stored map; a\n * reusable component is one repeated block; a layout is the chrome every\n * page loads, so it carries the least imagery; a form is fields and a\n * button — each field a Form Control over a Form Label and its answer, a\n * choice a Choice Label over its Radio or Checkbox, so a form of a few dozen\n * questions is a hundred-odd elements; an email is held to the clipping threshold on its rendered HTML.\n * No kind names a font beyond the theme's.\n *\n * A third-party player is a page's or a component's to carry, and only where\n * a confirmed plan lists it (rule 16). A template is rendered for every\n * record it serves and a layout around every page, so neither carries one\n * (AGL-3433): an embed there loads a stranger's code on every page of a\n * collection or a site.\n */\nexport const AI_OUTPUT_BUDGETS: Record<AiOutputKind, AiOutputBudget> = {\n page: {\n nodes: 400,\n bytes: 60_000,\n imageBytes: 1_500_000,\n embeds: 1,\n fontFamilies: 0,\n },\n template: {\n nodes: 400,\n bytes: 60_000,\n imageBytes: 1_500_000,\n embeds: 0,\n fontFamilies: 0,\n },\n component: {\n nodes: 80,\n bytes: 12_000,\n imageBytes: 500_000,\n embeds: 1,\n fontFamilies: 0,\n },\n layout: {\n nodes: 160,\n bytes: 24_000,\n imageBytes: 200_000,\n embeds: 0,\n fontFamilies: 0,\n },\n form: {\n nodes: 150,\n bytes: 15_000,\n imageBytes: 0,\n embeds: 0,\n fontFamilies: 0,\n },\n email: {\n nodes: 150,\n bytes: 24_000,\n imageBytes: 1_000_000,\n embeds: 0,\n fontFamilies: 0,\n emailHtmlBytes: AI_EMAIL_CLIP_BYTES,\n },\n}\n\n/**\n * What a first visit to a generated output is estimated to transfer — the\n * weight the proposal shows before anything is applied (rule 17). A page\n * starts from the platform's measured page weight; a component or a layout\n * reports only what it adds to a page; an email is not a page load.\n */\nexport interface AiLoadEstimate {\n /** The measured weight of a published page before this output's own bytes; 0 for a fragment. */\n pageBytes: number\n /** The stored node map, in the save's own bytes. */\n documentBytes: number\n /** What the placed library images request at their slots. */\n imageBytes: number\n /** Images the estimate could not size: no recorded size, or not from the library. */\n imagesUnmeasured: number\n embeds: number\n totalBytes: number\n}\n\n/** Typography variants that read as headlines, for the `children` ceiling. */\nexport function isHeadlineVariant(variant: unknown): boolean {\n return typeof variant === 'string' && /^(display\\w*|h[1-6]|heading)$/.test(variant)\n}\n"],"names":["AI_SURFACE_NAMES","nodeCapabilityProps","capabilities","acceptsChildren","properties","propRoles","textLimits","capability","Object","values","name","schema","entries","propsSchema","childrenOnlyProps","includes","role","limit","undefined","auditNodeCapabilities","declared","carried","offeredFieldKinds","problems","entry","id","push","keys","length","attribute","attributes","omittedProps","has","component","some","AI_TEXT_LIMITS","headline","body","button","label","AI_PALETTE_CATALOG_MAX_TOKENS","AI_PALETTE_CATALOG_MAX_CHARS","estimateCatalogTokens","catalog","Math","ceil","AI_OUTPUT_KINDS","AI_OUTPUT_SURFACE","page","template","layout","form","email","AI_EMAIL_CLIP_BYTES","AI_OUTPUT_BUDGETS","nodes","bytes","imageBytes","embeds","fontFamilies","emailHtmlBytes","isHeadlineVariant","variant","test"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/runtime/ai-palette.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 { JsonLinealOrder } from '@aglyn/aglyn/app-utils/lineal-order'\n\n/**\n * The element palette as a model sees it (AGL-2905).\n *\n * `ai-palette.generated.ts` is produced from the registered plugin bundles by\n * `libs/plugins/ai/scripts/generate-ai-palette.mts` and holds the data; this module\n * holds the shapes that data is typed against and the small readers over it,\n * so a regeneration never rewrites a type.\n */\n\n/** The document kinds a model is asked to compose. */\nexport type AiSurface = 'screen' | 'email' | 'form' | 'layout' | 'component'\n\nexport const AI_SURFACE_NAMES: readonly AiSurface[] = [\n 'screen',\n 'email',\n 'form',\n 'layout',\n 'component',\n]\n\n/**\n * The subset of JSON Schema a prop declaration is expressed in. Enough to\n * validate what an attribute field can persist; nothing the field editor\n * cannot itself model.\n */\nexport interface AiPropSchema {\n type?: 'string' | 'number' | 'integer' | 'boolean'\n enum?: readonly string[]\n minLength?: number\n maxLength?: number\n minimum?: number\n maximum?: number\n pattern?: string\n description?: string\n}\n\nexport interface AiPropsSchema {\n type: 'object'\n properties: Record<string, AiPropSchema>\n required: readonly string[]\n additionalProperties: false\n}\n\n/**\n * What a string prop MEANS, which decides how the validator checks it.\n *\n * - `text` — copy the visitor reads; held to a length ceiling.\n * - `url` — a navigable address: an `https:` URL or a root-relative path.\n * - `screen` — a screen id, checked against the screens the host has.\n * - `media` — an image or video source: a DAM reference or an `https:` URL.\n */\nexport type AiPropRole = 'text' | 'url' | 'screen' | 'media'\n\n/**\n * A `restrictChildren` / `restrictParent` directive as generated data: the\n * same tuple the schema declares, with the flag spelled as the string the\n * enum holds so a JSON emitter can write it.\n */\nexport type AiLinealOrder = JsonLinealOrder\n\nexport interface AiPaletteEntry {\n pluginId: string\n kind: 'element' | 'plaintext' | 'markdown'\n category: string\n displayName: string\n /** One sentence, from the schema's own description. */\n summary: string\n /** Whether the editor lets anything be dropped inside this element. */\n acceptsChildren: boolean\n restrictChildren?: AiLinealOrder\n restrictParent?: AiLinealOrder\n propsSchema: AiPropsSchema\n /** Roles of the string props the validator treats specially. */\n propRoles: Record<string, AiPropRole>\n /**\n * The attribute field kind each declared prop is edited with, as its\n * `FieldComponentType` value (`switch`, `select`, `screen-select`, …): what\n * the editor reads to decide which component properties a field can be\n * bound to (AGL-2908).\n */\n propFields: Record<string, string>\n /** Length ceilings for `text` props, in characters. */\n textLimits: Record<string, number>\n /** Names of the presets that place this component. */\n presets: readonly string[]\n}\n\n/**\n * What every NODE can do, as against what one component declares (AGL-3156).\n *\n * Generated from the node capabilities core declares, beside the per-component\n * schemas rather than out of them: repeating is not a Stack's feature, so no\n * component schema names it and a palette built from schemas alone advertises\n * it on nothing. Declared ONCE for the whole palette — a model may write these\n * props on any element the surface allows.\n */\nexport interface AiNodeCapability {\n /** Stable id, as core declares it. */\n id: string\n displayName: string\n /** One sentence, for the prompt catalog. */\n summary: string\n propsSchema: AiPropsSchema\n propRoles: Record<string, AiPropRole>\n propFields: Record<string, string>\n textLimits: Record<string, number>\n /** Props offered only on a node that holds a child list. */\n childrenOnlyProps: readonly string[]\n /**\n * Declared attributes the palette deliberately does not offer: a picker\n * that chooses a record the site holds, which a model cannot name from a\n * description. Recorded so a field left off on purpose reads differently\n * from one that went missing.\n */\n omittedProps: readonly string[]\n}\n\n/**\n * The capability props a node with this child contract may carry, merged into\n * one map the prop sanitizer can read beside the element's own declarations.\n */\nexport function nodeCapabilityProps(\n capabilities: Readonly<Record<string, AiNodeCapability>>,\n acceptsChildren: boolean,\n): {\n properties: Record<string, AiPropSchema>\n propRoles: Record<string, AiPropRole>\n textLimits: Record<string, number>\n} {\n const properties: Record<string, AiPropSchema> = {}\n const propRoles: Record<string, AiPropRole> = {}\n const textLimits: Record<string, number> = {}\n for (const capability of Object.values(capabilities)) {\n for (const [name, schema] of Object.entries(\n capability.propsSchema.properties,\n )) {\n if (!acceptsChildren && capability.childrenOnlyProps.includes(name))\n continue\n properties[name] = schema\n const role = capability.propRoles[name]\n if (role) propRoles[name] = role\n const limit = capability.textLimits[name]\n if (limit !== undefined) textLimits[name] = limit\n }\n }\n return { properties, propRoles, textLimits }\n}\n\n/**\n * What a node capability declared in core and one carried by the palette\n * disagree about (AGL-3156).\n *\n * `check:ai-palette` pins the generated file to the generator's output for its\n * inputs, which cannot notice a capability leaving those inputs: when repeat\n * stopped being a component's attribute, the generator regenerated cleanly and\n * the palette simply stopped mentioning it. This reads the core declarations\n * instead and asks whether the palette still accounts for each one, so the\n * next generic capability cannot vanish the same way.\n *\n * `offeredFieldKinds` is every field kind the palette DOES carry somewhere,\n * derived from the generated entries rather than restated here: a capability\n * attribute drawn with one of those kinds has no excuse for being omitted.\n */\nexport function auditNodeCapabilities(\n declared: ReadonlyArray<{\n id: string\n label: string\n attributes: ReadonlyArray<{ name: string; component: string }>\n }>,\n carried: Readonly<Record<string, AiNodeCapability>>,\n offeredFieldKinds: ReadonlySet<string>,\n): string[] {\n const problems: string[] = []\n for (const capability of declared) {\n const entry = carried[capability.id]\n if (!entry) {\n problems.push(\n `the palette carries no node capability \"${capability.id}\": a model cannot use it at all`,\n )\n continue\n }\n if (!Object.keys(entry.propsSchema.properties).length) {\n problems.push(\n `node capability \"${capability.id}\" is carried with no prop a model can write`,\n )\n }\n for (const attribute of capability.attributes) {\n if (entry.propsSchema.properties[attribute.name]) continue\n if (!entry.omittedProps.includes(attribute.name)) {\n problems.push(\n `node capability \"${capability.id}\" declares \"${attribute.name}\", which the palette neither offers nor records as omitted`,\n )\n continue\n }\n if (offeredFieldKinds.has(attribute.component)) {\n problems.push(\n `node capability \"${capability.id}\" omits \"${attribute.name}\", whose \"${attribute.component}\" field the palette offers elsewhere`,\n )\n }\n }\n }\n for (const id of Object.keys(carried)) {\n if (!declared.some((capability) => capability.id === id)) {\n problems.push(\n `the palette carries node capability \"${id}\", which core no longer declares`,\n )\n }\n }\n return problems\n}\n\nexport interface AiSurfaceDefinition {\n /** The component id at the top of a tree for this surface. */\n root: string\n /** Every component id a node in this surface may carry. */\n allow: readonly string[]\n /**\n * Component ids admitted only in a tree the platform's own code composed\n * (AGL-3660), never listed in the surface's catalog: see\n * `AiNodeTreeContext.codeBuilt`.\n */\n codeOnly?: readonly string[]\n}\n\n/**\n * Theme vocabulary an `sx` value may name, read off the theme sources so the\n * validator's allowlist is derived rather than typed by hand.\n */\nexport interface AiSxTokens {\n /** Dotted palette paths such as `primary.main`. */\n palette: readonly string[]\n /** Responsive object keys. */\n breakpoints: readonly string[]\n /** Values `typography` may take. */\n typographyVariants: readonly string[]\n}\n\n/**\n * Text ceilings per role of copy (AGL-2905). A headline is one line of a\n * hero, body copy is a paragraph or two, and a button label has to fit a\n * button.\n */\nexport const AI_TEXT_LIMITS = {\n headline: 120,\n body: 2_000,\n button: 40,\n /** A short label, title or caption. */\n label: 200,\n /** A table's grid, every cell's words (AGL-4107): a page brought in carries its source's tables whole. */\n table: 20_000,\n} as const\n\n/**\n * Prompt budget per surface catalog, in characters at four per token.\n * Asserted by a spec so a component that grows its description cannot\n * silently push a system prompt past what the route pays for.\n */\nexport const AI_PALETTE_CATALOG_MAX_TOKENS = 2_500\nexport const AI_PALETTE_CATALOG_MAX_CHARS = AI_PALETTE_CATALOG_MAX_TOKENS * 4\n\n/** The model's own token estimate: characters over four. */\nexport function estimateCatalogTokens(catalog: string): number {\n return Math.ceil(catalog.length / 4)\n}\n\n/**\n * The documents a generator emits (AGL-2935). Each is composed on one\n * surface of the palette and held to its own budget below.\n */\nexport type AiOutputKind =\n | 'page'\n | 'template'\n | 'component'\n | 'layout'\n | 'form'\n | 'email'\n\nexport const AI_OUTPUT_KINDS: readonly AiOutputKind[] = [\n 'page',\n 'template',\n 'component',\n 'layout',\n 'form',\n 'email',\n]\n\n/** The surface each output kind is composed on. */\nexport const AI_OUTPUT_SURFACE: Record<AiOutputKind, AiSurface> = {\n page: 'screen',\n template: 'screen',\n component: 'component',\n layout: 'layout',\n form: 'form',\n email: 'email',\n}\n\n/**\n * What the rule-17 scorer measures on an emitted tree:\n *\n * - `nodes` — how many elements the tree carries;\n * - `bytes` — the stored node map, in the unit the save ceiling uses\n * (`nodeMapBytes`, msgpack);\n * - `imageBytes` — what the placed library images request at the slot's\n * variant, from each asset's recorded size;\n * - `embeds` — third-party players and frames the output loads;\n * - `fontFamilies` — families named beyond the theme's own;\n * - `emailHtmlBytes` — an email's rendered HTML, which is what a mail\n * client clips.\n */\nexport type AiBudgetMetric =\n | 'nodes'\n | 'bytes'\n | 'imageBytes'\n | 'embeds'\n | 'fontFamilies'\n | 'emailHtmlBytes'\n\nexport type AiOutputBudget = Record<Exclude<AiBudgetMetric, 'emailHtmlBytes'>, number> & {\n emailHtmlBytes?: number\n}\n\n/**\n * The size past which Gmail clips a message and hides the rest behind a\n * \"View entire message\" link — the unsubscribe footer included.\n */\nexport const AI_EMAIL_CLIP_BYTES = 102_000\n\n/**\n * The budget per output kind (AGL-2935, rule 17), beside the catalog budget\n * above for the same reason: a generator that grows its output past what\n * its kind is for is caught by a number, not noticed by a reader.\n *\n * A page is a few hundred elements and tens of kilobytes of stored map; a\n * reusable component is one repeated block; a layout is the chrome every\n * page loads, so it carries the least imagery; a form is fields and a\n * button — each field a Form Control over a Form Label and its answer, a\n * choice a Choice Label over its Radio or Checkbox, so a form of a few dozen\n * questions is a hundred-odd elements; an email is held to the clipping threshold on its rendered HTML.\n * No kind names a font beyond the theme's.\n *\n * A third-party player is a page's or a component's to carry, and only where\n * a confirmed plan lists it (rule 16). A template is rendered for every\n * record it serves and a layout around every page, so neither carries one\n * (AGL-3433): an embed there loads a stranger's code on every page of a\n * collection or a site.\n */\nexport const AI_OUTPUT_BUDGETS: Record<AiOutputKind, AiOutputBudget> = {\n page: {\n nodes: 400,\n bytes: 60_000,\n imageBytes: 1_500_000,\n embeds: 1,\n fontFamilies: 0,\n },\n template: {\n nodes: 400,\n bytes: 60_000,\n imageBytes: 1_500_000,\n embeds: 0,\n fontFamilies: 0,\n },\n component: {\n nodes: 80,\n bytes: 12_000,\n imageBytes: 500_000,\n embeds: 1,\n fontFamilies: 0,\n },\n layout: {\n nodes: 160,\n bytes: 24_000,\n imageBytes: 200_000,\n embeds: 0,\n fontFamilies: 0,\n },\n form: {\n nodes: 150,\n bytes: 15_000,\n imageBytes: 0,\n embeds: 0,\n fontFamilies: 0,\n },\n email: {\n nodes: 150,\n bytes: 24_000,\n imageBytes: 1_000_000,\n embeds: 0,\n fontFamilies: 0,\n emailHtmlBytes: AI_EMAIL_CLIP_BYTES,\n },\n}\n\n/**\n * What a first visit to a generated output is estimated to transfer — the\n * weight the proposal shows before anything is applied (rule 17). A page\n * starts from the platform's measured page weight; a component or a layout\n * reports only what it adds to a page; an email is not a page load.\n */\nexport interface AiLoadEstimate {\n /** The measured weight of a published page before this output's own bytes; 0 for a fragment. */\n pageBytes: number\n /** The stored node map, in the save's own bytes. */\n documentBytes: number\n /** What the placed library images request at their slots. */\n imageBytes: number\n /** Images the estimate could not size: no recorded size, or not from the library. */\n imagesUnmeasured: number\n embeds: number\n totalBytes: number\n}\n\n/** Typography variants that read as headlines, for the `children` ceiling. */\nexport function isHeadlineVariant(variant: unknown): boolean {\n return typeof variant === 'string' && /^(display\\w*|h[1-6]|heading)$/.test(variant)\n}\n"],"names":["AI_SURFACE_NAMES","nodeCapabilityProps","capabilities","acceptsChildren","properties","propRoles","textLimits","capability","Object","values","name","schema","entries","propsSchema","childrenOnlyProps","includes","role","limit","undefined","auditNodeCapabilities","declared","carried","offeredFieldKinds","problems","entry","id","push","keys","length","attribute","attributes","omittedProps","has","component","some","AI_TEXT_LIMITS","headline","body","button","label","table","AI_PALETTE_CATALOG_MAX_TOKENS","AI_PALETTE_CATALOG_MAX_CHARS","estimateCatalogTokens","catalog","Math","ceil","AI_OUTPUT_KINDS","AI_OUTPUT_SURFACE","page","template","layout","form","email","AI_EMAIL_CLIP_BYTES","AI_OUTPUT_BUDGETS","nodes","bytes","imageBytes","embeds","fontFamilies","emailHtmlBytes","isHeadlineVariant","variant","test"],"mappings":"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"}
|
|
@@ -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 AiInventoryScreen, type AiInventoryTheme, type AiSiteInventory } from '../model/ai-site-inventory';
|
|
17
|
+
import { type AiInventoryDataset, type AiInventoryScreen, type AiInventoryTheme, type AiSiteInventory } from '../model/ai-site-inventory';
|
|
18
18
|
/**
|
|
19
19
|
* The reader behind the site inventory (AGL-2935): one site's components,
|
|
20
20
|
* layouts, templates, forms, datasets, content collections, screens and
|
|
@@ -73,6 +73,13 @@ export declare function aiInventoryRecordPage(fact: unknown, fields: ReadonlyArr
|
|
|
73
73
|
recordBase: string;
|
|
74
74
|
addressField: string;
|
|
75
75
|
} | null;
|
|
76
|
+
/**
|
|
77
|
+
* The datasets shared with one site (AGL-3616), alone: what an edit to one of
|
|
78
|
+
* its pages may repeat an element over. The caller has already held the site
|
|
79
|
+
* to the org; a failed read answers none, and the edit admits only the
|
|
80
|
+
* repeats already on its canvas.
|
|
81
|
+
*/
|
|
82
|
+
export declare function readAiSiteDatasets(orgId: string, hostId: string): Promise<AiInventoryDataset[]>;
|
|
76
83
|
/**
|
|
77
84
|
* Read one site's inventory for a generation.
|
|
78
85
|
*
|
|
@@ -150,6 +150,7 @@ async function readWindow(query, fields, cap, map) {
|
|
|
150
150
|
// The record page the site already serves for it (AGL-3747), whose
|
|
151
151
|
// address field is the one its pages are reached by.
|
|
152
152
|
const page = aiInventoryRecordPage(record.facts['recordPage'], fields);
|
|
153
|
+
const recordScreenId = page ? text(record.facts['recordPage']['screenId']) : '';
|
|
153
154
|
const address = (_ref = page == null ? void 0 : page.addressField) != null ? _ref : (_fields_find = fields.find((field)=>field.address && field.id)) == null ? void 0 : _fields_find.id;
|
|
154
155
|
// Made for this site, where its scope names this site alone (AGL-3912).
|
|
155
156
|
const visibleTo = Array.isArray(record.facts['visibleTo']) ? record.facts['visibleTo'] : [];
|
|
@@ -165,6 +166,8 @@ async function readWindow(query, fields, cap, map) {
|
|
|
165
166
|
addressField: address
|
|
166
167
|
} : {}, page ? {
|
|
167
168
|
recordBase: page.recordBase
|
|
169
|
+
} : {}, recordScreenId ? {
|
|
170
|
+
recordScreenId
|
|
168
171
|
} : {}, siteOnly ? {
|
|
169
172
|
siteOnly: true
|
|
170
173
|
} : {});
|
|
@@ -172,6 +175,21 @@ async function readWindow(query, fields, cap, map) {
|
|
|
172
175
|
truncated
|
|
173
176
|
};
|
|
174
177
|
}
|
|
178
|
+
/**
|
|
179
|
+
* The datasets shared with one site (AGL-3616), alone: what an edit to one of
|
|
180
|
+
* its pages may repeat an element over. The caller has already held the site
|
|
181
|
+
* to the org; a failed read answers none, and the edit admits only the
|
|
182
|
+
* repeats already on its canvas.
|
|
183
|
+
*/ export async function readAiSiteDatasets(orgId, hostId) {
|
|
184
|
+
return readIndexedDatasets(orgId, hostId, AI_SITE_INVENTORY_MAX_PER_KIND).then((window)=>window.rows).catch((error)=>{
|
|
185
|
+
console.error('ai site datasets read failed', {
|
|
186
|
+
orgId,
|
|
187
|
+
hostId,
|
|
188
|
+
error
|
|
189
|
+
});
|
|
190
|
+
return [];
|
|
191
|
+
});
|
|
192
|
+
}
|
|
175
193
|
/**
|
|
176
194
|
* Read one site's inventory for a generation.
|
|
177
195
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/runtime/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\nimport { hostCollectionKind } from '@aglyn/aglyn/app-utils/collection-kind'\nimport { isFormArchived } from '@aglyn/aglyn/app-utils/forms'\nimport {\n describeTheme,\n resolveSiteTheme,\n type ThemeHostDocument,\n} from '@aglyn/aglyn/app-utils/site-theme'\nimport {\n REUSABLE_COMPONENT_KIND_EMAIL,\n reusableComponentKindOf,\n} from '@aglyn/aglyn/app-utils/reusable-component-kind'\nimport { SCREEN_KIND_TEMPLATE, SCREEN_ROOT_PATH } from '@aglyn/aglyn/app-utils/screen-route'\nimport { pluginRecordIndex } from '@aglyn/aglyn/plugin-manager/plugin-record-index'\nimport { hostScopeToken } from '@aglyn/aglyn/app-utils/scope-tokens'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin/server/firebase-admin'\nimport { resolveOrgIdForHost } from '@aglyn/tenant-data-admin/server/organizations'\nimport {\n AI_INVENTORY_KINDS,\n AI_SITE_INVENTORY_MAX_PER_KIND,\n AiInventoryScopeError,\n type AiComponentPropTypes,\n type AiInventoryCollection,\n type AiInventoryComponent,\n type AiInventoryDataset,\n type AiInventoryForm,\n type AiInventoryKind,\n type AiInventoryLayout,\n type AiInventoryScreen,\n type AiInventoryTemplate,\n type AiInventoryTheme,\n type AiSiteInventory,\n} from '../model/ai-site-inventory'\nimport { aiBracketedFacts } from './ai-doctrine-validators'\n\n/**\n * The reader behind the site inventory (AGL-2935): one site's components,\n * layouts, templates, forms, datasets, content collections, screens and\n * theme, read as projections through the Admin SDK and reduced to the\n * compact shape `model/ai-site-inventory.ts` describes.\n *\n * Every kind is a projection (`select`) of the fields the inventory names —\n * never a node map, never a record's content — read one row past the cap so\n * a full page is told apart from a cut one. Retired rows (soft-deleted,\n * archived, a component never published) are filtered after the read, so a\n * site with many retired rows can list fewer than the cap; the kind is still\n * marked truncated when the read was cut, which is the honest answer.\n *\n * The window is WIDER than the prompt lists (AGL-2937). The prompt block\n * lists `AI_SITE_INVENTORY_LISTED_PER_KIND` records of a kind because every\n * character of it is billed on every attempt; the rest of the window is what\n * the lookup tool searches, and a record never read cannot be found. The\n * difference is paid in document reads rather than tokens — a projection of\n * a few fields, once per job step — which is the cheaper of the two by some\n * orders of magnitude.\n */\n\ntype Firestore = FirebaseFirestore.Firestore\ntype Data = Record<string, unknown>\n\nexport interface ReadSiteInventoryOptions {\n /** The Admin SDK handle; the platform's default app when absent. */\n firestore?: Firestore\n /** Rows read per kind; `AI_SITE_INVENTORY_MAX_PER_KIND` when absent, and never above it. */\n maxPerKind?: number\n}\n\nfunction text(value: unknown): string {\n return typeof value === 'string' ? value.trim() : ''\n}\n\n/** A record's name, or its id when it was saved without one. */\nfunction nameOf(data: Data, id: string, ...fields: string[]): string {\n for (const field of fields) {\n const value = text(data[field])\n if (value) return value\n }\n return id\n}\n\n/** The flat `path → color` leaves of one scheme, e.g. `primary.main`. */\nfunction schemeColors(scheme: unknown): Record<string, string> {\n const colors: Record<string, string> = {}\n const visit = (value: unknown, path: string) => {\n if (typeof value === 'string') {\n if (path) colors[path] = value\n return\n }\n if (!value || typeof value !== 'object' || Array.isArray(value)) return\n for (const [key, inner] of Object.entries(value as Data)) {\n visit(inner, path ? `${path}.${key}` : key)\n }\n }\n visit(scheme, '')\n return colors\n}\n\n/** The theme a site renders with, reduced to what a plan and an email can act on. */\nexport function aiInventoryTheme(host: Data | null): AiInventoryTheme | null {\n const theme = resolveSiteTheme(host as ThemeHostDocument | null)\n if (!theme) return null\n const fonts = new Set<string>()\n for (const font of theme.fonts ?? []) {\n if (text(font?.family)) fonts.add(text(font.family))\n }\n return {\n summary: describeTheme(theme),\n colors: schemeColors(theme.colorSchemes?.light),\n fonts: [...fonts],\n }\n}\n\ninterface Window<T> {\n rows: T[]\n truncated: boolean\n}\n\nasync function readWindow<T>(\n query: FirebaseFirestore.Query,\n fields: string[],\n cap: number,\n map: (id: string, data: Data) => T | null,\n): Promise<Window<T>> {\n const snapshot = await query\n .select(...fields)\n .limit(cap + 1)\n .get()\n const rows: T[] = []\n for (const doc of snapshot.docs.slice(0, cap)) {\n const row = map(doc.id, (doc.data() ?? {}) as Data)\n if (row) rows.push(row)\n }\n return { rows, truncated: snapshot.docs.length > cap }\n}\n\n/**\n * The screen a site's starter home may be (AGL-3594, AGL-3408): the one\n * `host.defaultHomeScreenId` names, while it still answers `/` in the routing\n * map and is listed as a page. `null` on every site whose owner published a\n * home page of their own, which clears the marker (AGL-3478), and on every\n * site created before the marker existed, whose home page is the owner's.\n */\nexport function aiStarterHomeCandidate(\n host: Readonly<Record<string, unknown>> | null,\n screens: ReadonlyArray<Pick<AiInventoryScreen, 'id' | 'template'>>,\n): string | null {\n const id = text(host?.['defaultHomeScreenId'])\n if (!id) return null\n const routing = host?.['screens']\n const path =\n routing && typeof routing === 'object' ? (routing as Record<string, unknown>)[id] : undefined\n if (path !== SCREEN_ROOT_PATH) return null\n return screens.some((screen) => screen.id === id && !screen.template) ? id : null\n}\n\n/**\n * Whether the starter home is as the platform wrote it: its one version is\n * the one it was provisioned and published with. A version saved in the\n * editor is an edit, published or not, and an edited starter is the owner's\n * page — a plan keeps it.\n */\nexport function aiStarterHomeUntouched(\n publishedVersionId: string,\n versionIds: readonly string[],\n): boolean {\n return Boolean(publishedVersionId) && versionIds.length === 1 && versionIds[0] === publishedVersionId\n}\n\n/** A record template's base, as the data plugin keeps it: one to four URL-safe segments. */\nconst RECORD_BASE = /^[a-z0-9]+(?:-[a-z0-9]+)*(?:\\/[a-z0-9]+(?:-[a-z0-9]+)*){0,3}$/\n\n/**\n * The record page a site already serves for a dataset (AGL-3747), from the\n * `recordPage` fact the data plugin's index answers for a site: its base and\n * the address field its pages are reached by. `null` where there is none, or\n * where its address field is no longer one of the dataset's fields.\n */\nexport function aiInventoryRecordPage(\n fact: unknown,\n fields: ReadonlyArray<{ id: string }>,\n): { recordBase: string; addressField: string } | null {\n if (!fact || typeof fact !== 'object') return null\n const base = text((fact as Data)['base']).replace(/^\\/+|\\/+$/g, '')\n const slugField = text((fact as Data)['slugField'])\n if (!RECORD_BASE.test(base) || !slugField || !fields.some((field) => field.id === slugField)) return null\n return { recordBase: base, addressField: slugField }\n}\n\n/**\n * The org's datasets shared with the site, through the `dataset` index the\n * data plugin publishes (AGL-3080) rather than its collection: their names\n * and their fields' names, in the dataset page's order. None where no plugin\n * keeps datasets here.\n */\nasync function readIndexedDatasets(\n orgId: string,\n hostId: string,\n cap: number,\n): Promise<Window<AiInventoryDataset>> {\n const datasets = pluginRecordIndex('dataset')\n if (!datasets) return { rows: [], truncated: false }\n const { records, truncated } = await datasets.index.list({ orgId, hostId, limit: cap })\n return {\n rows: records.map((record) => {\n const fields = (Array.isArray(record.facts['fields']) ? (record.facts['fields'] as Data[]) : [])\n .map((field) => ({\n id: text(field?.['id']),\n name: text(field?.['name']) || text(field?.['id']),\n image: text(field?.['customType']) === 'image',\n // A record page's address, the data plugin's `pageAddress` type (AGL-3727).\n address: text(field?.['customType']) === 'pageAddress',\n }))\n .filter((field) => field.name)\n // A record's photo (AGL-3616), the data plugin's `image` field type.\n const image = fields.find((field) => field.image && field.id)\n // The record page the site already serves for it (AGL-3747), whose\n // address field is the one its pages are reached by.\n const page = aiInventoryRecordPage(record.facts['recordPage'], fields)\n const address = page?.addressField ?? fields.find((field) => field.address && field.id)?.id\n // Made for this site, where its scope names this site alone (AGL-3912).\n const visibleTo = Array.isArray(record.facts['visibleTo']) ? (record.facts['visibleTo'] as unknown[]) : []\n const siteOnly = visibleTo.length > 0 && visibleTo.every((token) => token === hostScopeToken(hostId))\n return {\n id: record.id,\n name: record.name,\n fields: fields.map((field) => field.name),\n fieldIds: fields.map((field) => field.id || field.name),\n ...(image ? { imageField: image.id } : {}),\n ...(address ? { addressField: address } : {}),\n ...(page ? { recordBase: page.recordBase } : {}),\n ...(siteOnly ? { siteOnly: true as const } : {}),\n }\n }),\n truncated,\n }\n}\n\n/**\n * Read one site's inventory for a generation.\n *\n * SCOPED before anything is read. The job names a site and an org, and\n * nothing the reader is handed proves the site belongs to the org, so it asks\n * the host index first and refuses a pairing it does not record: another\n * workspace's component names are not this workspace's prompt. Datasets are\n * org-owned and narrowed to what this site may see.\n *\n * Throws `AiInventoryScopeError` for a site outside the org, and lets a\n * Firestore failure propagate: a planner handed an empty inventory by\n * mistake would create everything the site already has.\n */\nexport async function readSiteInventory(\n orgId: string,\n hostId: string,\n options: ReadSiteInventoryOptions = {},\n): Promise<AiSiteInventory> {\n const owner = await resolveOrgIdForHost(hostId)\n if (!owner || owner !== orgId) throw new AiInventoryScopeError(hostId)\n const firestore = options.firestore ?? firebaseAdmin.app().firestore()\n const cap = Math.min(\n AI_SITE_INVENTORY_MAX_PER_KIND,\n Math.max(1, Math.floor(options.maxPerKind ?? AI_SITE_INVENTORY_MAX_PER_KIND)),\n )\n const host = firestore.collection('hosts').doc(hostId)\n // Each listed screen's published version, read in the same window, for the\n // starter home's untouched test below.\n const screenVersions = new Map<string, string>()\n\n const [components, layouts, templates, forms, datasets, collections, screens, hostDoc] =\n await Promise.all([\n readWindow<AiInventoryComponent>(\n host.collection('components'),\n ['displayName', 'props', 'rootId', 'versionId', 'deletedAt', 'kind'],\n cap,\n (id, data) => {\n // Soft-deleted, or never published: an instance of either renders nothing.\n if (data['deletedAt'] || !(data['rootId'] || data['versionId'])) return null\n // An email block (AGL-3287) is built from email elements and placed\n // in emails alone: no page, layout or component a job builds can\n // render one, and the email a job builds places no instances.\n if (reusableComponentKindOf(data) === REUSABLE_COMPONENT_KIND_EMAIL) return null\n const props: AiComponentPropTypes = {}\n const bracketedDefaults: Record<string, string[]> = {}\n for (const prop of Array.isArray(data['props']) ? (data['props'] as Data[]) : []) {\n const name = text(prop?.['name'])\n if (!name) continue\n props[name] = text(prop['type']) || 'text'\n const facts = aiBracketedFacts([text(prop['defaultValue'])])\n if (facts.length) bracketedDefaults[name] = facts\n }\n return {\n id,\n name: nameOf(data, id, 'displayName'),\n props,\n ...(Object.keys(bracketedDefaults).length ? { bracketedDefaults } : {}),\n }\n },\n ),\n readWindow<AiInventoryLayout>(\n host.collection('layouts'),\n ['displayName', 'layoutId', 'deletedAt'],\n cap,\n (id, data) =>\n data['deletedAt']\n ? null\n : {\n id,\n name: nameOf(data, id, 'displayName'),\n parentId: text(data['layoutId']) || null,\n },\n ),\n readWindow<AiInventoryTemplate>(\n host.collection('templates'),\n ['displayName', 'kind', 'deletedAt'],\n cap,\n (id, data) =>\n data['deletedAt']\n ? null\n : { id, name: nameOf(data, id, 'displayName'), kind: text(data['kind']) || 'page' },\n ),\n readWindow<AiInventoryForm>(\n host.collection('forms'),\n ['displayName', 'slug', 'fields', 'archivedAt'],\n cap,\n (id, data) =>\n isFormArchived(data as { archivedAt?: unknown })\n ? null\n : {\n id,\n name: nameOf(data, id, 'displayName', 'slug'),\n fields: (Array.isArray(data['fields']) ? (data['fields'] as Data[]) : [])\n .map((field) => text(field?.['fieldName']))\n .filter(Boolean),\n },\n ),\n readIndexedDatasets(orgId, hostId, cap),\n readWindow<AiInventoryCollection>(\n host.collection('collections'),\n ['displayName', 'name', 'slug', 'kind'],\n cap,\n (id, data) =>\n hostCollectionKind(data) !== 'content'\n ? null\n : {\n id,\n name: nameOf(data, id, 'displayName', 'name', 'slug'),\n slug: text(data['slug']),\n },\n ),\n readWindow<AiInventoryScreen>(\n host.collection('screens'),\n ['displayName', 'slug', 'layoutId', 'kind', 'deletedAt', 'versionId'],\n cap,\n (id, data) => {\n const kind = text(data['kind'])\n // An email design and an error body are not pages a plan links or duplicates.\n if (data['deletedAt'] || (kind && kind !== SCREEN_KIND_TEMPLATE)) return null\n screenVersions.set(id, text(data['versionId']))\n return {\n id,\n name: nameOf(data, id, 'displayName', 'slug'),\n slug: text(data['slug']),\n layoutId: text(data['layoutId']) || null,\n template: kind === SCREEN_KIND_TEMPLATE,\n }\n },\n ),\n host.get(),\n ])\n\n const hostData = hostDoc.exists ? ((hostDoc.data() ?? {}) as Data) : null\n const starter = aiStarterHomeCandidate(hostData, screens.rows)\n if (starter) {\n // One more read, and only on a site that still carries the marker: the\n // starter's versions, of which an untouched one has exactly the one it\n // was provisioned with.\n const versions = await host\n .collection('screens')\n .doc(starter)\n .collection('versions')\n .select()\n .limit(2)\n .get()\n if (aiStarterHomeUntouched(screenVersions.get(starter) ?? '', versions.docs.map((doc) => doc.id))) {\n const row = screens.rows.find((screen) => screen.id === starter)\n if (row) row.replaceable = true\n }\n }\n\n const windows: Record<AiInventoryKind, Window<unknown>> = {\n components,\n layouts,\n templates,\n forms,\n datasets,\n collections,\n screens,\n }\n return {\n hostId,\n components: components.rows,\n layouts: layouts.rows,\n templates: templates.rows,\n forms: forms.rows,\n datasets: datasets.rows,\n collections: collections.rows,\n screens: screens.rows,\n theme: aiInventoryTheme(hostData),\n truncated: AI_INVENTORY_KINDS.filter((kind) => windows[kind].truncated),\n }\n}\n"],"names":["hostCollectionKind","isFormArchived","describeTheme","resolveSiteTheme","REUSABLE_COMPONENT_KIND_EMAIL","reusableComponentKindOf","SCREEN_KIND_TEMPLATE","SCREEN_ROOT_PATH","pluginRecordIndex","hostScopeToken","firebaseAdmin","resolveOrgIdForHost","AI_INVENTORY_KINDS","AI_SITE_INVENTORY_MAX_PER_KIND","AiInventoryScopeError","aiBracketedFacts","text","value","trim","nameOf","data","id","fields","field","schemeColors","scheme","colors","visit","path","Array","isArray","key","inner","Object","entries","aiInventoryTheme","host","theme","fonts","Set","font","family","add","summary","colorSchemes","light","readWindow","query","cap","map","snapshot","select","limit","get","rows","doc","docs","slice","row","push","truncated","length","aiStarterHomeCandidate","screens","routing","undefined","some","screen","template","aiStarterHomeUntouched","publishedVersionId","versionIds","Boolean","RECORD_BASE","aiInventoryRecordPage","fact","base","replace","slugField","test","recordBase","addressField","readIndexedDatasets","orgId","hostId","datasets","records","index","list","record","facts","name","image","address","filter","find","page","visibleTo","siteOnly","every","token","fieldIds","imageField","readSiteInventory","options","hostDoc","owner","firestore","app","Math","min","max","floor","maxPerKind","collection","screenVersions","Map","components","layouts","templates","forms","collections","Promise","all","props","bracketedDefaults","prop","keys","parentId","kind","slug","set","layoutId","hostData","exists","starter","versions","replaceable","windows"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/runtime/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\nimport { hostCollectionKind } from '@aglyn/aglyn/app-utils/collection-kind'\nimport { isFormArchived } from '@aglyn/aglyn/app-utils/forms'\nimport {\n describeTheme,\n resolveSiteTheme,\n type ThemeHostDocument,\n} from '@aglyn/aglyn/app-utils/site-theme'\nimport {\n REUSABLE_COMPONENT_KIND_EMAIL,\n reusableComponentKindOf,\n} from '@aglyn/aglyn/app-utils/reusable-component-kind'\nimport { SCREEN_KIND_TEMPLATE, SCREEN_ROOT_PATH } from '@aglyn/aglyn/app-utils/screen-route'\nimport { pluginRecordIndex } from '@aglyn/aglyn/plugin-manager/plugin-record-index'\nimport { hostScopeToken } from '@aglyn/aglyn/app-utils/scope-tokens'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin/server/firebase-admin'\nimport { resolveOrgIdForHost } from '@aglyn/tenant-data-admin/server/organizations'\nimport {\n AI_INVENTORY_KINDS,\n AI_SITE_INVENTORY_MAX_PER_KIND,\n AiInventoryScopeError,\n type AiComponentPropTypes,\n type AiInventoryCollection,\n type AiInventoryComponent,\n type AiInventoryDataset,\n type AiInventoryForm,\n type AiInventoryKind,\n type AiInventoryLayout,\n type AiInventoryScreen,\n type AiInventoryTemplate,\n type AiInventoryTheme,\n type AiSiteInventory,\n} from '../model/ai-site-inventory'\nimport { aiBracketedFacts } from './ai-doctrine-validators'\n\n/**\n * The reader behind the site inventory (AGL-2935): one site's components,\n * layouts, templates, forms, datasets, content collections, screens and\n * theme, read as projections through the Admin SDK and reduced to the\n * compact shape `model/ai-site-inventory.ts` describes.\n *\n * Every kind is a projection (`select`) of the fields the inventory names —\n * never a node map, never a record's content — read one row past the cap so\n * a full page is told apart from a cut one. Retired rows (soft-deleted,\n * archived, a component never published) are filtered after the read, so a\n * site with many retired rows can list fewer than the cap; the kind is still\n * marked truncated when the read was cut, which is the honest answer.\n *\n * The window is WIDER than the prompt lists (AGL-2937). The prompt block\n * lists `AI_SITE_INVENTORY_LISTED_PER_KIND` records of a kind because every\n * character of it is billed on every attempt; the rest of the window is what\n * the lookup tool searches, and a record never read cannot be found. The\n * difference is paid in document reads rather than tokens — a projection of\n * a few fields, once per job step — which is the cheaper of the two by some\n * orders of magnitude.\n */\n\ntype Firestore = FirebaseFirestore.Firestore\ntype Data = Record<string, unknown>\n\nexport interface ReadSiteInventoryOptions {\n /** The Admin SDK handle; the platform's default app when absent. */\n firestore?: Firestore\n /** Rows read per kind; `AI_SITE_INVENTORY_MAX_PER_KIND` when absent, and never above it. */\n maxPerKind?: number\n}\n\nfunction text(value: unknown): string {\n return typeof value === 'string' ? value.trim() : ''\n}\n\n/** A record's name, or its id when it was saved without one. */\nfunction nameOf(data: Data, id: string, ...fields: string[]): string {\n for (const field of fields) {\n const value = text(data[field])\n if (value) return value\n }\n return id\n}\n\n/** The flat `path → color` leaves of one scheme, e.g. `primary.main`. */\nfunction schemeColors(scheme: unknown): Record<string, string> {\n const colors: Record<string, string> = {}\n const visit = (value: unknown, path: string) => {\n if (typeof value === 'string') {\n if (path) colors[path] = value\n return\n }\n if (!value || typeof value !== 'object' || Array.isArray(value)) return\n for (const [key, inner] of Object.entries(value as Data)) {\n visit(inner, path ? `${path}.${key}` : key)\n }\n }\n visit(scheme, '')\n return colors\n}\n\n/** The theme a site renders with, reduced to what a plan and an email can act on. */\nexport function aiInventoryTheme(host: Data | null): AiInventoryTheme | null {\n const theme = resolveSiteTheme(host as ThemeHostDocument | null)\n if (!theme) return null\n const fonts = new Set<string>()\n for (const font of theme.fonts ?? []) {\n if (text(font?.family)) fonts.add(text(font.family))\n }\n return {\n summary: describeTheme(theme),\n colors: schemeColors(theme.colorSchemes?.light),\n fonts: [...fonts],\n }\n}\n\ninterface Window<T> {\n rows: T[]\n truncated: boolean\n}\n\nasync function readWindow<T>(\n query: FirebaseFirestore.Query,\n fields: string[],\n cap: number,\n map: (id: string, data: Data) => T | null,\n): Promise<Window<T>> {\n const snapshot = await query\n .select(...fields)\n .limit(cap + 1)\n .get()\n const rows: T[] = []\n for (const doc of snapshot.docs.slice(0, cap)) {\n const row = map(doc.id, (doc.data() ?? {}) as Data)\n if (row) rows.push(row)\n }\n return { rows, truncated: snapshot.docs.length > cap }\n}\n\n/**\n * The screen a site's starter home may be (AGL-3594, AGL-3408): the one\n * `host.defaultHomeScreenId` names, while it still answers `/` in the routing\n * map and is listed as a page. `null` on every site whose owner published a\n * home page of their own, which clears the marker (AGL-3478), and on every\n * site created before the marker existed, whose home page is the owner's.\n */\nexport function aiStarterHomeCandidate(\n host: Readonly<Record<string, unknown>> | null,\n screens: ReadonlyArray<Pick<AiInventoryScreen, 'id' | 'template'>>,\n): string | null {\n const id = text(host?.['defaultHomeScreenId'])\n if (!id) return null\n const routing = host?.['screens']\n const path =\n routing && typeof routing === 'object' ? (routing as Record<string, unknown>)[id] : undefined\n if (path !== SCREEN_ROOT_PATH) return null\n return screens.some((screen) => screen.id === id && !screen.template) ? id : null\n}\n\n/**\n * Whether the starter home is as the platform wrote it: its one version is\n * the one it was provisioned and published with. A version saved in the\n * editor is an edit, published or not, and an edited starter is the owner's\n * page — a plan keeps it.\n */\nexport function aiStarterHomeUntouched(\n publishedVersionId: string,\n versionIds: readonly string[],\n): boolean {\n return Boolean(publishedVersionId) && versionIds.length === 1 && versionIds[0] === publishedVersionId\n}\n\n/** A record template's base, as the data plugin keeps it: one to four URL-safe segments. */\nconst RECORD_BASE = /^[a-z0-9]+(?:-[a-z0-9]+)*(?:\\/[a-z0-9]+(?:-[a-z0-9]+)*){0,3}$/\n\n/**\n * The record page a site already serves for a dataset (AGL-3747), from the\n * `recordPage` fact the data plugin's index answers for a site: its base and\n * the address field its pages are reached by. `null` where there is none, or\n * where its address field is no longer one of the dataset's fields.\n */\nexport function aiInventoryRecordPage(\n fact: unknown,\n fields: ReadonlyArray<{ id: string }>,\n): { recordBase: string; addressField: string } | null {\n if (!fact || typeof fact !== 'object') return null\n const base = text((fact as Data)['base']).replace(/^\\/+|\\/+$/g, '')\n const slugField = text((fact as Data)['slugField'])\n if (!RECORD_BASE.test(base) || !slugField || !fields.some((field) => field.id === slugField)) return null\n return { recordBase: base, addressField: slugField }\n}\n\n/**\n * The org's datasets shared with the site, through the `dataset` index the\n * data plugin publishes (AGL-3080) rather than its collection: their names\n * and their fields' names, in the dataset page's order. None where no plugin\n * keeps datasets here.\n */\nasync function readIndexedDatasets(\n orgId: string,\n hostId: string,\n cap: number,\n): Promise<Window<AiInventoryDataset>> {\n const datasets = pluginRecordIndex('dataset')\n if (!datasets) return { rows: [], truncated: false }\n const { records, truncated } = await datasets.index.list({ orgId, hostId, limit: cap })\n return {\n rows: records.map((record) => {\n const fields = (Array.isArray(record.facts['fields']) ? (record.facts['fields'] as Data[]) : [])\n .map((field) => ({\n id: text(field?.['id']),\n name: text(field?.['name']) || text(field?.['id']),\n image: text(field?.['customType']) === 'image',\n // A record page's address, the data plugin's `pageAddress` type (AGL-3727).\n address: text(field?.['customType']) === 'pageAddress',\n }))\n .filter((field) => field.name)\n // A record's photo (AGL-3616), the data plugin's `image` field type.\n const image = fields.find((field) => field.image && field.id)\n // The record page the site already serves for it (AGL-3747), whose\n // address field is the one its pages are reached by.\n const page = aiInventoryRecordPage(record.facts['recordPage'], fields)\n const recordScreenId = page ? text((record.facts['recordPage'] as Data)['screenId']) : ''\n const address = page?.addressField ?? fields.find((field) => field.address && field.id)?.id\n // Made for this site, where its scope names this site alone (AGL-3912).\n const visibleTo = Array.isArray(record.facts['visibleTo']) ? (record.facts['visibleTo'] as unknown[]) : []\n const siteOnly = visibleTo.length > 0 && visibleTo.every((token) => token === hostScopeToken(hostId))\n return {\n id: record.id,\n name: record.name,\n fields: fields.map((field) => field.name),\n fieldIds: fields.map((field) => field.id || field.name),\n ...(image ? { imageField: image.id } : {}),\n ...(address ? { addressField: address } : {}),\n ...(page ? { recordBase: page.recordBase } : {}),\n ...(recordScreenId ? { recordScreenId } : {}),\n ...(siteOnly ? { siteOnly: true as const } : {}),\n }\n }),\n truncated,\n }\n}\n\n/**\n * The datasets shared with one site (AGL-3616), alone: what an edit to one of\n * its pages may repeat an element over. The caller has already held the site\n * to the org; a failed read answers none, and the edit admits only the\n * repeats already on its canvas.\n */\nexport async function readAiSiteDatasets(orgId: string, hostId: string): Promise<AiInventoryDataset[]> {\n return readIndexedDatasets(orgId, hostId, AI_SITE_INVENTORY_MAX_PER_KIND)\n .then((window) => window.rows)\n .catch((error: unknown) => {\n console.error('ai site datasets read failed', { orgId, hostId, error })\n return []\n })\n}\n\n/**\n * Read one site's inventory for a generation.\n *\n * SCOPED before anything is read. The job names a site and an org, and\n * nothing the reader is handed proves the site belongs to the org, so it asks\n * the host index first and refuses a pairing it does not record: another\n * workspace's component names are not this workspace's prompt. Datasets are\n * org-owned and narrowed to what this site may see.\n *\n * Throws `AiInventoryScopeError` for a site outside the org, and lets a\n * Firestore failure propagate: a planner handed an empty inventory by\n * mistake would create everything the site already has.\n */\nexport async function readSiteInventory(\n orgId: string,\n hostId: string,\n options: ReadSiteInventoryOptions = {},\n): Promise<AiSiteInventory> {\n const owner = await resolveOrgIdForHost(hostId)\n if (!owner || owner !== orgId) throw new AiInventoryScopeError(hostId)\n const firestore = options.firestore ?? firebaseAdmin.app().firestore()\n const cap = Math.min(\n AI_SITE_INVENTORY_MAX_PER_KIND,\n Math.max(1, Math.floor(options.maxPerKind ?? AI_SITE_INVENTORY_MAX_PER_KIND)),\n )\n const host = firestore.collection('hosts').doc(hostId)\n // Each listed screen's published version, read in the same window, for the\n // starter home's untouched test below.\n const screenVersions = new Map<string, string>()\n\n const [components, layouts, templates, forms, datasets, collections, screens, hostDoc] =\n await Promise.all([\n readWindow<AiInventoryComponent>(\n host.collection('components'),\n ['displayName', 'props', 'rootId', 'versionId', 'deletedAt', 'kind'],\n cap,\n (id, data) => {\n // Soft-deleted, or never published: an instance of either renders nothing.\n if (data['deletedAt'] || !(data['rootId'] || data['versionId'])) return null\n // An email block (AGL-3287) is built from email elements and placed\n // in emails alone: no page, layout or component a job builds can\n // render one, and the email a job builds places no instances.\n if (reusableComponentKindOf(data) === REUSABLE_COMPONENT_KIND_EMAIL) return null\n const props: AiComponentPropTypes = {}\n const bracketedDefaults: Record<string, string[]> = {}\n for (const prop of Array.isArray(data['props']) ? (data['props'] as Data[]) : []) {\n const name = text(prop?.['name'])\n if (!name) continue\n props[name] = text(prop['type']) || 'text'\n const facts = aiBracketedFacts([text(prop['defaultValue'])])\n if (facts.length) bracketedDefaults[name] = facts\n }\n return {\n id,\n name: nameOf(data, id, 'displayName'),\n props,\n ...(Object.keys(bracketedDefaults).length ? { bracketedDefaults } : {}),\n }\n },\n ),\n readWindow<AiInventoryLayout>(\n host.collection('layouts'),\n ['displayName', 'layoutId', 'deletedAt'],\n cap,\n (id, data) =>\n data['deletedAt']\n ? null\n : {\n id,\n name: nameOf(data, id, 'displayName'),\n parentId: text(data['layoutId']) || null,\n },\n ),\n readWindow<AiInventoryTemplate>(\n host.collection('templates'),\n ['displayName', 'kind', 'deletedAt'],\n cap,\n (id, data) =>\n data['deletedAt']\n ? null\n : { id, name: nameOf(data, id, 'displayName'), kind: text(data['kind']) || 'page' },\n ),\n readWindow<AiInventoryForm>(\n host.collection('forms'),\n ['displayName', 'slug', 'fields', 'archivedAt'],\n cap,\n (id, data) =>\n isFormArchived(data as { archivedAt?: unknown })\n ? null\n : {\n id,\n name: nameOf(data, id, 'displayName', 'slug'),\n fields: (Array.isArray(data['fields']) ? (data['fields'] as Data[]) : [])\n .map((field) => text(field?.['fieldName']))\n .filter(Boolean),\n },\n ),\n readIndexedDatasets(orgId, hostId, cap),\n readWindow<AiInventoryCollection>(\n host.collection('collections'),\n ['displayName', 'name', 'slug', 'kind'],\n cap,\n (id, data) =>\n hostCollectionKind(data) !== 'content'\n ? null\n : {\n id,\n name: nameOf(data, id, 'displayName', 'name', 'slug'),\n slug: text(data['slug']),\n },\n ),\n readWindow<AiInventoryScreen>(\n host.collection('screens'),\n ['displayName', 'slug', 'layoutId', 'kind', 'deletedAt', 'versionId'],\n cap,\n (id, data) => {\n const kind = text(data['kind'])\n // An email design and an error body are not pages a plan links or duplicates.\n if (data['deletedAt'] || (kind && kind !== SCREEN_KIND_TEMPLATE)) return null\n screenVersions.set(id, text(data['versionId']))\n return {\n id,\n name: nameOf(data, id, 'displayName', 'slug'),\n slug: text(data['slug']),\n layoutId: text(data['layoutId']) || null,\n template: kind === SCREEN_KIND_TEMPLATE,\n }\n },\n ),\n host.get(),\n ])\n\n const hostData = hostDoc.exists ? ((hostDoc.data() ?? {}) as Data) : null\n const starter = aiStarterHomeCandidate(hostData, screens.rows)\n if (starter) {\n // One more read, and only on a site that still carries the marker: the\n // starter's versions, of which an untouched one has exactly the one it\n // was provisioned with.\n const versions = await host\n .collection('screens')\n .doc(starter)\n .collection('versions')\n .select()\n .limit(2)\n .get()\n if (aiStarterHomeUntouched(screenVersions.get(starter) ?? '', versions.docs.map((doc) => doc.id))) {\n const row = screens.rows.find((screen) => screen.id === starter)\n if (row) row.replaceable = true\n }\n }\n\n const windows: Record<AiInventoryKind, Window<unknown>> = {\n components,\n layouts,\n templates,\n forms,\n datasets,\n collections,\n screens,\n }\n return {\n hostId,\n components: components.rows,\n layouts: layouts.rows,\n templates: templates.rows,\n forms: forms.rows,\n datasets: datasets.rows,\n collections: collections.rows,\n screens: screens.rows,\n theme: aiInventoryTheme(hostData),\n truncated: AI_INVENTORY_KINDS.filter((kind) => windows[kind].truncated),\n }\n}\n"],"names":["hostCollectionKind","isFormArchived","describeTheme","resolveSiteTheme","REUSABLE_COMPONENT_KIND_EMAIL","reusableComponentKindOf","SCREEN_KIND_TEMPLATE","SCREEN_ROOT_PATH","pluginRecordIndex","hostScopeToken","firebaseAdmin","resolveOrgIdForHost","AI_INVENTORY_KINDS","AI_SITE_INVENTORY_MAX_PER_KIND","AiInventoryScopeError","aiBracketedFacts","text","value","trim","nameOf","data","id","fields","field","schemeColors","scheme","colors","visit","path","Array","isArray","key","inner","Object","entries","aiInventoryTheme","host","theme","fonts","Set","font","family","add","summary","colorSchemes","light","readWindow","query","cap","map","snapshot","select","limit","get","rows","doc","docs","slice","row","push","truncated","length","aiStarterHomeCandidate","screens","routing","undefined","some","screen","template","aiStarterHomeUntouched","publishedVersionId","versionIds","Boolean","RECORD_BASE","aiInventoryRecordPage","fact","base","replace","slugField","test","recordBase","addressField","readIndexedDatasets","orgId","hostId","datasets","records","index","list","record","facts","name","image","address","filter","find","page","recordScreenId","visibleTo","siteOnly","every","token","fieldIds","imageField","readAiSiteDatasets","then","window","catch","error","console","readSiteInventory","options","hostDoc","owner","firestore","app","Math","min","max","floor","maxPerKind","collection","screenVersions","Map","components","layouts","templates","forms","collections","Promise","all","props","bracketedDefaults","prop","keys","parentId","kind","slug","set","layoutId","hostData","exists","starter","versions","replaceable","windows"],"mappings":"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"}
|
|
@@ -62,6 +62,8 @@ import { aiJobStepRunnerFor } from "../jobs/ai-jobs.js";
|
|
|
62
62
|
'Building on this site:',
|
|
63
63
|
`- When the user asks you to make or set up things on their site — pages, a header and footer, forms, or anything listed in the "A build here can make" block — write one or two sentences saying what you will plan, then call the ${ASSIST_BUILD_TOOL_NAME} tool once with the whole request. Write no JSON in your message.`,
|
|
64
64
|
'- brief: everything they asked for, in their words, with each page, form and other part named and what each is for. Include what the conversation already settled. Do not invent parts they did not ask for beyond what their request plainly needs (a page that shows a form needs that form).',
|
|
65
|
+
// AGL-4116: each follow-up to a plan made another plan beside it.
|
|
66
|
+
'- When they change a plan you proposed earlier in this conversation and have not confirmed, call the tool again with the whole request as it now stands: what still holds from before, with their change. The new plan replaces the waiting one; say you have updated the plan.',
|
|
65
67
|
'- publish: true only when they asked for the new pages to go live or be published; otherwise false. New work is an unpublished draft by default.',
|
|
66
68
|
'- summary: one short sentence, such as "Plan three pages, a contact form and a booking service".',
|
|
67
69
|
// AGL-4092: a brief the chat wrote asked the build to "use bracketed placeholders where details are missing".
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/server/assist-build.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { AglynOrgBilling } from '@aglyn/aglyn/foundation/definitions/org-billing.types'\nimport {\n ASSIST_BUILD_ACTION_ID,\n ASSIST_BUILD_SUMMARY_MAX_CHARS,\n ASSIST_BUILD_TOOL_NAME,\n type AssistBuildProposal,\n} from '../model/assist-build'\nimport type { AiTool } from '../providers/contract'\nimport { aiBuildIntents, aiBuildOps } from '../jobs/ai-build-capabilities'\nimport { aiBuildFreeTaste } from '../jobs/ai-job-build-step'\nimport { AI_JOB_BRIEF_MAX_CHARS } from '../jobs/ai-job-text-step'\nimport { aiJobStepRunnerFor } from '../jobs/ai-jobs'\n\n/**\n * The chat's build rung (AGL-3616): on a site, a request that names things\n * to make — \"a few new pages, a booking form and a contact form, then an\n * about page\" — is proposed as ONE build through a strict tool. The tool\n * carries the request, not a plan: planning is the build job's own plan\n * step, against the site's real inventory, with its re-ask and its limits.\n * The panel starts that job and shows its plan card with one price; nothing\n * is built until the person confirms it, and what is built is a draft unless\n * they asked to publish and tick the card's box.\n */\n\nconst STRING = { type: 'string' } as const\n\n/** The strict tool the model proposes a build through. */\nexport function assistBuildTool(): AiTool {\n return {\n name: ASSIST_BUILD_TOOL_NAME,\n description:\n 'Propose building what the user asked for on this site: pages, a header ' +\n 'and footer, forms and the other things listed. The user sees a plan with ' +\n 'its price and confirms it before anything is made. Call at most once per ' +\n 'message, with the whole request.',\n inputSchema: {\n type: 'object',\n properties: {\n summary: STRING,\n brief: STRING,\n publish: { type: 'boolean' },\n },\n required: ['summary', 'brief', 'publish'],\n additionalProperties: false,\n },\n strict: true,\n }\n}\n\n/**\n * The build protocol — the same for every site, so it sits inside the cached\n * prefix. What a build can make on THIS site is per site and rides\n * `assistBuildIntentsBlock`, after every breakpoint.\n */\nexport function assistBuildBlock(): string {\n return [\n 'Building on this site:',\n `- When the user asks you to make or set up things on their site — pages, a header and footer, forms, or anything listed in the \"A build here can make\" block — write one or two sentences saying what you will plan, then call the ${ASSIST_BUILD_TOOL_NAME} tool once with the whole request. Write no JSON in your message.`,\n '- brief: everything they asked for, in their words, with each page, form and other part named and what each is for. Include what the conversation already settled. Do not invent parts they did not ask for beyond what their request plainly needs (a page that shows a form needs that form).',\n '- publish: true only when they asked for the new pages to go live or be published; otherwise false. New work is an unpublished draft by default.',\n '- summary: one short sentence, such as \"Plan three pages, a contact form and a booking service\".',\n // AGL-4092: a brief the chat wrote asked the build to \"use bracketed placeholders where details are missing\".\n '- Never ask the build for placeholders, brackets or \"to be added\" text. Where the user gave no fact, say nothing of it: the build reads the site’s business profile and contact details itself.',\n '- Nothing is built when you call the tool: the user is shown a plan with its price and confirms it. Never say anything is made, and never quote a price.',\n '- Only offer what the \"A build here can make\" block lists. If they ask for something it does not list, say plainly it cannot be built here from the chat and build the rest.',\n '- A change to the site’s theme or look (its colors, fonts or corners) is offered only where the \"A build here can make\" block lists it, which it does on paid plans. When it is asked for and not listed, say a build cannot change the theme on this workspace’s plan and that they can change it in Setup → Theme, where the Theme assistant can propose one; then build the rest.',\n '- A question, or a change to the canvas the user has open, is not a build: answer it, or use the canvas edit tool where it is offered.',\n ].join('\\n')\n}\n\n/** What a build can make on this site, in each owner's own words. Per site: volatile. */\nexport function assistBuildIntentsBlock(intents: readonly string[]): string {\n return ['A build here can make:', ...intents.map((intent) => `- ${intent}`)].join('\\n')\n}\n\n/**\n * Where a build's facts come from, said once at the end of every brief the\n * chat proposes (AGL-4092): never a placeholder for one it lacks.\n */\nexport const ASSIST_BUILD_FACTS_LINE =\n 'Facts: use the site’s business profile and contact details on file; where a fact is missing, leave it out — no placeholders, no brackets.'\n\n/**\n * A clause of a brief that asks for a gap instead of a fact: \"use bracketed\n * placeholders where details are missing\", \"mark it in square brackets\",\n * \"show TBD\". A shop's \"shelf brackets\" asks for none.\n */\nconst ASKS_FOR_GAPS = /\\b(?:placeholders?|bracketed|in (?:square )?brackets|to be (?:added|filled|confirmed)|tbd)\\b/i\n\n/**\n * The brief with every clause that asks for placeholders, brackets or \"to be\n * added\" text taken out (AGL-4092): \"Do not invent names; use bracketed\n * placeholders where details are missing.\" keeps \"Do not invent names.\" A\n * sentence nothing is left of goes. Pure.\n */\nexport function assistBuildBriefWithoutGaps(brief: string): string {\n return brief\n .split('\\n')\n .map((line) =>\n line\n .split(/(?<=[.!?])\\s+/)\n .map((sentence) => {\n if (!ASKS_FOR_GAPS.test(sentence)) return sentence\n const kept = sentence\n .replace(/[.!?]$/, '')\n .split(/\\s*;\\s*/)\n .filter((clause) => !ASKS_FOR_GAPS.test(clause))\n .join('; ')\n .trim()\n return kept ? `${kept}.` : ''\n })\n .filter(Boolean)\n .join(' '),\n )\n .filter((line) => line.trim())\n .join('\\n')\n .trim()\n}\n\n/**\n * The tool call held to what a build job accepts, or `null` when it is\n * unusable. The site is the request's own, never the model's; the brief is\n * clamped to the job door's limit, with a line naming the console page it\n * was asked from so the plan step can read \"this page\" against it. Before\n * it, the brief asks for no placeholder and says where facts come from\n * (AGL-4092).\n */\nexport function resolveAssistBuild(\n input: Record<string, unknown> | null,\n scope: { hostId: string; screen: string | null },\n): AssistBuildProposal | null {\n if (!input) return null\n const asked = typeof input['brief'] === 'string' ? assistBuildBriefWithoutGaps(input['brief'].trim()) : ''\n if (!asked) return null\n const where = scope.screen ? `\\nAsked from the console page: ${scope.screen}` : ''\n const facts = `\\n${ASSIST_BUILD_FACTS_LINE}`\n const brief = `${asked.slice(0, AI_JOB_BRIEF_MAX_CHARS - facts.length - where.length).trim()}${facts}${where}`.trim()\n const summary =\n typeof input['summary'] === 'string' && input['summary'].trim()\n ? input['summary'].trim().slice(0, ASSIST_BUILD_SUMMARY_MAX_CHARS)\n : 'Plan what you asked for'\n return {\n id: ASSIST_BUILD_ACTION_ID,\n hostId: scope.hostId,\n brief,\n publish: input['publish'] === true,\n summary,\n }\n}\n\nexport interface AssistBuildIntentsDeps {\n ops?: typeof aiBuildOps\n hasBuildRunner?: () => boolean\n}\n\n/**\n * What a build may make on this site, or `null` where none can be built: a\n * deployment that has not loaded the build kind, or a site where no\n * operation passes its owner's gates. The same operations the build's plan\n * step offers, so the chat never offers what the plan cannot make.\n */\nexport async function assistBuildIntents(\n firestore: FirebaseFirestore.Firestore,\n input: { orgId: string; hostId: string; org: Record<string, unknown> },\n deps: AssistBuildIntentsDeps = {},\n): Promise<string[] | null> {\n const hasBuildRunner = deps.hasBuildRunner ?? (() => aiJobStepRunnerFor('build') !== null)\n if (!hasBuildRunner()) return null\n const host = ((await firestore.collection('hosts').doc(input.hostId).get()).data() ?? null) as Record<\n string,\n unknown\n > | null\n if (!host) return null\n const org = input.org as Partial<AglynOrgBilling>\n const ops = await (deps.ops ?? aiBuildOps)({\n orgId: input.orgId,\n hostId: input.hostId,\n org,\n host,\n freeTaste: aiBuildFreeTaste(org),\n })\n const intents = aiBuildIntents(ops)\n return intents.length ? intents : null\n}\n"],"names":["ASSIST_BUILD_ACTION_ID","ASSIST_BUILD_SUMMARY_MAX_CHARS","ASSIST_BUILD_TOOL_NAME","aiBuildIntents","aiBuildOps","aiBuildFreeTaste","AI_JOB_BRIEF_MAX_CHARS","aiJobStepRunnerFor","STRING","type","assistBuildTool","name","description","inputSchema","properties","summary","brief","publish","required","additionalProperties","strict","assistBuildBlock","join","assistBuildIntentsBlock","intents","map","intent","ASSIST_BUILD_FACTS_LINE","ASKS_FOR_GAPS","assistBuildBriefWithoutGaps","split","line","sentence","test","kept","replace","filter","clause","trim","Boolean","resolveAssistBuild","input","scope","asked","where","screen","facts","slice","length","id","hostId","assistBuildIntents","firestore","deps","hasBuildRunner","host","collection","doc","get","data","org","ops","orgId","freeTaste"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/server/assist-build.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { AglynOrgBilling } from '@aglyn/aglyn/foundation/definitions/org-billing.types'\nimport {\n ASSIST_BUILD_ACTION_ID,\n ASSIST_BUILD_SUMMARY_MAX_CHARS,\n ASSIST_BUILD_TOOL_NAME,\n type AssistBuildProposal,\n} from '../model/assist-build'\nimport type { AiTool } from '../providers/contract'\nimport { aiBuildIntents, aiBuildOps } from '../jobs/ai-build-capabilities'\nimport { aiBuildFreeTaste } from '../jobs/ai-job-build-step'\nimport { AI_JOB_BRIEF_MAX_CHARS } from '../jobs/ai-job-text-step'\nimport { aiJobStepRunnerFor } from '../jobs/ai-jobs'\n\n/**\n * The chat's build rung (AGL-3616): on a site, a request that names things\n * to make — \"a few new pages, a booking form and a contact form, then an\n * about page\" — is proposed as ONE build through a strict tool. The tool\n * carries the request, not a plan: planning is the build job's own plan\n * step, against the site's real inventory, with its re-ask and its limits.\n * The panel starts that job and shows its plan card with one price; nothing\n * is built until the person confirms it, and what is built is a draft unless\n * they asked to publish and tick the card's box.\n */\n\nconst STRING = { type: 'string' } as const\n\n/** The strict tool the model proposes a build through. */\nexport function assistBuildTool(): AiTool {\n return {\n name: ASSIST_BUILD_TOOL_NAME,\n description:\n 'Propose building what the user asked for on this site: pages, a header ' +\n 'and footer, forms and the other things listed. The user sees a plan with ' +\n 'its price and confirms it before anything is made. Call at most once per ' +\n 'message, with the whole request.',\n inputSchema: {\n type: 'object',\n properties: {\n summary: STRING,\n brief: STRING,\n publish: { type: 'boolean' },\n },\n required: ['summary', 'brief', 'publish'],\n additionalProperties: false,\n },\n strict: true,\n }\n}\n\n/**\n * The build protocol — the same for every site, so it sits inside the cached\n * prefix. What a build can make on THIS site is per site and rides\n * `assistBuildIntentsBlock`, after every breakpoint.\n */\nexport function assistBuildBlock(): string {\n return [\n 'Building on this site:',\n `- When the user asks you to make or set up things on their site — pages, a header and footer, forms, or anything listed in the \"A build here can make\" block — write one or two sentences saying what you will plan, then call the ${ASSIST_BUILD_TOOL_NAME} tool once with the whole request. Write no JSON in your message.`,\n '- brief: everything they asked for, in their words, with each page, form and other part named and what each is for. Include what the conversation already settled. Do not invent parts they did not ask for beyond what their request plainly needs (a page that shows a form needs that form).',\n // AGL-4116: each follow-up to a plan made another plan beside it.\n '- When they change a plan you proposed earlier in this conversation and have not confirmed, call the tool again with the whole request as it now stands: what still holds from before, with their change. The new plan replaces the waiting one; say you have updated the plan.',\n '- publish: true only when they asked for the new pages to go live or be published; otherwise false. New work is an unpublished draft by default.',\n '- summary: one short sentence, such as \"Plan three pages, a contact form and a booking service\".',\n // AGL-4092: a brief the chat wrote asked the build to \"use bracketed placeholders where details are missing\".\n '- Never ask the build for placeholders, brackets or \"to be added\" text. Where the user gave no fact, say nothing of it: the build reads the site’s business profile and contact details itself.',\n '- Nothing is built when you call the tool: the user is shown a plan with its price and confirms it. Never say anything is made, and never quote a price.',\n '- Only offer what the \"A build here can make\" block lists. If they ask for something it does not list, say plainly it cannot be built here from the chat and build the rest.',\n '- A change to the site’s theme or look (its colors, fonts or corners) is offered only where the \"A build here can make\" block lists it, which it does on paid plans. When it is asked for and not listed, say a build cannot change the theme on this workspace’s plan and that they can change it in Setup → Theme, where the Theme assistant can propose one; then build the rest.',\n '- A question, or a change to the canvas the user has open, is not a build: answer it, or use the canvas edit tool where it is offered.',\n ].join('\\n')\n}\n\n/** What a build can make on this site, in each owner's own words. Per site: volatile. */\nexport function assistBuildIntentsBlock(intents: readonly string[]): string {\n return ['A build here can make:', ...intents.map((intent) => `- ${intent}`)].join('\\n')\n}\n\n/**\n * Where a build's facts come from, said once at the end of every brief the\n * chat proposes (AGL-4092): never a placeholder for one it lacks.\n */\nexport const ASSIST_BUILD_FACTS_LINE =\n 'Facts: use the site’s business profile and contact details on file; where a fact is missing, leave it out — no placeholders, no brackets.'\n\n/**\n * A clause of a brief that asks for a gap instead of a fact: \"use bracketed\n * placeholders where details are missing\", \"mark it in square brackets\",\n * \"show TBD\". A shop's \"shelf brackets\" asks for none.\n */\nconst ASKS_FOR_GAPS = /\\b(?:placeholders?|bracketed|in (?:square )?brackets|to be (?:added|filled|confirmed)|tbd)\\b/i\n\n/**\n * The brief with every clause that asks for placeholders, brackets or \"to be\n * added\" text taken out (AGL-4092): \"Do not invent names; use bracketed\n * placeholders where details are missing.\" keeps \"Do not invent names.\" A\n * sentence nothing is left of goes. Pure.\n */\nexport function assistBuildBriefWithoutGaps(brief: string): string {\n return brief\n .split('\\n')\n .map((line) =>\n line\n .split(/(?<=[.!?])\\s+/)\n .map((sentence) => {\n if (!ASKS_FOR_GAPS.test(sentence)) return sentence\n const kept = sentence\n .replace(/[.!?]$/, '')\n .split(/\\s*;\\s*/)\n .filter((clause) => !ASKS_FOR_GAPS.test(clause))\n .join('; ')\n .trim()\n return kept ? `${kept}.` : ''\n })\n .filter(Boolean)\n .join(' '),\n )\n .filter((line) => line.trim())\n .join('\\n')\n .trim()\n}\n\n/**\n * The tool call held to what a build job accepts, or `null` when it is\n * unusable. The site is the request's own, never the model's; the brief is\n * clamped to the job door's limit, with a line naming the console page it\n * was asked from so the plan step can read \"this page\" against it. Before\n * it, the brief asks for no placeholder and says where facts come from\n * (AGL-4092).\n */\nexport function resolveAssistBuild(\n input: Record<string, unknown> | null,\n scope: { hostId: string; screen: string | null },\n): AssistBuildProposal | null {\n if (!input) return null\n const asked = typeof input['brief'] === 'string' ? assistBuildBriefWithoutGaps(input['brief'].trim()) : ''\n if (!asked) return null\n const where = scope.screen ? `\\nAsked from the console page: ${scope.screen}` : ''\n const facts = `\\n${ASSIST_BUILD_FACTS_LINE}`\n const brief = `${asked.slice(0, AI_JOB_BRIEF_MAX_CHARS - facts.length - where.length).trim()}${facts}${where}`.trim()\n const summary =\n typeof input['summary'] === 'string' && input['summary'].trim()\n ? input['summary'].trim().slice(0, ASSIST_BUILD_SUMMARY_MAX_CHARS)\n : 'Plan what you asked for'\n return {\n id: ASSIST_BUILD_ACTION_ID,\n hostId: scope.hostId,\n brief,\n publish: input['publish'] === true,\n summary,\n }\n}\n\nexport interface AssistBuildIntentsDeps {\n ops?: typeof aiBuildOps\n hasBuildRunner?: () => boolean\n}\n\n/**\n * What a build may make on this site, or `null` where none can be built: a\n * deployment that has not loaded the build kind, or a site where no\n * operation passes its owner's gates. The same operations the build's plan\n * step offers, so the chat never offers what the plan cannot make.\n */\nexport async function assistBuildIntents(\n firestore: FirebaseFirestore.Firestore,\n input: { orgId: string; hostId: string; org: Record<string, unknown> },\n deps: AssistBuildIntentsDeps = {},\n): Promise<string[] | null> {\n const hasBuildRunner = deps.hasBuildRunner ?? (() => aiJobStepRunnerFor('build') !== null)\n if (!hasBuildRunner()) return null\n const host = ((await firestore.collection('hosts').doc(input.hostId).get()).data() ?? null) as Record<\n string,\n unknown\n > | null\n if (!host) return null\n const org = input.org as Partial<AglynOrgBilling>\n const ops = await (deps.ops ?? aiBuildOps)({\n orgId: input.orgId,\n hostId: input.hostId,\n org,\n host,\n freeTaste: aiBuildFreeTaste(org),\n })\n const intents = aiBuildIntents(ops)\n return intents.length ? intents : null\n}\n"],"names":["ASSIST_BUILD_ACTION_ID","ASSIST_BUILD_SUMMARY_MAX_CHARS","ASSIST_BUILD_TOOL_NAME","aiBuildIntents","aiBuildOps","aiBuildFreeTaste","AI_JOB_BRIEF_MAX_CHARS","aiJobStepRunnerFor","STRING","type","assistBuildTool","name","description","inputSchema","properties","summary","brief","publish","required","additionalProperties","strict","assistBuildBlock","join","assistBuildIntentsBlock","intents","map","intent","ASSIST_BUILD_FACTS_LINE","ASKS_FOR_GAPS","assistBuildBriefWithoutGaps","split","line","sentence","test","kept","replace","filter","clause","trim","Boolean","resolveAssistBuild","input","scope","asked","where","screen","facts","slice","length","id","hostId","assistBuildIntents","firestore","deps","hasBuildRunner","host","collection","doc","get","data","org","ops","orgId","freeTaste"],"mappings":"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"}
|
|
@@ -34,6 +34,8 @@ import { composeDocsLinksAnswer, deflectToDocs, questionStandsAlone, sectionLabe
|
|
|
34
34
|
import { ASSIST_EDIT_TOOL_NAME, assistEditDocumentOf } from "../model/assist-edit.js";
|
|
35
35
|
import { ASSIST_EDIT_MAX_OUTPUT_TOKENS, assistEditTool, editCanvasBlock, editSelectionBlock, parseAssistEditContext, resolveAssistEdit } from "./assist-edit.js";
|
|
36
36
|
import { ASSIST_BUILD_TOOL_NAME } from "../model/assist-build.js";
|
|
37
|
+
import { ASSIST_PAGE_CHANGE_TOOL_NAME } from "../model/assist-page-change.js";
|
|
38
|
+
import { assistPageChangeBlock, assistPageChangeTool, assistSitePagesBlock, readAssistSitePages, resolveAssistPageChange } from "./assist-page-change.js";
|
|
37
39
|
import { assistBuildDraftsBlock, assistDraftsForTurn, parseAssistBuildDrafts } from "../model/assist-follow-up.js";
|
|
38
40
|
import { assistBuildBlock, assistBuildIntents, assistBuildIntentsBlock, assistBuildTool, resolveAssistBuild } from "./assist-build.js";
|
|
39
41
|
import { describeView, extractAssistAction, finalAssistText, resolveAssistProposal, safeOrgFacts, sanitiseId, sanitiseRoute, viewFactsBlock, viewScreenBlock, visibleAssistText, withAssistOpenDraftAction } from "./assist-view-context.js";
|
|
@@ -849,7 +851,7 @@ async function handler(request) {
|
|
|
849
851
|
});
|
|
850
852
|
}
|
|
851
853
|
try {
|
|
852
|
-
var _resolved_org, _org_slug, _ref, _ref1, _ref2, _ref3, _ref4, _ref5, _ref6, _ref7;
|
|
854
|
+
var _resolved_org, _org_slug, _ref, _ref1, _ref2, _ref3, _ref4, _ref5, _ref6, _ref7, _assistSitePagesBlock;
|
|
853
855
|
var _body_context, _body_context1, _body_context2, _body_context3, _quota_allotment, _quota_allotment1, _body_context4, _body_context5;
|
|
854
856
|
const app = firebaseAdmin.app();
|
|
855
857
|
const decoded = await app.auth().verifyIdToken(idToken);
|
|
@@ -971,6 +973,22 @@ async function handler(request) {
|
|
|
971
973
|
member: resolved.member,
|
|
972
974
|
firestore
|
|
973
975
|
});
|
|
976
|
+
// A change to a page the site already has (AGL-4116): wherever a build
|
|
977
|
+
// may be proposed and no canvas is open (the canvas's own edit answers
|
|
978
|
+
// there), on every plan — the change goes into the page's draft, and a
|
|
979
|
+
// new version only where it is asked for and the plan keeps them. The
|
|
980
|
+
// edit job's door checks the page again when the panel starts it.
|
|
981
|
+
const pageChangeRung = buildRung && !editRung ? await readAssistSitePages(firestore, buildRung.hostId).then((pages)=>pages.length ? {
|
|
982
|
+
hostId: buildRung.hostId,
|
|
983
|
+
pages,
|
|
984
|
+
versioning: checkEntitlement(org, 'versioning')
|
|
985
|
+
} : null).catch((error)=>{
|
|
986
|
+
console.error('assist page change rung failed', {
|
|
987
|
+
orgId: body.orgId,
|
|
988
|
+
error
|
|
989
|
+
});
|
|
990
|
+
return null;
|
|
991
|
+
}) : null;
|
|
974
992
|
// ── Retrieval FIRST (AGL-2486) ────────────────────────────────────────
|
|
975
993
|
// Level-1 grounding for everyone. Hoisted above the reservation and the
|
|
976
994
|
// model call because it is now a candidate ANSWER, not only a prompt
|
|
@@ -1360,9 +1378,11 @@ async function handler(request) {
|
|
|
1360
1378
|
}
|
|
1361
1379
|
] : [],
|
|
1362
1380
|
// The build protocol (AGL-3616): the same for every site, so cached.
|
|
1381
|
+
// A page change's protocol rides the same block (AGL-4116), so it
|
|
1382
|
+
// takes no breakpoint of its own.
|
|
1363
1383
|
...buildRung ? [
|
|
1364
1384
|
{
|
|
1365
|
-
text: assistBuildBlock(),
|
|
1385
|
+
text: pageChangeRung ? `${assistBuildBlock()}\n\n${assistPageChangeBlock()}` : assistBuildBlock(),
|
|
1366
1386
|
cacheBreakpoint: true
|
|
1367
1387
|
}
|
|
1368
1388
|
] : [],
|
|
@@ -1411,6 +1431,15 @@ async function handler(request) {
|
|
|
1411
1431
|
volatile: true
|
|
1412
1432
|
}
|
|
1413
1433
|
] : [],
|
|
1434
|
+
// The pages a change may name (AGL-4116): per site, so volatile.
|
|
1435
|
+
...pageChangeRung ? [
|
|
1436
|
+
{
|
|
1437
|
+
text: (_assistSitePagesBlock = assistSitePagesBlock(pageChangeRung.pages, {
|
|
1438
|
+
versioning: pageChangeRung.versioning
|
|
1439
|
+
})) != null ? _assistSitePagesBlock : '',
|
|
1440
|
+
volatile: true
|
|
1441
|
+
}
|
|
1442
|
+
] : [],
|
|
1414
1443
|
...docsBlock ? [
|
|
1415
1444
|
{
|
|
1416
1445
|
text: docsBlock,
|
|
@@ -1434,6 +1463,9 @@ async function handler(request) {
|
|
|
1434
1463
|
] : [],
|
|
1435
1464
|
...buildRung ? [
|
|
1436
1465
|
assistBuildTool()
|
|
1466
|
+
] : [],
|
|
1467
|
+
...pageChangeRung ? [
|
|
1468
|
+
assistPageChangeTool()
|
|
1437
1469
|
] : []
|
|
1438
1470
|
]
|
|
1439
1471
|
} : {}, CHAT_ROUTE.thinking ? {
|
|
@@ -1478,6 +1510,7 @@ async function handler(request) {
|
|
|
1478
1510
|
let stopReason = null;
|
|
1479
1511
|
/** The first call of the edit tool, as the stream delivered it. */ let editInput = null;
|
|
1480
1512
|
/** The first call of the build tool (AGL-3616). */ let buildInput = null;
|
|
1513
|
+
/** The first call of the page change tool (AGL-4116). */ let pageChangeInput = null;
|
|
1481
1514
|
/**
|
|
1482
1515
|
* What the model wrote into its tool calls, when the stream stopped
|
|
1483
1516
|
* at its ceiling (AGL-3143). Held here only long enough to be
|
|
@@ -1531,6 +1564,9 @@ async function handler(request) {
|
|
|
1531
1564
|
if (buildRung && !buildInput && event.name === ASSIST_BUILD_TOOL_NAME) {
|
|
1532
1565
|
buildInput = event.input;
|
|
1533
1566
|
}
|
|
1567
|
+
if (pageChangeRung && !pageChangeInput && event.name === ASSIST_PAGE_CHANGE_TOOL_NAME) {
|
|
1568
|
+
pageChangeInput = event.input;
|
|
1569
|
+
}
|
|
1534
1570
|
} else if (event.type === 'done') {
|
|
1535
1571
|
var _event_rawOutput;
|
|
1536
1572
|
usage = event.usage;
|
|
@@ -1598,6 +1634,16 @@ async function handler(request) {
|
|
|
1598
1634
|
hostId: buildRung.hostId,
|
|
1599
1635
|
screen: (_ref = (_ref1 = guide == null ? void 0 : (_guide_view = guide.view) == null ? void 0 : _guide_view.screen) != null ? _ref1 : (_describeView = describeView(sanitiseRoute((_ref2 = (_body_context = body.context) == null ? void 0 : _body_context.route) != null ? _ref2 : ''))) == null ? void 0 : _describeView.screen) != null ? _ref : null
|
|
1600
1636
|
}) : null;
|
|
1637
|
+
// The page change (AGL-4116), held to the site's own pages. Still
|
|
1638
|
+
// nothing is changed: the panel starts the edit job, whose draft
|
|
1639
|
+
// the person opens in the Besigner and publishes.
|
|
1640
|
+
const pageChange = pageChangeRung && pageChangeInput && stopReason !== 'max_tokens' ? resolveAssistPageChange(pageChangeInput, pageChangeRung) : null;
|
|
1641
|
+
if (pageChangeRung && pageChangeInput && !pageChange && stopReason !== 'max_tokens') {
|
|
1642
|
+
emit({
|
|
1643
|
+
type: 'error',
|
|
1644
|
+
error: 'That page could not be found on this site, so nothing was changed. Say which page and ask again.'
|
|
1645
|
+
});
|
|
1646
|
+
}
|
|
1601
1647
|
// A refusal is an HTTP 200 with an empty or partial answer, not an
|
|
1602
1648
|
// error — so without this the user watches the spinner stop and
|
|
1603
1649
|
// gets nothing back. Truncation is the same shape: the tokens are
|
|
@@ -1698,6 +1744,9 @@ async function handler(request) {
|
|
|
1698
1744
|
// The build proposal (AGL-3616): the panel starts its job and
|
|
1699
1745
|
// shows the plan card; nothing is built before it is confirmed.
|
|
1700
1746
|
build,
|
|
1747
|
+
// The page change (AGL-4116): the panel starts its edit job and
|
|
1748
|
+
// links the draft it makes in the Besigner.
|
|
1749
|
+
pageChange,
|
|
1701
1750
|
// The reservation VERBATIM (AGL-2238). It already describes the
|
|
1702
1751
|
// standing after the message, because it is what moved the
|
|
1703
1752
|
// counter — `reserveAssistMessage` returns `used + 1` and
|