@aglyn/plugins-ai 1.0.0-beta.244 → 1.0.0-beta.246
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/activity/ai-activity-actions.js +3 -1
- package/src/lib/activity/ai-activity-actions.js.map +1 -1
- package/src/lib/components/ai-job-links.js +7 -0
- package/src/lib/components/ai-job-links.js.map +1 -1
- package/src/lib/components/ai-product-zones.d.ts +3 -1
- package/src/lib/components/ai-product-zones.js.map +1 -1
- package/src/lib/components/ai-products-hub-card.component.js +1 -1
- package/src/lib/components/ai-products-hub-card.component.js.map +1 -1
- package/src/lib/components/ai-products-proposals.component.js +17 -9
- package/src/lib/components/ai-products-proposals.component.js.map +1 -1
- package/src/lib/components/ai-site-start-card.component.js +6 -4
- package/src/lib/components/ai-site-start-card.component.js.map +1 -1
- package/src/lib/components/ai-upsell-dialog.component.js +1 -1
- package/src/lib/components/ai-upsell-dialog.component.js.map +1 -1
- package/src/lib/jobs/ai-build-capabilities.js +162 -2
- package/src/lib/jobs/ai-build-capabilities.js.map +1 -1
- package/src/lib/jobs/ai-job-build-step.d.ts +7 -0
- package/src/lib/jobs/ai-job-build-step.js +85 -16
- package/src/lib/jobs/ai-job-build-step.js.map +1 -1
- package/src/lib/jobs/ai-job-duplicate-step.d.ts +123 -0
- package/src/lib/jobs/ai-job-duplicate-step.js +269 -0
- package/src/lib/jobs/ai-job-duplicate-step.js.map +1 -0
- package/src/lib/jobs/ai-job-layout-language.js +13 -0
- package/src/lib/jobs/ai-job-layout-language.js.map +1 -1
- package/src/lib/jobs/ai-job-navigation-step.d.ts +157 -0
- package/src/lib/jobs/ai-job-navigation-step.js +439 -0
- package/src/lib/jobs/ai-job-navigation-step.js.map +1 -0
- package/src/lib/jobs/ai-job-page-language.d.ts +2 -1
- package/src/lib/jobs/ai-job-page-language.js +21 -5
- package/src/lib/jobs/ai-job-page-language.js.map +1 -1
- package/src/lib/jobs/ai-job-page-sections.js +1 -1
- package/src/lib/jobs/ai-job-page-sections.js.map +1 -1
- package/src/lib/jobs/ai-job-plan-step.d.ts +4 -2
- package/src/lib/jobs/ai-job-plan-step.js +16 -7
- package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
- package/src/lib/jobs/ai-job-products-step.d.ts +2 -2
- package/src/lib/jobs/ai-job-products-step.js +2 -2
- package/src/lib/jobs/ai-job-products-step.js.map +1 -1
- package/src/lib/jobs/ai-job-settings-step.d.ts +272 -0
- package/src/lib/jobs/ai-job-settings-step.js +695 -0
- package/src/lib/jobs/ai-job-settings-step.js.map +1 -0
- package/src/lib/jobs/ai-job-site-content.d.ts +20 -6
- package/src/lib/jobs/ai-job-site-content.js +28 -12
- package/src/lib/jobs/ai-job-site-content.js.map +1 -1
- package/src/lib/jobs/ai-job-site-datasets.d.ts +2 -0
- package/src/lib/jobs/ai-job-site-datasets.js +10 -3
- package/src/lib/jobs/ai-job-site-datasets.js.map +1 -1
- package/src/lib/jobs/ai-job-site-step.d.ts +3 -1
- package/src/lib/jobs/ai-job-site-step.js +70 -10
- package/src/lib/jobs/ai-job-site-step.js.map +1 -1
- package/src/lib/jobs/ai-site-publish.d.ts +62 -4
- package/src/lib/jobs/ai-site-publish.js +203 -56
- package/src/lib/jobs/ai-site-publish.js.map +1 -1
- package/src/lib/layout-language/ai-layout-compiler.d.ts +7 -4
- package/src/lib/layout-language/ai-layout-compiler.js +26 -25
- package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
- package/src/lib/layout-language/ai-layout-frame-hollows.d.ts +41 -0
- package/src/lib/layout-language/ai-layout-frame-hollows.js +330 -0
- package/src/lib/layout-language/ai-layout-frame-hollows.js.map +1 -0
- package/src/lib/layout-language/ai-layout-frame.d.ts +18 -1
- package/src/lib/layout-language/ai-layout-frame.js +83 -13
- package/src/lib/layout-language/ai-layout-frame.js.map +1 -1
- package/src/lib/layout-language/ai-layout-listings.d.ts +35 -0
- package/src/lib/layout-language/ai-layout-listings.js +34 -0
- package/src/lib/layout-language/ai-layout-listings.js.map +1 -1
- package/src/lib/layout-language/ai-layout-tree.d.ts +2 -0
- package/src/lib/layout-language/ai-layout-tree.js +17 -0
- package/src/lib/layout-language/ai-layout-tree.js.map +1 -1
- package/src/lib/model/ai-build-job.d.ts +23 -0
- package/src/lib/model/ai-build-job.js +102 -13
- package/src/lib/model/ai-build-job.js.map +1 -1
- package/src/lib/model/ai-build-plan.d.ts +25 -0
- package/src/lib/model/ai-build-plan.js +45 -1
- package/src/lib/model/ai-build-plan.js.map +1 -1
- package/src/lib/model/ai-credit-estimate.d.ts +52 -0
- package/src/lib/model/ai-credit-estimate.js +51 -0
- package/src/lib/model/ai-credit-estimate.js.map +1 -1
- package/src/lib/model/ai-job-activity.js +4 -1
- package/src/lib/model/ai-job-activity.js.map +1 -1
- package/src/lib/model/ai-jobs.types.d.ts +10 -2
- package/src/lib/model/ai-jobs.types.js +4 -1
- package/src/lib/model/ai-jobs.types.js.map +1 -1
- package/src/lib/model/ai-layout-menus.d.ts +104 -0
- package/src/lib/model/ai-layout-menus.js +518 -0
- package/src/lib/model/ai-layout-menus.js.map +1 -0
- package/src/lib/model/ai-products.d.ts +31 -1
- package/src/lib/model/ai-products.js +34 -1
- package/src/lib/model/ai-products.js.map +1 -1
- package/src/lib/model/ai-site-inventory.d.ts +15 -0
- package/src/lib/model/ai-site-inventory.js.map +1 -1
- package/src/lib/model/ai-site-job.d.ts +10 -0
- package/src/lib/model/ai-site-job.js +36 -9
- package/src/lib/model/ai-site-job.js.map +1 -1
- package/src/lib/model/ai-site-store-pages.js +1 -1
- package/src/lib/model/ai-site-store-pages.js.map +1 -1
- package/src/lib/runtime/ai-doctrine-validators.d.ts +53 -0
- package/src/lib/runtime/ai-doctrine-validators.js +166 -25
- package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
- package/src/lib/runtime/ai-doctrine.js +7 -2
- package/src/lib/runtime/ai-doctrine.js.map +1 -1
- package/src/lib/runtime/ai-palette.generated.js +1 -1
- package/src/lib/runtime/ai-palette.generated.js.map +1 -1
- package/src/lib/runtime/ai-products-generation.d.ts +4 -2
- package/src/lib/runtime/ai-products-generation.js +12 -6
- package/src/lib/runtime/ai-products-generation.js.map +1 -1
- package/src/lib/runtime/site-inventory.d.ts +12 -0
- package/src/lib/runtime/site-inventory.js +29 -1
- package/src/lib/runtime/site-inventory.js.map +1 -1
- package/src/lib/server.js +12 -0
- package/src/lib/server.js.map +1 -1
- package/src/lib/testing/stand-in-dataset-index.js +37 -3
- package/src/lib/testing/stand-in-dataset-index.js.map +1 -1
- package/src/lib/tools/ai-products-tool.d.ts +3 -1
- package/src/lib/tools/ai-products-tool.js +15 -5
- package/src/lib/tools/ai-products-tool.js.map +1 -1
|
@@ -32,8 +32,10 @@ import type { AiSystemBlock } from './ai-runtime';
|
|
|
32
32
|
* Every answer is held by a check in `tools/ai-products-tool.ts`, which is
|
|
33
33
|
* where storefront copy safety is enforced — no health, financial or legal
|
|
34
34
|
* claim, no certification or endorsement the merchant did not state, and no
|
|
35
|
-
* price. The rules below say the same, so the model is never
|
|
36
|
-
* rule it was not told.
|
|
35
|
+
* price in any text. The rules below say the same, so the model is never
|
|
36
|
+
* refused for a rule it was not told. A catalog's products each carry a
|
|
37
|
+
* suggested price in a field of its own (Zach, 2026-10-12), which code keeps
|
|
38
|
+
* in range (`aiCatalogPrice`); the copy still states none.
|
|
37
39
|
*/
|
|
38
40
|
/** The step kind every generation of a `products` job is routed as. */
|
|
39
41
|
export declare const AI_PRODUCTS_STEP: AiStepKind;
|
|
@@ -30,8 +30,10 @@ import { runValidatedGeneration } from "./ai-doctrine.js";
|
|
|
30
30
|
* Every answer is held by a check in `tools/ai-products-tool.ts`, which is
|
|
31
31
|
* where storefront copy safety is enforced — no health, financial or legal
|
|
32
32
|
* claim, no certification or endorsement the merchant did not state, and no
|
|
33
|
-
* price. The rules below say the same, so the model is never
|
|
34
|
-
* rule it was not told.
|
|
33
|
+
* price in any text. The rules below say the same, so the model is never
|
|
34
|
+
* refused for a rule it was not told. A catalog's products each carry a
|
|
35
|
+
* suggested price in a field of its own (Zach, 2026-10-12), which code keeps
|
|
36
|
+
* in range (`aiCatalogPrice`); the copy still states none.
|
|
35
37
|
*/ /** The step kind every generation of a `products` job is routed as. */ export const AI_PRODUCTS_STEP = 'job.products';
|
|
36
38
|
/**
|
|
37
39
|
* Each generation's answer ceiling. A catalog is the largest answer the step
|
|
@@ -46,8 +48,9 @@ export const AI_PRODUCT_CATEGORIES_MAX_TOKENS = 2000;
|
|
|
46
48
|
/**
|
|
47
49
|
* Rules every storefront generation states, because its check refuses an
|
|
48
50
|
* answer for each of them.
|
|
49
|
-
*/ const
|
|
50
|
-
|
|
51
|
+
*/ const STOREFRONT_PRICE_RULE = 'Never write a price, a discount, a shipping promise or a stock level: the store owner sets those.';
|
|
52
|
+
const STOREFRONT_RULES = [
|
|
53
|
+
STOREFRONT_PRICE_RULE,
|
|
51
54
|
'Never say a product cures, treats, heals or prevents anything, and never promise a health, financial or legal result.',
|
|
52
55
|
'Never claim a certification, an award, a license, an endorsement or a best seller that the owner’s own words do not state.',
|
|
53
56
|
'Write plain text: no markup, no formatting marks, no emoji, no links.'
|
|
@@ -70,10 +73,13 @@ export const AI_PRODUCT_CATEGORIES_MAX_TOKENS = 2000;
|
|
|
70
73
|
];
|
|
71
74
|
/** The rules for a store's first products. Byte-identical on every request. */ export const AI_CATALOG_INSTRUCTIONS = [
|
|
72
75
|
{
|
|
73
|
-
text: 'You propose the first products of an online store from its owner’s brief. Each is a draft the owner completes: a name, a type, a description, tags, options, a search listing
|
|
76
|
+
text: 'You propose the first products of an online store from its owner’s brief. Each is a draft the owner completes: a name, a type, a description, tags, options, a search listing, what its photo should show and a suggested price.\n\n' + `Answer by calling ${AI_CATALOG_TOOL_NAME} exactly once. A reply in prose cannot be used.\n\n` + 'Rules:\n' + [
|
|
74
77
|
'Propose 6 to 12 products that fit the brief, unless the brief says how many.',
|
|
75
78
|
'Write only from the brief. Never invent a material, a size, an origin or a feature it does not give; where a shopper would need a fact the brief does not give, mark it in square brackets, like [material], in the description only.',
|
|
76
|
-
|
|
79
|
+
// The price has its own field (Zach, 2026-10-12); the copy states none.
|
|
80
|
+
'Never write a price, a discount, a shipping promise or a stock level in any text: the price goes only in its own field, and the owner sets the rest.',
|
|
81
|
+
...STOREFRONT_RULES.filter((rule)=>rule !== STOREFRONT_PRICE_RULE),
|
|
82
|
+
'Price each product as a small independent shop would list that kind of product, in US dollars, for the size or quantity the brief gives: for example a bar of handmade soap about 8 to 12, a body lotion 14 to 22, a candle 18 to 32, a ceramic mug 24 to 38, a gift set 30 to 45. Use a whole amount or one ending in .50 or .95. One price covers every option.',
|
|
77
83
|
`Options are only the choices a shopper makes, such as a size or a color, with at most ${AI_CATALOG_OPTION_VALUES_MAX} values each; a product with no choice has none.`,
|
|
78
84
|
'The photo says in one sentence what a photo of the product should show, so the owner can take or pick it. Never a link.',
|
|
79
85
|
'The search title names the product plainly, and neither it nor the search description marks a missing fact.',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/runtime/ai-products-generation.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n AI_CATALOG_OPTION_VALUES_MAX,\n AI_PRODUCT_CATEGORY_LIST_MAX,\n aiProductMerchantWords,\n type AiCatalogProduct,\n type AiCategoriesProposal,\n type AiProductCategory,\n type AiProductCopy,\n type AiProductFacts,\n} from '../model/ai-products'\nimport type { AiStepKind } from '../providers/catalog'\nimport type { AiImagePart, AiMessagePart, AiProvider } from '../providers/contract'\nimport { AI_ROUTING_TABLE, type AiPluginSettings } from '../providers/routing'\nimport {\n AI_CATALOG_TOOL,\n AI_CATALOG_TOOL_NAME,\n AI_CATEGORIES_TOOL,\n AI_CATEGORIES_TOOL_NAME,\n AI_PRODUCT_COPY_TOOL,\n AI_PRODUCT_COPY_TOOL_NAME,\n checkAiCatalog,\n checkAiCategories,\n checkAiProductCopy,\n} from '../tools/ai-products-tool'\nimport { runValidatedGeneration, type AiValidatedGeneration } from './ai-doctrine'\nimport type { AiSystemBlock } from './ai-runtime'\n\n/**\n * COMMERCE BY AI (AGL-2916): the three generations a `products` job makes, each\n * through the building doctrine's generation call (`runValidatedGeneration`):\n * the doctrine's cached block, which carries the acceptable-use rules, then the\n * generation's own rules, cached after it, and one strict tool. The store's\n * name, a product's facts, its categories, its photo and the brief ride in the\n * user turn, so no byte of a store sits inside the cached prefix, and no site\n * inventory is sent: product copy is written from the product.\n *\n * Every answer is held by a check in `tools/ai-products-tool.ts`, which is\n * where storefront copy safety is enforced — no health, financial or legal\n * claim, no certification or endorsement the merchant did not state, and no\n * price. The rules below say the same, so the model is never refused for a\n * rule it was not told.\n */\n\n/** The step kind every generation of a `products` job is routed as. */\nexport const AI_PRODUCTS_STEP: AiStepKind = 'job.products'\n\n/**\n * Each generation's answer ceiling. A catalog is the largest answer the step\n * asks for, so it takes the routing table's; one product's copy and a set of\n * categories with discounts are far smaller, and asking them for less keeps a\n * pass's least time short enough for a bulk job to run several a beat.\n * `ai-job-products-step.spec.ts` measures the largest answer each tool accepts\n * against its ceiling.\n */\nexport const AI_CATALOG_MAX_TOKENS = AI_ROUTING_TABLE[AI_PRODUCTS_STEP].maxTokens\nexport const AI_PRODUCT_COPY_MAX_TOKENS = 1_500\nexport const AI_PRODUCT_CATEGORIES_MAX_TOKENS = 2_000\n\n/**\n * Rules every storefront generation states, because its check refuses an\n * answer for each of them.\n */\nconst STOREFRONT_RULES = [\n 'Never write a price, a discount, a shipping promise or a stock level: the store owner sets those.',\n 'Never say a product cures, treats, heals or prevents anything, and never promise a health, financial or legal result.',\n 'Never claim a certification, an award, a license, an endorsement or a best seller that the owner’s own words do not state.',\n 'Write plain text: no markup, no formatting marks, no emoji, no links.',\n]\n\n/** The rules for one product's copy. Byte-identical on every request. */\nexport const AI_PRODUCT_COPY_INSTRUCTIONS: AiSystemBlock[] = [\n {\n text:\n 'You write the storefront copy for one product of an online store: its description, its search listing, its tags, the store categories it belongs in and clear names for its options.\\n\\n' +\n `Answer by calling ${AI_PRODUCT_COPY_TOOL_NAME} exactly once. A reply in prose cannot be used.\\n\\n` +\n 'Rules:\\n' +\n [\n 'Write only from what you are given: the product’s name, its current text, its tags, its options and its photo. Never invent a material, a size, a quantity, an origin or a feature. Where a shopper would need a fact you do not have, mark it in square brackets, like [material], in the description only.',\n 'Keep every fact the current text states, in your own words.',\n ...STOREFRONT_RULES,\n 'The description is short paragraphs a shopper reads. The search title names the product plainly; the search description is one or two sentences; neither marks a missing fact.',\n 'Tags are words or short phrases a shopper filters by.',\n 'Choose categories only from the list you are given, by id, and none when none fits.',\n 'Give one name per option the product has, in order, keeping a name that is already clear.',\n 'When a photo is attached, use only what it plainly shows, and prefer the product’s own text where the two differ. What it shows — a color, a finish, a shape, a part — is a fact you have: write it, rather than marking it in brackets.',\n 'Write in the language of the product’s name and text.',\n ]\n .map((rule) => `- ${rule}`)\n .join('\\n'),\n cacheBreakpoint: true,\n },\n]\n\n/** The rules for a store's first products. Byte-identical on every request. */\nexport const AI_CATALOG_INSTRUCTIONS: AiSystemBlock[] = [\n {\n text:\n 'You propose the first products of an online store from its owner’s brief. Each is a draft the owner completes: a name, a type, a description, tags, options, a search listing and what its photo should show.\\n\\n' +\n `Answer by calling ${AI_CATALOG_TOOL_NAME} exactly once. A reply in prose cannot be used.\\n\\n` +\n 'Rules:\\n' +\n [\n 'Propose 6 to 12 products that fit the brief, unless the brief says how many.',\n 'Write only from the brief. Never invent a material, a size, an origin or a feature it does not give; where a shopper would need a fact the brief does not give, mark it in square brackets, like [material], in the description only.',\n ...STOREFRONT_RULES,\n `Options are only the choices a shopper makes, such as a size or a color, with at most ${AI_CATALOG_OPTION_VALUES_MAX} values each; a product with no choice has none.`,\n 'The photo says in one sentence what a photo of the product should show, so the owner can take or pick it. Never a link.',\n 'The search title names the product plainly, and neither it nor the search description marks a missing fact.',\n 'Write in the language of the brief.',\n ]\n .map((rule) => `- ${rule}`)\n .join('\\n'),\n cacheBreakpoint: true,\n },\n]\n\n/** The rules for categories and discounts. Byte-identical on every request. */\nexport const AI_CATEGORIES_INSTRUCTIONS: AiSystemBlock[] = [\n {\n text:\n 'You propose store categories and a first set of discounts from an online store owner’s brief. The owner creates the ones they accept, and every discount starts switched off.\\n\\n' +\n `Answer by calling ${AI_CATEGORIES_TOOL_NAME} exactly once. A reply in prose cannot be used.\\n\\n` +\n 'Rules:\\n' +\n [\n 'Categories group the store’s products the way a shopper browses them. Never repeat a category the store already has.',\n 'A discount is a percentage off, an amount off in dollars, or free shipping, with an optional minimum order, and a code a shopper types or no code when it applies on its own. Keep them few and modest, and in line with the brief.',\n 'Each category and each discount says in one sentence why it helps this store.',\n 'Never promise savings, earnings or results beyond the discount itself, and never make a health, financial or legal claim.',\n 'Write plain text: no markup, no emoji, no links.',\n 'Propose nothing the brief gives no reason for; an empty list is an answer.',\n 'Write in the language of the brief.',\n ]\n .map((rule) => `- ${rule}`)\n .join('\\n'),\n cacheBreakpoint: true,\n },\n]\n\nexport interface AiProductCopyPromptInput {\n /** The store's name, as its visitors see it. */\n store: string\n product: AiProductFacts\n categories: readonly AiProductCategory[]\n /** Whether a photo rides with the request. */\n photo: boolean\n}\n\n/** The user turn's text: the store, the product as it stands, and the categories to choose from. */\nexport function aiProductCopyPrompt(input: AiProductCopyPromptInput): string {\n const { product } = input\n const lines = [\n `Store: ${input.store || 'untitled store'}`,\n `Product: ${product.name}`,\n `Type: ${product.type}`,\n 'Current text:',\n product.text.trim() || '(none)',\n `Current tags: ${product.tags.length ? product.tags.join(', ') : '(none)'}`,\n ]\n if (product.options.length) {\n lines.push('Options:', ...product.options.map((option) => `- ${option.name}: ${option.values.join(', ')}`))\n } else {\n lines.push('Options: (none)')\n }\n if (product.seoTitle || product.seoDescription) {\n lines.push(\n `Current search title: ${product.seoTitle || '(none)'}`,\n `Current search description: ${product.seoDescription || '(none)'}`,\n )\n }\n const categories = input.categories.slice(0, AI_PRODUCT_CATEGORY_LIST_MAX)\n lines.push(\n categories.length ? 'Categories to choose from:' : 'Categories to choose from: (none)',\n ...categories.map((category) => `- ${category.id}: ${category.name}`),\n `Photo: ${input.photo ? 'attached' : 'none'}`,\n )\n return lines.join('\\n')\n}\n\ninterface AiProductsGenerationBase {\n settings?: AiPluginSettings\n model?: string\n maxTokens?: number\n signal?: AbortSignal\n provider?: AiProvider\n}\n\nexport interface GenerateAiProductCopyInput extends AiProductsGenerationBase, AiProductCopyPromptInput {\n /** The product's photo, when the model reads pictures and the photo was read. */\n image?: AiImagePart | null\n}\n\n/**\n * Propose one product's copy. It asks the routing table's `job.products` model\n * through the doctrine's generation call and resolves to `ok` with the copy,\n * `needs_input` when the answer could not be held to the rules after one\n * re-ask, or `refused`. It writes nothing.\n */\nexport function generateAiProductCopy(input: GenerateAiProductCopyInput): Promise<AiValidatedGeneration<AiProductCopy>> {\n const image = input.photo ? (input.image ?? null) : null\n const content: AiMessagePart[] = [\n ...(image ? [image] : []),\n { type: 'text', text: aiProductCopyPrompt({ ...input, photo: Boolean(image) }) },\n ]\n return runValidatedGeneration('product copy', {\n step: AI_PRODUCTS_STEP,\n ...(input.settings ? { settings: input.settings } : {}),\n ...(input.model ? { model: input.model } : {}),\n instructions: AI_PRODUCT_COPY_INSTRUCTIONS,\n messages: [{ role: 'user', content: image ? content : aiProductCopyPrompt({ ...input, photo: false }) }],\n tool: AI_PRODUCT_COPY_TOOL,\n maxTokens: input.maxTokens ?? AI_PRODUCT_COPY_MAX_TOKENS,\n ...thinkingOf(),\n ...(input.signal ? { signal: input.signal } : {}),\n ...(input.provider ? { provider: input.provider } : {}),\n check: (answer) =>\n checkAiProductCopy(answer, {\n categoryIds: input.categories.slice(0, AI_PRODUCT_CATEGORY_LIST_MAX).map((category) => category.id),\n optionCount: input.product.options.length,\n merchantWords: aiProductMerchantWords(input.product),\n }),\n })\n}\n\nexport interface GenerateAiCatalogInput extends AiProductsGenerationBase {\n store: string\n brief: string\n}\n\n/** The user turn for a catalog: the store and the brief. */\nexport function aiCatalogPrompt(input: Pick<GenerateAiCatalogInput, 'store' | 'brief'>): string {\n return [`Store: ${input.store || 'untitled store'}`, 'Brief:', input.brief.trim()].join('\\n')\n}\n\n/** Propose a store's first products from a brief. It writes nothing. */\nexport function generateAiCatalog(input: GenerateAiCatalogInput & { maxTokens: number }): Promise<AiValidatedGeneration<AiCatalogProduct[]>> {\n return runValidatedGeneration('catalog', {\n step: AI_PRODUCTS_STEP,\n ...(input.settings ? { settings: input.settings } : {}),\n ...(input.model ? { model: input.model } : {}),\n instructions: AI_CATALOG_INSTRUCTIONS,\n messages: [{ role: 'user', content: aiCatalogPrompt(input) }],\n tool: AI_CATALOG_TOOL,\n maxTokens: input.maxTokens,\n ...thinkingOf(),\n ...(input.signal ? { signal: input.signal } : {}),\n ...(input.provider ? { provider: input.provider } : {}),\n check: (answer) => checkAiCatalog(answer, { merchantWords: input.brief }),\n })\n}\n\nexport interface GenerateAiCategoriesInput extends AiProductsGenerationBase {\n store: string\n brief: string\n /** The names of the categories the store already has. */\n existingCategoryNames: readonly string[]\n}\n\n/** The user turn for categories and discounts: the store, what it has, and the brief. */\nexport function aiCategoriesPrompt(input: Pick<GenerateAiCategoriesInput, 'store' | 'brief' | 'existingCategoryNames'>): string {\n const existing = input.existingCategoryNames.slice(0, AI_PRODUCT_CATEGORY_LIST_MAX)\n return [\n `Store: ${input.store || 'untitled store'}`,\n `Categories the store already has: ${existing.length ? existing.join(', ') : '(none)'}`,\n 'Brief:',\n input.brief.trim(),\n ].join('\\n')\n}\n\n/** Propose categories and discounts from a brief. It writes nothing. */\nexport function generateAiCategories(\n input: GenerateAiCategoriesInput & { maxTokens: number },\n): Promise<AiValidatedGeneration<Pick<AiCategoriesProposal, 'categories' | 'discounts'>>> {\n return runValidatedGeneration('store categories and discounts', {\n step: AI_PRODUCTS_STEP,\n ...(input.settings ? { settings: input.settings } : {}),\n ...(input.model ? { model: input.model } : {}),\n instructions: AI_CATEGORIES_INSTRUCTIONS,\n messages: [{ role: 'user', content: aiCategoriesPrompt(input) }],\n tool: AI_CATEGORIES_TOOL,\n maxTokens: input.maxTokens,\n ...thinkingOf(),\n ...(input.signal ? { signal: input.signal } : {}),\n ...(input.provider ? { provider: input.provider } : {}),\n check: (answer) =>\n checkAiCategories(answer, {\n existingCategoryNames: input.existingCategoryNames,\n merchantWords: input.brief,\n }),\n })\n}\n\n/** The routing row's thinking and effort, sent only where the row names one. */\nfunction thinkingOf() {\n const row = AI_ROUTING_TABLE[AI_PRODUCTS_STEP]\n return {\n ...(row.thinking ? { thinking: row.thinking } : {}),\n ...(row.effort ? { effort: row.effort } : {}),\n }\n}\n"],"names":["AI_CATALOG_OPTION_VALUES_MAX","AI_PRODUCT_CATEGORY_LIST_MAX","aiProductMerchantWords","AI_ROUTING_TABLE","AI_CATALOG_TOOL","AI_CATALOG_TOOL_NAME","AI_CATEGORIES_TOOL","AI_CATEGORIES_TOOL_NAME","AI_PRODUCT_COPY_TOOL","AI_PRODUCT_COPY_TOOL_NAME","checkAiCatalog","checkAiCategories","checkAiProductCopy","runValidatedGeneration","AI_PRODUCTS_STEP","AI_CATALOG_MAX_TOKENS","maxTokens","AI_PRODUCT_COPY_MAX_TOKENS","AI_PRODUCT_CATEGORIES_MAX_TOKENS","STOREFRONT_RULES","AI_PRODUCT_COPY_INSTRUCTIONS","text","map","rule","join","cacheBreakpoint","AI_CATALOG_INSTRUCTIONS","AI_CATEGORIES_INSTRUCTIONS","aiProductCopyPrompt","input","product","lines","store","name","type","trim","tags","length","options","push","option","values","seoTitle","seoDescription","categories","slice","category","id","photo","generateAiProductCopy","image","content","Boolean","step","settings","model","instructions","messages","role","tool","thinkingOf","signal","provider","check","answer","categoryIds","optionCount","merchantWords","aiCatalogPrompt","brief","generateAiCatalog","aiCategoriesPrompt","existing","existingCategoryNames","generateAiCategories","row","thinking","effort"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/runtime/ai-products-generation.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n AI_CATALOG_OPTION_VALUES_MAX,\n AI_PRODUCT_CATEGORY_LIST_MAX,\n aiProductMerchantWords,\n type AiCatalogProduct,\n type AiCategoriesProposal,\n type AiProductCategory,\n type AiProductCopy,\n type AiProductFacts,\n} from '../model/ai-products'\nimport type { AiStepKind } from '../providers/catalog'\nimport type { AiImagePart, AiMessagePart, AiProvider } from '../providers/contract'\nimport { AI_ROUTING_TABLE, type AiPluginSettings } from '../providers/routing'\nimport {\n AI_CATALOG_TOOL,\n AI_CATALOG_TOOL_NAME,\n AI_CATEGORIES_TOOL,\n AI_CATEGORIES_TOOL_NAME,\n AI_PRODUCT_COPY_TOOL,\n AI_PRODUCT_COPY_TOOL_NAME,\n checkAiCatalog,\n checkAiCategories,\n checkAiProductCopy,\n} from '../tools/ai-products-tool'\nimport { runValidatedGeneration, type AiValidatedGeneration } from './ai-doctrine'\nimport type { AiSystemBlock } from './ai-runtime'\n\n/**\n * COMMERCE BY AI (AGL-2916): the three generations a `products` job makes, each\n * through the building doctrine's generation call (`runValidatedGeneration`):\n * the doctrine's cached block, which carries the acceptable-use rules, then the\n * generation's own rules, cached after it, and one strict tool. The store's\n * name, a product's facts, its categories, its photo and the brief ride in the\n * user turn, so no byte of a store sits inside the cached prefix, and no site\n * inventory is sent: product copy is written from the product.\n *\n * Every answer is held by a check in `tools/ai-products-tool.ts`, which is\n * where storefront copy safety is enforced — no health, financial or legal\n * claim, no certification or endorsement the merchant did not state, and no\n * price in any text. The rules below say the same, so the model is never\n * refused for a rule it was not told. A catalog's products each carry a\n * suggested price in a field of its own (Zach, 2026-10-12), which code keeps\n * in range (`aiCatalogPrice`); the copy still states none.\n */\n\n/** The step kind every generation of a `products` job is routed as. */\nexport const AI_PRODUCTS_STEP: AiStepKind = 'job.products'\n\n/**\n * Each generation's answer ceiling. A catalog is the largest answer the step\n * asks for, so it takes the routing table's; one product's copy and a set of\n * categories with discounts are far smaller, and asking them for less keeps a\n * pass's least time short enough for a bulk job to run several a beat.\n * `ai-job-products-step.spec.ts` measures the largest answer each tool accepts\n * against its ceiling.\n */\nexport const AI_CATALOG_MAX_TOKENS = AI_ROUTING_TABLE[AI_PRODUCTS_STEP].maxTokens\nexport const AI_PRODUCT_COPY_MAX_TOKENS = 1_500\nexport const AI_PRODUCT_CATEGORIES_MAX_TOKENS = 2_000\n\n/**\n * Rules every storefront generation states, because its check refuses an\n * answer for each of them.\n */\nconst STOREFRONT_PRICE_RULE =\n 'Never write a price, a discount, a shipping promise or a stock level: the store owner sets those.'\nconst STOREFRONT_RULES = [\n STOREFRONT_PRICE_RULE,\n 'Never say a product cures, treats, heals or prevents anything, and never promise a health, financial or legal result.',\n 'Never claim a certification, an award, a license, an endorsement or a best seller that the owner’s own words do not state.',\n 'Write plain text: no markup, no formatting marks, no emoji, no links.',\n]\n\n/** The rules for one product's copy. Byte-identical on every request. */\nexport const AI_PRODUCT_COPY_INSTRUCTIONS: AiSystemBlock[] = [\n {\n text:\n 'You write the storefront copy for one product of an online store: its description, its search listing, its tags, the store categories it belongs in and clear names for its options.\\n\\n' +\n `Answer by calling ${AI_PRODUCT_COPY_TOOL_NAME} exactly once. A reply in prose cannot be used.\\n\\n` +\n 'Rules:\\n' +\n [\n 'Write only from what you are given: the product’s name, its current text, its tags, its options and its photo. Never invent a material, a size, a quantity, an origin or a feature. Where a shopper would need a fact you do not have, mark it in square brackets, like [material], in the description only.',\n 'Keep every fact the current text states, in your own words.',\n ...STOREFRONT_RULES,\n 'The description is short paragraphs a shopper reads. The search title names the product plainly; the search description is one or two sentences; neither marks a missing fact.',\n 'Tags are words or short phrases a shopper filters by.',\n 'Choose categories only from the list you are given, by id, and none when none fits.',\n 'Give one name per option the product has, in order, keeping a name that is already clear.',\n 'When a photo is attached, use only what it plainly shows, and prefer the product’s own text where the two differ. What it shows — a color, a finish, a shape, a part — is a fact you have: write it, rather than marking it in brackets.',\n 'Write in the language of the product’s name and text.',\n ]\n .map((rule) => `- ${rule}`)\n .join('\\n'),\n cacheBreakpoint: true,\n },\n]\n\n/** The rules for a store's first products. Byte-identical on every request. */\nexport const AI_CATALOG_INSTRUCTIONS: AiSystemBlock[] = [\n {\n text:\n 'You propose the first products of an online store from its owner’s brief. Each is a draft the owner completes: a name, a type, a description, tags, options, a search listing, what its photo should show and a suggested price.\\n\\n' +\n `Answer by calling ${AI_CATALOG_TOOL_NAME} exactly once. A reply in prose cannot be used.\\n\\n` +\n 'Rules:\\n' +\n [\n 'Propose 6 to 12 products that fit the brief, unless the brief says how many.',\n 'Write only from the brief. Never invent a material, a size, an origin or a feature it does not give; where a shopper would need a fact the brief does not give, mark it in square brackets, like [material], in the description only.',\n // The price has its own field (Zach, 2026-10-12); the copy states none.\n 'Never write a price, a discount, a shipping promise or a stock level in any text: the price goes only in its own field, and the owner sets the rest.',\n ...STOREFRONT_RULES.filter((rule) => rule !== STOREFRONT_PRICE_RULE),\n 'Price each product as a small independent shop would list that kind of product, in US dollars, for the size or quantity the brief gives: for example a bar of handmade soap about 8 to 12, a body lotion 14 to 22, a candle 18 to 32, a ceramic mug 24 to 38, a gift set 30 to 45. Use a whole amount or one ending in .50 or .95. One price covers every option.',\n `Options are only the choices a shopper makes, such as a size or a color, with at most ${AI_CATALOG_OPTION_VALUES_MAX} values each; a product with no choice has none.`,\n 'The photo says in one sentence what a photo of the product should show, so the owner can take or pick it. Never a link.',\n 'The search title names the product plainly, and neither it nor the search description marks a missing fact.',\n 'Write in the language of the brief.',\n ]\n .map((rule) => `- ${rule}`)\n .join('\\n'),\n cacheBreakpoint: true,\n },\n]\n\n/** The rules for categories and discounts. Byte-identical on every request. */\nexport const AI_CATEGORIES_INSTRUCTIONS: AiSystemBlock[] = [\n {\n text:\n 'You propose store categories and a first set of discounts from an online store owner’s brief. The owner creates the ones they accept, and every discount starts switched off.\\n\\n' +\n `Answer by calling ${AI_CATEGORIES_TOOL_NAME} exactly once. A reply in prose cannot be used.\\n\\n` +\n 'Rules:\\n' +\n [\n 'Categories group the store’s products the way a shopper browses them. Never repeat a category the store already has.',\n 'A discount is a percentage off, an amount off in dollars, or free shipping, with an optional minimum order, and a code a shopper types or no code when it applies on its own. Keep them few and modest, and in line with the brief.',\n 'Each category and each discount says in one sentence why it helps this store.',\n 'Never promise savings, earnings or results beyond the discount itself, and never make a health, financial or legal claim.',\n 'Write plain text: no markup, no emoji, no links.',\n 'Propose nothing the brief gives no reason for; an empty list is an answer.',\n 'Write in the language of the brief.',\n ]\n .map((rule) => `- ${rule}`)\n .join('\\n'),\n cacheBreakpoint: true,\n },\n]\n\nexport interface AiProductCopyPromptInput {\n /** The store's name, as its visitors see it. */\n store: string\n product: AiProductFacts\n categories: readonly AiProductCategory[]\n /** Whether a photo rides with the request. */\n photo: boolean\n}\n\n/** The user turn's text: the store, the product as it stands, and the categories to choose from. */\nexport function aiProductCopyPrompt(input: AiProductCopyPromptInput): string {\n const { product } = input\n const lines = [\n `Store: ${input.store || 'untitled store'}`,\n `Product: ${product.name}`,\n `Type: ${product.type}`,\n 'Current text:',\n product.text.trim() || '(none)',\n `Current tags: ${product.tags.length ? product.tags.join(', ') : '(none)'}`,\n ]\n if (product.options.length) {\n lines.push('Options:', ...product.options.map((option) => `- ${option.name}: ${option.values.join(', ')}`))\n } else {\n lines.push('Options: (none)')\n }\n if (product.seoTitle || product.seoDescription) {\n lines.push(\n `Current search title: ${product.seoTitle || '(none)'}`,\n `Current search description: ${product.seoDescription || '(none)'}`,\n )\n }\n const categories = input.categories.slice(0, AI_PRODUCT_CATEGORY_LIST_MAX)\n lines.push(\n categories.length ? 'Categories to choose from:' : 'Categories to choose from: (none)',\n ...categories.map((category) => `- ${category.id}: ${category.name}`),\n `Photo: ${input.photo ? 'attached' : 'none'}`,\n )\n return lines.join('\\n')\n}\n\ninterface AiProductsGenerationBase {\n settings?: AiPluginSettings\n model?: string\n maxTokens?: number\n signal?: AbortSignal\n provider?: AiProvider\n}\n\nexport interface GenerateAiProductCopyInput extends AiProductsGenerationBase, AiProductCopyPromptInput {\n /** The product's photo, when the model reads pictures and the photo was read. */\n image?: AiImagePart | null\n}\n\n/**\n * Propose one product's copy. It asks the routing table's `job.products` model\n * through the doctrine's generation call and resolves to `ok` with the copy,\n * `needs_input` when the answer could not be held to the rules after one\n * re-ask, or `refused`. It writes nothing.\n */\nexport function generateAiProductCopy(input: GenerateAiProductCopyInput): Promise<AiValidatedGeneration<AiProductCopy>> {\n const image = input.photo ? (input.image ?? null) : null\n const content: AiMessagePart[] = [\n ...(image ? [image] : []),\n { type: 'text', text: aiProductCopyPrompt({ ...input, photo: Boolean(image) }) },\n ]\n return runValidatedGeneration('product copy', {\n step: AI_PRODUCTS_STEP,\n ...(input.settings ? { settings: input.settings } : {}),\n ...(input.model ? { model: input.model } : {}),\n instructions: AI_PRODUCT_COPY_INSTRUCTIONS,\n messages: [{ role: 'user', content: image ? content : aiProductCopyPrompt({ ...input, photo: false }) }],\n tool: AI_PRODUCT_COPY_TOOL,\n maxTokens: input.maxTokens ?? AI_PRODUCT_COPY_MAX_TOKENS,\n ...thinkingOf(),\n ...(input.signal ? { signal: input.signal } : {}),\n ...(input.provider ? { provider: input.provider } : {}),\n check: (answer) =>\n checkAiProductCopy(answer, {\n categoryIds: input.categories.slice(0, AI_PRODUCT_CATEGORY_LIST_MAX).map((category) => category.id),\n optionCount: input.product.options.length,\n merchantWords: aiProductMerchantWords(input.product),\n }),\n })\n}\n\nexport interface GenerateAiCatalogInput extends AiProductsGenerationBase {\n store: string\n brief: string\n}\n\n/** The user turn for a catalog: the store and the brief. */\nexport function aiCatalogPrompt(input: Pick<GenerateAiCatalogInput, 'store' | 'brief'>): string {\n return [`Store: ${input.store || 'untitled store'}`, 'Brief:', input.brief.trim()].join('\\n')\n}\n\n/** Propose a store's first products from a brief. It writes nothing. */\nexport function generateAiCatalog(input: GenerateAiCatalogInput & { maxTokens: number }): Promise<AiValidatedGeneration<AiCatalogProduct[]>> {\n return runValidatedGeneration('catalog', {\n step: AI_PRODUCTS_STEP,\n ...(input.settings ? { settings: input.settings } : {}),\n ...(input.model ? { model: input.model } : {}),\n instructions: AI_CATALOG_INSTRUCTIONS,\n messages: [{ role: 'user', content: aiCatalogPrompt(input) }],\n tool: AI_CATALOG_TOOL,\n maxTokens: input.maxTokens,\n ...thinkingOf(),\n ...(input.signal ? { signal: input.signal } : {}),\n ...(input.provider ? { provider: input.provider } : {}),\n check: (answer) => checkAiCatalog(answer, { merchantWords: input.brief }),\n })\n}\n\nexport interface GenerateAiCategoriesInput extends AiProductsGenerationBase {\n store: string\n brief: string\n /** The names of the categories the store already has. */\n existingCategoryNames: readonly string[]\n}\n\n/** The user turn for categories and discounts: the store, what it has, and the brief. */\nexport function aiCategoriesPrompt(input: Pick<GenerateAiCategoriesInput, 'store' | 'brief' | 'existingCategoryNames'>): string {\n const existing = input.existingCategoryNames.slice(0, AI_PRODUCT_CATEGORY_LIST_MAX)\n return [\n `Store: ${input.store || 'untitled store'}`,\n `Categories the store already has: ${existing.length ? existing.join(', ') : '(none)'}`,\n 'Brief:',\n input.brief.trim(),\n ].join('\\n')\n}\n\n/** Propose categories and discounts from a brief. It writes nothing. */\nexport function generateAiCategories(\n input: GenerateAiCategoriesInput & { maxTokens: number },\n): Promise<AiValidatedGeneration<Pick<AiCategoriesProposal, 'categories' | 'discounts'>>> {\n return runValidatedGeneration('store categories and discounts', {\n step: AI_PRODUCTS_STEP,\n ...(input.settings ? { settings: input.settings } : {}),\n ...(input.model ? { model: input.model } : {}),\n instructions: AI_CATEGORIES_INSTRUCTIONS,\n messages: [{ role: 'user', content: aiCategoriesPrompt(input) }],\n tool: AI_CATEGORIES_TOOL,\n maxTokens: input.maxTokens,\n ...thinkingOf(),\n ...(input.signal ? { signal: input.signal } : {}),\n ...(input.provider ? { provider: input.provider } : {}),\n check: (answer) =>\n checkAiCategories(answer, {\n existingCategoryNames: input.existingCategoryNames,\n merchantWords: input.brief,\n }),\n })\n}\n\n/** The routing row's thinking and effort, sent only where the row names one. */\nfunction thinkingOf() {\n const row = AI_ROUTING_TABLE[AI_PRODUCTS_STEP]\n return {\n ...(row.thinking ? { thinking: row.thinking } : {}),\n ...(row.effort ? { effort: row.effort } : {}),\n }\n}\n"],"names":["AI_CATALOG_OPTION_VALUES_MAX","AI_PRODUCT_CATEGORY_LIST_MAX","aiProductMerchantWords","AI_ROUTING_TABLE","AI_CATALOG_TOOL","AI_CATALOG_TOOL_NAME","AI_CATEGORIES_TOOL","AI_CATEGORIES_TOOL_NAME","AI_PRODUCT_COPY_TOOL","AI_PRODUCT_COPY_TOOL_NAME","checkAiCatalog","checkAiCategories","checkAiProductCopy","runValidatedGeneration","AI_PRODUCTS_STEP","AI_CATALOG_MAX_TOKENS","maxTokens","AI_PRODUCT_COPY_MAX_TOKENS","AI_PRODUCT_CATEGORIES_MAX_TOKENS","STOREFRONT_PRICE_RULE","STOREFRONT_RULES","AI_PRODUCT_COPY_INSTRUCTIONS","text","map","rule","join","cacheBreakpoint","AI_CATALOG_INSTRUCTIONS","filter","AI_CATEGORIES_INSTRUCTIONS","aiProductCopyPrompt","input","product","lines","store","name","type","trim","tags","length","options","push","option","values","seoTitle","seoDescription","categories","slice","category","id","photo","generateAiProductCopy","image","content","Boolean","step","settings","model","instructions","messages","role","tool","thinkingOf","signal","provider","check","answer","categoryIds","optionCount","merchantWords","aiCatalogPrompt","brief","generateAiCatalog","aiCategoriesPrompt","existing","existingCategoryNames","generateAiCategories","row","thinking","effort"],"mappings":"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"}
|
|
@@ -61,6 +61,18 @@ export declare function aiStarterHomeCandidate(host: Readonly<Record<string, unk
|
|
|
61
61
|
* page — a plan keeps it.
|
|
62
62
|
*/
|
|
63
63
|
export declare function aiStarterHomeUntouched(publishedVersionId: string, versionIds: readonly string[]): boolean;
|
|
64
|
+
/**
|
|
65
|
+
* The record page a site already serves for a dataset (AGL-3747), from the
|
|
66
|
+
* `recordPage` fact the data plugin's index answers for a site: its base and
|
|
67
|
+
* the address field its pages are reached by. `null` where there is none, or
|
|
68
|
+
* where its address field is no longer one of the dataset's fields.
|
|
69
|
+
*/
|
|
70
|
+
export declare function aiInventoryRecordPage(fact: unknown, fields: ReadonlyArray<{
|
|
71
|
+
id: string;
|
|
72
|
+
}>): {
|
|
73
|
+
recordBase: string;
|
|
74
|
+
addressField: string;
|
|
75
|
+
} | null;
|
|
64
76
|
/**
|
|
65
77
|
* Read one site's inventory for a generation.
|
|
66
78
|
*
|
|
@@ -101,6 +101,22 @@ async function readWindow(query, fields, cap, map) {
|
|
|
101
101
|
*/ export function aiStarterHomeUntouched(publishedVersionId, versionIds) {
|
|
102
102
|
return Boolean(publishedVersionId) && versionIds.length === 1 && versionIds[0] === publishedVersionId;
|
|
103
103
|
}
|
|
104
|
+
/** A record template's base, as the data plugin keeps it: one to four URL-safe segments. */ const RECORD_BASE = /^[a-z0-9]+(?:-[a-z0-9]+)*(?:\/[a-z0-9]+(?:-[a-z0-9]+)*){0,3}$/;
|
|
105
|
+
/**
|
|
106
|
+
* The record page a site already serves for a dataset (AGL-3747), from the
|
|
107
|
+
* `recordPage` fact the data plugin's index answers for a site: its base and
|
|
108
|
+
* the address field its pages are reached by. `null` where there is none, or
|
|
109
|
+
* where its address field is no longer one of the dataset's fields.
|
|
110
|
+
*/ export function aiInventoryRecordPage(fact, fields) {
|
|
111
|
+
if (!fact || typeof fact !== 'object') return null;
|
|
112
|
+
const base = text(fact['base']).replace(/^\/+|\/+$/g, '');
|
|
113
|
+
const slugField = text(fact['slugField']);
|
|
114
|
+
if (!RECORD_BASE.test(base) || !slugField || !fields.some((field)=>field.id === slugField)) return null;
|
|
115
|
+
return {
|
|
116
|
+
recordBase: base,
|
|
117
|
+
addressField: slugField
|
|
118
|
+
};
|
|
119
|
+
}
|
|
104
120
|
/**
|
|
105
121
|
* The org's datasets shared with the site, through the `dataset` index the
|
|
106
122
|
* data plugin publishes (AGL-3080) rather than its collection: their names
|
|
@@ -119,13 +135,21 @@ async function readWindow(query, fields, cap, map) {
|
|
|
119
135
|
});
|
|
120
136
|
return {
|
|
121
137
|
rows: records.map((record)=>{
|
|
138
|
+
var _ref;
|
|
139
|
+
var _fields_find;
|
|
122
140
|
const fields = (Array.isArray(record.facts['fields']) ? record.facts['fields'] : []).map((field)=>({
|
|
123
141
|
id: text(field == null ? void 0 : field['id']),
|
|
124
142
|
name: text(field == null ? void 0 : field['name']) || text(field == null ? void 0 : field['id']),
|
|
125
|
-
image: text(field == null ? void 0 : field['customType']) === 'image'
|
|
143
|
+
image: text(field == null ? void 0 : field['customType']) === 'image',
|
|
144
|
+
// A record page's address, the data plugin's `pageAddress` type (AGL-3727).
|
|
145
|
+
address: text(field == null ? void 0 : field['customType']) === 'pageAddress'
|
|
126
146
|
})).filter((field)=>field.name);
|
|
127
147
|
// A record's photo (AGL-3616), the data plugin's `image` field type.
|
|
128
148
|
const image = fields.find((field)=>field.image && field.id);
|
|
149
|
+
// The record page the site already serves for it (AGL-3747), whose
|
|
150
|
+
// address field is the one its pages are reached by.
|
|
151
|
+
const page = aiInventoryRecordPage(record.facts['recordPage'], fields);
|
|
152
|
+
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;
|
|
129
153
|
return _extends({
|
|
130
154
|
id: record.id,
|
|
131
155
|
name: record.name,
|
|
@@ -133,6 +157,10 @@ async function readWindow(query, fields, cap, map) {
|
|
|
133
157
|
fieldIds: fields.map((field)=>field.id || field.name)
|
|
134
158
|
}, image ? {
|
|
135
159
|
imageField: image.id
|
|
160
|
+
} : {}, address ? {
|
|
161
|
+
addressField: address
|
|
162
|
+
} : {}, page ? {
|
|
163
|
+
recordBase: page.recordBase
|
|
136
164
|
} : {});
|
|
137
165
|
}),
|
|
138
166
|
truncated
|
|
@@ -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 { 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/**\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 }))\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 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 }\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","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","readIndexedDatasets","orgId","hostId","datasets","records","index","list","record","facts","name","image","filter","find","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 { 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 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 }\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","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","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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"}
|
package/src/lib/server.js
CHANGED
|
@@ -35,6 +35,9 @@ import { registerAiBuildCapabilities } from "./jobs/ai-build-capabilities.js";
|
|
|
35
35
|
import { registerAiWorkflowJob } from "./jobs/ai-job-workflow-step.js";
|
|
36
36
|
import { registerAiLogicJob } from "./jobs/ai-job-logic-step.js";
|
|
37
37
|
import { registerAiEditJob } from "./jobs/ai-job-edit-step.js";
|
|
38
|
+
import { registerAiNavigationJob } from "./jobs/ai-job-navigation-step.js";
|
|
39
|
+
import { registerAiDuplicateJob } from "./jobs/ai-job-duplicate-step.js";
|
|
40
|
+
import { registerAiSettingsJob } from "./jobs/ai-job-settings-step.js";
|
|
38
41
|
import { registerAiInsightJob } from "./jobs/ai-job-insight-step.js";
|
|
39
42
|
import { registerAiFigureReaders } from "./insights/ai-figure-readers.js";
|
|
40
43
|
import { registerAiTextGenerator } from "./server/plugin-text-generation.js";
|
|
@@ -124,6 +127,15 @@ export * from "./runtime/ai-runtime.js";
|
|
|
124
127
|
// A change to a page or a layout the site has, from a description, saved
|
|
125
128
|
// where no visitor sees it — the Assist edit rung run as a job (AGL-3616).
|
|
126
129
|
registerAiEditJob();
|
|
130
|
+
// A change to a site's header or footer menu, made by code from a build's
|
|
131
|
+
// item and saved beside the live layout (AGL-3728).
|
|
132
|
+
registerAiNavigationJob();
|
|
133
|
+
// A copy of something a site already has, through core's duplicate — the
|
|
134
|
+
// console's own Duplicate… — as a build's item (AGL-3740).
|
|
135
|
+
registerAiDuplicateJob();
|
|
136
|
+
// A change to a site's settings — its name, search defaults, icons,
|
|
137
|
+
// language, time zone and contact details — saved live (AGL-3748).
|
|
138
|
+
registerAiSettingsJob();
|
|
127
139
|
// Product copy, and a store's products, categories and discounts from a
|
|
128
140
|
// brief, as proposals the commerce plugin's surfaces apply (AGL-2916).
|
|
129
141
|
registerAiProductsJob();
|
package/src/lib/server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/plugins/ai/src/lib/server.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 { registerPluginApiRoute } from '@aglyn/aglyn/server'\nimport { registerAiDeclarations } from './declarations'\nimport { AI_JOBS_BEAT_PATH } from './jobs/ai-jobs-beat'\nimport { registerAiJobPlan } from './jobs/ai-job-plan-step'\nimport { registerAiJobsPause } from './jobs/ai-jobs-pause'\nimport { registerAiJobsNotify } from './jobs/ai-jobs-notify'\nimport { registerAiComponentJob } from './jobs/ai-job-component-step'\nimport { registerAiCrmJob } from './jobs/ai-job-crm-step'\nimport { registerAiLayoutJob } from './jobs/ai-job-layout-step'\nimport { registerAiTemplateJob } from './jobs/ai-job-template-step'\nimport { registerAiExperimentJob } from './jobs/ai-job-experiment-step'\nimport { registerAiFormJob } from './jobs/ai-job-form-step'\nimport { registerAiPageJob } from './jobs/ai-job-page-step'\nimport { registerAiProductsJob } from './jobs/ai-job-products-step'\nimport { registerAiEmailJob } from './jobs/ai-job-email-step'\nimport { registerAiCampaignJob } from './jobs/ai-job-campaign-step'\nimport { registerAiSiteJob } from './jobs/ai-job-site-step'\nimport { registerAiBuildJob } from './jobs/ai-job-build-step'\nimport { registerAiBuildCapabilities } from './jobs/ai-build-capabilities'\nimport { registerAiWorkflowJob } from './jobs/ai-job-workflow-step'\nimport { registerAiLogicJob } from './jobs/ai-job-logic-step'\nimport { registerAiEditJob } from './jobs/ai-job-edit-step'\nimport { registerAiInsightJob } from './jobs/ai-job-insight-step'\nimport { registerAiFigureReaders } from './insights/ai-figure-readers'\nimport { registerAiTextGenerator } from './server/plugin-text-generation'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin/server/firebase-admin'\nimport { ensureFirstPartyAiProviders } from './providers/registry'\nimport { aiAssistHandler } from './server/ai-assist'\nimport { GET as aiCrmAnswer } from './server/ai-crm-answer'\nimport { POST as runAiJobsBeat } from './server/ai-jobs-beat-route'\nimport { POST as cancelAiJob } from './server/ai-jobs-cancel'\nimport { GET as aiJobEvents } from './server/ai-jobs-events-route'\nimport { POST as createAiSiteBatch } from './server/ai-jobs-batch'\nimport { GET as listAiJobs, POST as createAiJob } from './server/ai-jobs-route'\nimport { POST as resumeAiJob } from './server/ai-jobs-resume'\nimport { POST as aiGenerateComponent } from './server/ai-generate-component'\nimport { POST as createAiMediaImages } from './server/ai-media-image'\nimport { POST as applyAiSeoAudit } from './server/ai-seo-apply'\nimport { POST as createAiExperimentVersions } from './server/ai-experiment-versions'\nimport { POST as assistChat } from './server/assist-chat'\nimport { POST as assistEditApplied } from './server/assist-edit-applied'\nimport { POST as assistFeedback } from './server/assist-feedback'\nimport { PATCH as aiHostPermissions } from './server/ai-host-permissions'\nimport { GET as aiAdminOrg } from './server/ai-admin-org'\nimport { GET as aiAdminConversations } from './server/ai-admin-conversations'\nimport { GET as aiAdminOrgsSpend } from './server/ai-admin-orgs-spend'\nimport { GET as aiAdminSignals } from './server/ai-admin-signals'\nimport { GET as aiAdminUser } from './server/ai-admin-user'\nimport { POST as aiAdminOverage } from './server/ai-admin-overage'\nimport { POST as aiAdminCredits } from './server/ai-admin-credits'\nimport { GET as aiUsage } from './server/ai-usage'\nimport { GET as aiAllotments } from './server/ai-allotments'\nimport { GET as aiInsightAnswer } from './server/ai-insight-answer'\nimport { POST as runAiInsightsDigest } from './server/ai-insight-digest-route'\nimport { AI_INSIGHTS_DIGEST_PATH } from './insights/ai-insight-digest'\nimport { GET as aiModels } from './server/ai-models'\nimport { GET as billingCredits } from './server/billing-credits'\nimport { POST as billingOverage } from './server/billing-overage'\n\nexport * from './providers/contract'\nexport * from './providers/catalog'\nexport * from './providers/registry'\nexport * from './providers/routing'\nexport * from './runtime/ai-runtime'\n\n/**\n * The two first-party adapters, registered once per process. A marketplace\n * plugin registers its own against `AI_PROVIDER_CONTRACT` from its own\n * entry and never touches this.\n */\nconst registerFirstPartyProviders = ensureFirstPartyAiProviders\n\n/**\n * Every job kind this plugin runs beyond the three the machine registers\n * itself (AGL-3025). Each is a CALL, never an import made for what the\n * module does as it loads: this package declares only this file\n * effect-ful, so a bundler honoring `sideEffects` deletes an import whose\n * exports go unused — and a step registered that way ran in every spec and\n * in neither app. `registrations-survive-a-bundler.spec.ts` holds this.\n *\n * The console surface is the only caller (AGL-3026): its doors run a job's\n * first step inline, and its beat runs every step after it. The tenant app\n * loads no server surface of this plugin at all, so nothing that serves a\n * published site can reach a provider.\n */\nfunction registerAiJobKinds(): void {\n // The workspace AI pause the machine asks before it claims any step, so a\n // paused workspace's queued jobs spend nothing (AGL-3037).\n registerAiJobsPause()\n // The person who started a job is told when it needs them, finishes or\n // stops (AGL-3593), from the write that changed it.\n registerAiJobsNotify()\n // The plan step every planned kind runs first (AGL-2935).\n registerAiJobPlan()\n // Components, layouts and templates (AGL-2908, AGL-2909).\n registerAiComponentJob()\n registerAiLayoutJob()\n registerAiTemplateJob()\n // Forms (AGL-2913).\n registerAiExperimentJob()\n registerAiFormJob()\n // Pages, with the least time one pass needs (AGL-2907).\n registerAiPageJob()\n // Email designs and campaigns, whose drafts the email and marketing\n // plugins write on the resource-draft seam (AGL-2912).\n registerAiEmailJob()\n registerAiCampaignJob()\n // Automations drafted from a description, and explained, whose drafts the\n // workflows plugin writes on the resource-draft seam (AGL-2919).\n registerAiWorkflowJob()\n // Site functions and variables proposed from a description, and functions\n // explained, as proposals the logic editor opens unsaved (AGL-3603).\n registerAiLogicJob()\n // A change to a page or a layout the site has, from a description, saved\n // where no visitor sees it — the Assist edit rung run as a job (AGL-3616).\n registerAiEditJob()\n // Product copy, and a store's products, categories and discounts from a\n // brief, as proposals the commerce plugin's surfaces apply (AGL-2916).\n registerAiProductsJob()\n // The site scaffold, which builds a whole site through the steps above\n // (AGL-2911).\n registerAiSiteJob()\n // A build from one request (AGL-3616): pages, creations and other\n // plugins' drafts, through the steps above and the writers those plugins\n // register, settled item by item. What this plugin lets a build make is\n // registered on the core's capability contract like any plugin's.\n registerAiBuildCapabilities()\n registerAiBuildJob()\n // Insights (AGL-2915): a question about a site's figures, answered from\n // tables the figure readers return, and the weekly digest. The readers for\n // the platform's own records — page views, forms, datasets — are this\n // plugin's; every other plugin registers its own from its console surface.\n registerAiFigureReaders(() => firebaseAdmin.app().firestore())\n registerAiInsightJob()\n // CRM by AI (AGL-2917): a record's summary and next step, an email draft\n // and an import's column matches, read through the CRM's facts readers.\n registerAiCrmJob()\n}\n\n/**\n * The console-side API (AGL-2939): every AI door, under the `ai` and\n * `assist` prefixes the plugin owns. The URLs the panel and the besigner\n * call are unchanged — `/api/assist/chat`, `/api/ai/assist`,\n * `/api/ai/jobs` — because the dispatcher serves them from the registry\n * exactly where the named routes used to; the two billing doors and the\n * per-member usage read (`ai/usage`) live under the plugin's own prefix.\n * The jobs beat is the one path outside them, for the reason\n * `AI_JOBS_BEAT_PATH` gives.\n */\nexport function registerAiConsoleApi(): void {\n registerAiDeclarations()\n registerFirstPartyProviders()\n registerAiJobKinds()\n // This plugin as the workspace's text generator on the core's seam\n // (AGL-3324): another plugin drafts a paragraph through it under the\n // generation doors' own rules, metered as a text job is.\n registerAiTextGenerator()\n registerPluginApiRoute('assist/chat', { web: assistChat })\n registerPluginApiRoute('assist/feedback', { web: assistFeedback })\n // The applied-edit record (AGL-2906): counts of a proposal the author\n // applied in their editor, written to the site's activity log.\n registerPluginApiRoute('assist/edit-applied', { web: assistEditApplied })\n // A collaborator's AI toggles on one site (AGL-2927), set from the site's\n // collaborators card through this plugin's column there.\n registerPluginApiRoute('ai/host-permissions', { web: aiHostPermissions })\n registerPluginApiRoute('ai/assist', aiAssistHandler)\n registerPluginApiRoute('ai/jobs', {\n web: (request) => (request.method === 'GET' ? listAiJobs(request) : createAiJob(request)),\n })\n // The agency batch (AGL-2911): one brief, one `site` job per named site,\n // under one batch id. Before the `:jobId` routes, which it is not one of.\n registerPluginApiRoute('ai/jobs/batch', { web: createAiSiteBatch })\n registerPluginApiRoute('ai/jobs/:jobId/cancel', {\n web: (request, context) =>\n cancelAiJob(request, { params: Promise.resolve({ jobId: String(context.params['jobId']) }) }),\n })\n registerPluginApiRoute('ai/jobs/:jobId/resume', {\n web: (request, context) =>\n resumeAiJob(request, { params: Promise.resolve({ jobId: String(context.params['jobId']) }) }),\n })\n // An insight job's answer (AGL-2915), which is kept off the job document\n // and served only to the member who asked, or for a digest to the site's\n // members.\n registerPluginApiRoute('ai/insights/:jobId', {\n web: (request, context) =>\n aiInsightAnswer(request, { params: Promise.resolve({ jobId: String(context.params['jobId']) }) }),\n })\n // A CRM job's answer (AGL-2917), which is kept off the job document and\n // served only to a member the CRM still lets read the record.\n registerPluginApiRoute('ai/crm/:jobId', {\n web: (request, context) =>\n aiCrmAnswer(request, { params: Promise.resolve({ jobId: String(context.params['jobId']) }) }),\n })\n registerPluginApiRoute('ai/jobs/:jobId/events', {\n web: (request, context) =>\n aiJobEvents(request, { params: Promise.resolve({ jobId: String(context.params['jobId']) }) }),\n })\n // The jobs beat (AGL-3026): every step the doors leave queued, run every\n // minute by the console's scheduler on the cron secret, on the one surface\n // that holds the provider's key.\n registerPluginApiRoute(AI_JOBS_BEAT_PATH, { web: runAiJobsBeat })\n // The weekly insights (AGL-2915): Monday's digests made for the people who\n // asked for them, and delivered once the beat has written them. Beside the\n // beat under `/api/admin/`, where the console's scheduled sweeps live.\n // A machine's route: Cloud Scheduler calls it on the cron secret with no\n // site and no member, and the route checks that secret and the AI release\n // flag itself. Unmarked, it was counted in the dispatcher's per-address\n // write bucket and refused with a 429 at 14:00Z on 2026-10-09.\n registerPluginApiRoute(AI_INSIGHTS_DIGEST_PATH, { web: runAiInsightsDigest }, { machine: true })\n // A site SEO audit's \"Apply all\" (AGL-2910): content fixes as new\n // unpublished versions, listing values staged for their SEO cards.\n registerPluginApiRoute('ai/seo/apply', { web: applyAiSeoAudit })\n // A page's or a section's A/B variants as draft versions (AGL-3603): the\n // published version copied once per proposed variant, the variant's copy\n // put in, stored unpublished for the experiment editor to pin.\n registerPluginApiRoute('ai/experiments/versions', { web: createAiExperimentVersions })\n // Save the selection as a reusable component, with AI (AGL-2908): the\n // second entry point of the component job, from the besigner's Attributes\n // panel. It proposes, and the person's Apply writes.\n registerPluginApiRoute('ai/generate/component', { web: aiGenerateComponent })\n // Pictures from a description, in Media (AGL-3602): photos and SVG\n // illustrations, stored through the media upload route. POST only — the\n // button draws from the shell's gates and asks nothing until it is used.\n registerPluginApiRoute('ai/media/images', { web: createAiMediaImages })\n registerPluginApiRoute('ai/billing/credits', { web: billingCredits })\n registerPluginApiRoute('ai/billing/overage', { web: billingOverage })\n registerPluginApiRoute('ai/usage', { web: aiUsage })\n // The allotments a manager sets, and the options the model switch lists\n // (AGL-2942). One handler answers the allotments' GET and POST.\n registerPluginApiRoute('ai/allotments', { web: aiAllotments })\n registerPluginApiRoute('ai/models', { web: aiModels })\n // The staff doors (AGL-2928, AGL-2930, AGL-2252): one org's AI in full,\n // one account's usage across its workspaces, and the fleet's Assist\n // signals behind the Assist signal staff page.\n registerPluginApiRoute('ai/admin/org', { web: aiAdminOrg })\n registerPluginApiRoute('ai/admin/orgs-spend', { web: aiAdminOrgsSpend })\n registerPluginApiRoute('ai/admin/user', { web: aiAdminUser })\n registerPluginApiRoute('ai/admin/signals', { web: aiAdminSignals })\n // What an org asked Aglyn AI and what it answered (AGL-3675): one page at a\n // time, read on request, each page an audited access.\n registerPluginApiRoute('ai/admin/conversations', { web: aiAdminConversations })\n // One workspace's overage standing and the four acts staff have over it\n // (AGL-3011): the ceiling override, lifting a pause — the only way a\n // dispute pause comes off — and resetting the step. Every act audited.\n registerPluginApiRoute('ai/admin/overage', { web: aiAdminOverage })\n // Credits given back to a workspace's band or a Free owner's allowance\n // (AGL-3595): compensation for a fault of ours, bounded by what the month\n // used, keyed against a double-click, and audited.\n registerPluginApiRoute('ai/admin/credits', { web: aiAdminCredits })\n}\n\n// Type-only (AGL-3080): the plugin's entitlement keys, declared by module\n// augmentation, for every program that loads this entry point.\nexport type { aiPlanEntitlements } from './plan-entitlements'\n"],"names":["registerPluginApiRoute","registerAiDeclarations","AI_JOBS_BEAT_PATH","registerAiJobPlan","registerAiJobsPause","registerAiJobsNotify","registerAiComponentJob","registerAiCrmJob","registerAiLayoutJob","registerAiTemplateJob","registerAiExperimentJob","registerAiFormJob","registerAiPageJob","registerAiProductsJob","registerAiEmailJob","registerAiCampaignJob","registerAiSiteJob","registerAiBuildJob","registerAiBuildCapabilities","registerAiWorkflowJob","registerAiLogicJob","registerAiEditJob","registerAiInsightJob","registerAiFigureReaders","registerAiTextGenerator","firebaseAdmin","ensureFirstPartyAiProviders","aiAssistHandler","GET","aiCrmAnswer","POST","runAiJobsBeat","cancelAiJob","aiJobEvents","createAiSiteBatch","listAiJobs","createAiJob","resumeAiJob","aiGenerateComponent","createAiMediaImages","applyAiSeoAudit","createAiExperimentVersions","assistChat","assistEditApplied","assistFeedback","PATCH","aiHostPermissions","aiAdminOrg","aiAdminConversations","aiAdminOrgsSpend","aiAdminSignals","aiAdminUser","aiAdminOverage","aiAdminCredits","aiUsage","aiAllotments","aiInsightAnswer","runAiInsightsDigest","AI_INSIGHTS_DIGEST_PATH","aiModels","billingCredits","billingOverage","registerFirstPartyProviders","registerAiJobKinds","app","firestore","registerAiConsoleApi","web","request","method","context","params","Promise","resolve","jobId","String","machine"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/plugins/ai/src/lib/server.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 { registerPluginApiRoute } from '@aglyn/aglyn/server'\nimport { registerAiDeclarations } from './declarations'\nimport { AI_JOBS_BEAT_PATH } from './jobs/ai-jobs-beat'\nimport { registerAiJobPlan } from './jobs/ai-job-plan-step'\nimport { registerAiJobsPause } from './jobs/ai-jobs-pause'\nimport { registerAiJobsNotify } from './jobs/ai-jobs-notify'\nimport { registerAiComponentJob } from './jobs/ai-job-component-step'\nimport { registerAiCrmJob } from './jobs/ai-job-crm-step'\nimport { registerAiLayoutJob } from './jobs/ai-job-layout-step'\nimport { registerAiTemplateJob } from './jobs/ai-job-template-step'\nimport { registerAiExperimentJob } from './jobs/ai-job-experiment-step'\nimport { registerAiFormJob } from './jobs/ai-job-form-step'\nimport { registerAiPageJob } from './jobs/ai-job-page-step'\nimport { registerAiProductsJob } from './jobs/ai-job-products-step'\nimport { registerAiEmailJob } from './jobs/ai-job-email-step'\nimport { registerAiCampaignJob } from './jobs/ai-job-campaign-step'\nimport { registerAiSiteJob } from './jobs/ai-job-site-step'\nimport { registerAiBuildJob } from './jobs/ai-job-build-step'\nimport { registerAiBuildCapabilities } from './jobs/ai-build-capabilities'\nimport { registerAiWorkflowJob } from './jobs/ai-job-workflow-step'\nimport { registerAiLogicJob } from './jobs/ai-job-logic-step'\nimport { registerAiEditJob } from './jobs/ai-job-edit-step'\nimport { registerAiNavigationJob } from './jobs/ai-job-navigation-step'\nimport { registerAiDuplicateJob } from './jobs/ai-job-duplicate-step'\nimport { registerAiSettingsJob } from './jobs/ai-job-settings-step'\nimport { registerAiInsightJob } from './jobs/ai-job-insight-step'\nimport { registerAiFigureReaders } from './insights/ai-figure-readers'\nimport { registerAiTextGenerator } from './server/plugin-text-generation'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin/server/firebase-admin'\nimport { ensureFirstPartyAiProviders } from './providers/registry'\nimport { aiAssistHandler } from './server/ai-assist'\nimport { GET as aiCrmAnswer } from './server/ai-crm-answer'\nimport { POST as runAiJobsBeat } from './server/ai-jobs-beat-route'\nimport { POST as cancelAiJob } from './server/ai-jobs-cancel'\nimport { GET as aiJobEvents } from './server/ai-jobs-events-route'\nimport { POST as createAiSiteBatch } from './server/ai-jobs-batch'\nimport { GET as listAiJobs, POST as createAiJob } from './server/ai-jobs-route'\nimport { POST as resumeAiJob } from './server/ai-jobs-resume'\nimport { POST as aiGenerateComponent } from './server/ai-generate-component'\nimport { POST as createAiMediaImages } from './server/ai-media-image'\nimport { POST as applyAiSeoAudit } from './server/ai-seo-apply'\nimport { POST as createAiExperimentVersions } from './server/ai-experiment-versions'\nimport { POST as assistChat } from './server/assist-chat'\nimport { POST as assistEditApplied } from './server/assist-edit-applied'\nimport { POST as assistFeedback } from './server/assist-feedback'\nimport { PATCH as aiHostPermissions } from './server/ai-host-permissions'\nimport { GET as aiAdminOrg } from './server/ai-admin-org'\nimport { GET as aiAdminConversations } from './server/ai-admin-conversations'\nimport { GET as aiAdminOrgsSpend } from './server/ai-admin-orgs-spend'\nimport { GET as aiAdminSignals } from './server/ai-admin-signals'\nimport { GET as aiAdminUser } from './server/ai-admin-user'\nimport { POST as aiAdminOverage } from './server/ai-admin-overage'\nimport { POST as aiAdminCredits } from './server/ai-admin-credits'\nimport { GET as aiUsage } from './server/ai-usage'\nimport { GET as aiAllotments } from './server/ai-allotments'\nimport { GET as aiInsightAnswer } from './server/ai-insight-answer'\nimport { POST as runAiInsightsDigest } from './server/ai-insight-digest-route'\nimport { AI_INSIGHTS_DIGEST_PATH } from './insights/ai-insight-digest'\nimport { GET as aiModels } from './server/ai-models'\nimport { GET as billingCredits } from './server/billing-credits'\nimport { POST as billingOverage } from './server/billing-overage'\n\nexport * from './providers/contract'\nexport * from './providers/catalog'\nexport * from './providers/registry'\nexport * from './providers/routing'\nexport * from './runtime/ai-runtime'\n\n/**\n * The two first-party adapters, registered once per process. A marketplace\n * plugin registers its own against `AI_PROVIDER_CONTRACT` from its own\n * entry and never touches this.\n */\nconst registerFirstPartyProviders = ensureFirstPartyAiProviders\n\n/**\n * Every job kind this plugin runs beyond the three the machine registers\n * itself (AGL-3025). Each is a CALL, never an import made for what the\n * module does as it loads: this package declares only this file\n * effect-ful, so a bundler honoring `sideEffects` deletes an import whose\n * exports go unused — and a step registered that way ran in every spec and\n * in neither app. `registrations-survive-a-bundler.spec.ts` holds this.\n *\n * The console surface is the only caller (AGL-3026): its doors run a job's\n * first step inline, and its beat runs every step after it. The tenant app\n * loads no server surface of this plugin at all, so nothing that serves a\n * published site can reach a provider.\n */\nfunction registerAiJobKinds(): void {\n // The workspace AI pause the machine asks before it claims any step, so a\n // paused workspace's queued jobs spend nothing (AGL-3037).\n registerAiJobsPause()\n // The person who started a job is told when it needs them, finishes or\n // stops (AGL-3593), from the write that changed it.\n registerAiJobsNotify()\n // The plan step every planned kind runs first (AGL-2935).\n registerAiJobPlan()\n // Components, layouts and templates (AGL-2908, AGL-2909).\n registerAiComponentJob()\n registerAiLayoutJob()\n registerAiTemplateJob()\n // Forms (AGL-2913).\n registerAiExperimentJob()\n registerAiFormJob()\n // Pages, with the least time one pass needs (AGL-2907).\n registerAiPageJob()\n // Email designs and campaigns, whose drafts the email and marketing\n // plugins write on the resource-draft seam (AGL-2912).\n registerAiEmailJob()\n registerAiCampaignJob()\n // Automations drafted from a description, and explained, whose drafts the\n // workflows plugin writes on the resource-draft seam (AGL-2919).\n registerAiWorkflowJob()\n // Site functions and variables proposed from a description, and functions\n // explained, as proposals the logic editor opens unsaved (AGL-3603).\n registerAiLogicJob()\n // A change to a page or a layout the site has, from a description, saved\n // where no visitor sees it — the Assist edit rung run as a job (AGL-3616).\n registerAiEditJob()\n // A change to a site's header or footer menu, made by code from a build's\n // item and saved beside the live layout (AGL-3728).\n registerAiNavigationJob()\n // A copy of something a site already has, through core's duplicate — the\n // console's own Duplicate… — as a build's item (AGL-3740).\n registerAiDuplicateJob()\n // A change to a site's settings — its name, search defaults, icons,\n // language, time zone and contact details — saved live (AGL-3748).\n registerAiSettingsJob()\n // Product copy, and a store's products, categories and discounts from a\n // brief, as proposals the commerce plugin's surfaces apply (AGL-2916).\n registerAiProductsJob()\n // The site scaffold, which builds a whole site through the steps above\n // (AGL-2911).\n registerAiSiteJob()\n // A build from one request (AGL-3616): pages, creations and other\n // plugins' drafts, through the steps above and the writers those plugins\n // register, settled item by item. What this plugin lets a build make is\n // registered on the core's capability contract like any plugin's.\n registerAiBuildCapabilities()\n registerAiBuildJob()\n // Insights (AGL-2915): a question about a site's figures, answered from\n // tables the figure readers return, and the weekly digest. The readers for\n // the platform's own records — page views, forms, datasets — are this\n // plugin's; every other plugin registers its own from its console surface.\n registerAiFigureReaders(() => firebaseAdmin.app().firestore())\n registerAiInsightJob()\n // CRM by AI (AGL-2917): a record's summary and next step, an email draft\n // and an import's column matches, read through the CRM's facts readers.\n registerAiCrmJob()\n}\n\n/**\n * The console-side API (AGL-2939): every AI door, under the `ai` and\n * `assist` prefixes the plugin owns. The URLs the panel and the besigner\n * call are unchanged — `/api/assist/chat`, `/api/ai/assist`,\n * `/api/ai/jobs` — because the dispatcher serves them from the registry\n * exactly where the named routes used to; the two billing doors and the\n * per-member usage read (`ai/usage`) live under the plugin's own prefix.\n * The jobs beat is the one path outside them, for the reason\n * `AI_JOBS_BEAT_PATH` gives.\n */\nexport function registerAiConsoleApi(): void {\n registerAiDeclarations()\n registerFirstPartyProviders()\n registerAiJobKinds()\n // This plugin as the workspace's text generator on the core's seam\n // (AGL-3324): another plugin drafts a paragraph through it under the\n // generation doors' own rules, metered as a text job is.\n registerAiTextGenerator()\n registerPluginApiRoute('assist/chat', { web: assistChat })\n registerPluginApiRoute('assist/feedback', { web: assistFeedback })\n // The applied-edit record (AGL-2906): counts of a proposal the author\n // applied in their editor, written to the site's activity log.\n registerPluginApiRoute('assist/edit-applied', { web: assistEditApplied })\n // A collaborator's AI toggles on one site (AGL-2927), set from the site's\n // collaborators card through this plugin's column there.\n registerPluginApiRoute('ai/host-permissions', { web: aiHostPermissions })\n registerPluginApiRoute('ai/assist', aiAssistHandler)\n registerPluginApiRoute('ai/jobs', {\n web: (request) => (request.method === 'GET' ? listAiJobs(request) : createAiJob(request)),\n })\n // The agency batch (AGL-2911): one brief, one `site` job per named site,\n // under one batch id. Before the `:jobId` routes, which it is not one of.\n registerPluginApiRoute('ai/jobs/batch', { web: createAiSiteBatch })\n registerPluginApiRoute('ai/jobs/:jobId/cancel', {\n web: (request, context) =>\n cancelAiJob(request, { params: Promise.resolve({ jobId: String(context.params['jobId']) }) }),\n })\n registerPluginApiRoute('ai/jobs/:jobId/resume', {\n web: (request, context) =>\n resumeAiJob(request, { params: Promise.resolve({ jobId: String(context.params['jobId']) }) }),\n })\n // An insight job's answer (AGL-2915), which is kept off the job document\n // and served only to the member who asked, or for a digest to the site's\n // members.\n registerPluginApiRoute('ai/insights/:jobId', {\n web: (request, context) =>\n aiInsightAnswer(request, { params: Promise.resolve({ jobId: String(context.params['jobId']) }) }),\n })\n // A CRM job's answer (AGL-2917), which is kept off the job document and\n // served only to a member the CRM still lets read the record.\n registerPluginApiRoute('ai/crm/:jobId', {\n web: (request, context) =>\n aiCrmAnswer(request, { params: Promise.resolve({ jobId: String(context.params['jobId']) }) }),\n })\n registerPluginApiRoute('ai/jobs/:jobId/events', {\n web: (request, context) =>\n aiJobEvents(request, { params: Promise.resolve({ jobId: String(context.params['jobId']) }) }),\n })\n // The jobs beat (AGL-3026): every step the doors leave queued, run every\n // minute by the console's scheduler on the cron secret, on the one surface\n // that holds the provider's key.\n registerPluginApiRoute(AI_JOBS_BEAT_PATH, { web: runAiJobsBeat })\n // The weekly insights (AGL-2915): Monday's digests made for the people who\n // asked for them, and delivered once the beat has written them. Beside the\n // beat under `/api/admin/`, where the console's scheduled sweeps live.\n // A machine's route: Cloud Scheduler calls it on the cron secret with no\n // site and no member, and the route checks that secret and the AI release\n // flag itself. Unmarked, it was counted in the dispatcher's per-address\n // write bucket and refused with a 429 at 14:00Z on 2026-10-09.\n registerPluginApiRoute(AI_INSIGHTS_DIGEST_PATH, { web: runAiInsightsDigest }, { machine: true })\n // A site SEO audit's \"Apply all\" (AGL-2910): content fixes as new\n // unpublished versions, listing values staged for their SEO cards.\n registerPluginApiRoute('ai/seo/apply', { web: applyAiSeoAudit })\n // A page's or a section's A/B variants as draft versions (AGL-3603): the\n // published version copied once per proposed variant, the variant's copy\n // put in, stored unpublished for the experiment editor to pin.\n registerPluginApiRoute('ai/experiments/versions', { web: createAiExperimentVersions })\n // Save the selection as a reusable component, with AI (AGL-2908): the\n // second entry point of the component job, from the besigner's Attributes\n // panel. It proposes, and the person's Apply writes.\n registerPluginApiRoute('ai/generate/component', { web: aiGenerateComponent })\n // Pictures from a description, in Media (AGL-3602): photos and SVG\n // illustrations, stored through the media upload route. POST only — the\n // button draws from the shell's gates and asks nothing until it is used.\n registerPluginApiRoute('ai/media/images', { web: createAiMediaImages })\n registerPluginApiRoute('ai/billing/credits', { web: billingCredits })\n registerPluginApiRoute('ai/billing/overage', { web: billingOverage })\n registerPluginApiRoute('ai/usage', { web: aiUsage })\n // The allotments a manager sets, and the options the model switch lists\n // (AGL-2942). One handler answers the allotments' GET and POST.\n registerPluginApiRoute('ai/allotments', { web: aiAllotments })\n registerPluginApiRoute('ai/models', { web: aiModels })\n // The staff doors (AGL-2928, AGL-2930, AGL-2252): one org's AI in full,\n // one account's usage across its workspaces, and the fleet's Assist\n // signals behind the Assist signal staff page.\n registerPluginApiRoute('ai/admin/org', { web: aiAdminOrg })\n registerPluginApiRoute('ai/admin/orgs-spend', { web: aiAdminOrgsSpend })\n registerPluginApiRoute('ai/admin/user', { web: aiAdminUser })\n registerPluginApiRoute('ai/admin/signals', { web: aiAdminSignals })\n // What an org asked Aglyn AI and what it answered (AGL-3675): one page at a\n // time, read on request, each page an audited access.\n registerPluginApiRoute('ai/admin/conversations', { web: aiAdminConversations })\n // One workspace's overage standing and the four acts staff have over it\n // (AGL-3011): the ceiling override, lifting a pause — the only way a\n // dispute pause comes off — and resetting the step. Every act audited.\n registerPluginApiRoute('ai/admin/overage', { web: aiAdminOverage })\n // Credits given back to a workspace's band or a Free owner's allowance\n // (AGL-3595): compensation for a fault of ours, bounded by what the month\n // used, keyed against a double-click, and audited.\n registerPluginApiRoute('ai/admin/credits', { web: aiAdminCredits })\n}\n\n// Type-only (AGL-3080): the plugin's entitlement keys, declared by module\n// augmentation, for every program that loads this entry point.\nexport type { aiPlanEntitlements } from './plan-entitlements'\n"],"names":["registerPluginApiRoute","registerAiDeclarations","AI_JOBS_BEAT_PATH","registerAiJobPlan","registerAiJobsPause","registerAiJobsNotify","registerAiComponentJob","registerAiCrmJob","registerAiLayoutJob","registerAiTemplateJob","registerAiExperimentJob","registerAiFormJob","registerAiPageJob","registerAiProductsJob","registerAiEmailJob","registerAiCampaignJob","registerAiSiteJob","registerAiBuildJob","registerAiBuildCapabilities","registerAiWorkflowJob","registerAiLogicJob","registerAiEditJob","registerAiNavigationJob","registerAiDuplicateJob","registerAiSettingsJob","registerAiInsightJob","registerAiFigureReaders","registerAiTextGenerator","firebaseAdmin","ensureFirstPartyAiProviders","aiAssistHandler","GET","aiCrmAnswer","POST","runAiJobsBeat","cancelAiJob","aiJobEvents","createAiSiteBatch","listAiJobs","createAiJob","resumeAiJob","aiGenerateComponent","createAiMediaImages","applyAiSeoAudit","createAiExperimentVersions","assistChat","assistEditApplied","assistFeedback","PATCH","aiHostPermissions","aiAdminOrg","aiAdminConversations","aiAdminOrgsSpend","aiAdminSignals","aiAdminUser","aiAdminOverage","aiAdminCredits","aiUsage","aiAllotments","aiInsightAnswer","runAiInsightsDigest","AI_INSIGHTS_DIGEST_PATH","aiModels","billingCredits","billingOverage","registerFirstPartyProviders","registerAiJobKinds","app","firestore","registerAiConsoleApi","web","request","method","context","params","Promise","resolve","jobId","String","machine"],"mappings":"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"}
|
|
@@ -25,7 +25,10 @@ import { unregisterPluginServices } from "@aglyn/aglyn/plugin-manager/plugin-ser
|
|
|
25
25
|
* datasets only, narrowed to a site to those shared with it, each with its
|
|
26
26
|
* fields (`{ id, name, type }` and a `customType` where one rides it, in
|
|
27
27
|
* the dataset page's order) and its
|
|
28
|
-
* `visibleTo
|
|
28
|
+
* `visibleTo`; narrowed to a site, the site's record page for it as
|
|
29
|
+
* `recordPage` (`{ screenId, base, slugField }`, or `null`, AGL-3747) read off
|
|
30
|
+
* `hosts/{hostId}/recordPages`. What the data plugin itself answers is held in
|
|
31
|
+
* its own spec.
|
|
29
32
|
*/ const OWNER = 'data';
|
|
30
33
|
const str = (value)=>typeof value === 'string' ? value.trim() : '';
|
|
31
34
|
/**
|
|
@@ -81,6 +84,34 @@ function record(id, data) {
|
|
|
81
84
|
/** Registers it, reading through `firestore`. */ export function standInDatasetIndex(firestore) {
|
|
82
85
|
const datasets = (orgId)=>firestore.collection('orgs').doc(orgId).collection('datasets');
|
|
83
86
|
const shared = (one, hostId)=>one && (!hostId || visibleToHost(one.facts['visibleTo'], hostId)) ? one : null;
|
|
87
|
+
const recordPages = async (hostId)=>{
|
|
88
|
+
const snapshot = await firestore.collection('hosts').doc(hostId).collection('recordPages').get();
|
|
89
|
+
const pages = new Map();
|
|
90
|
+
for (const doc of snapshot.docs){
|
|
91
|
+
var _doc_data;
|
|
92
|
+
const data = (_doc_data = doc.data()) != null ? _doc_data : {};
|
|
93
|
+
const base = str(data['base']).replace(/^\/+|\/+$/g, '');
|
|
94
|
+
if (!str(data['datasetId']) || !base || !str(data['slugField']) || pages.has(str(data['datasetId']))) continue;
|
|
95
|
+
pages.set(str(data['datasetId']), {
|
|
96
|
+
screenId: doc.id,
|
|
97
|
+
base,
|
|
98
|
+
slugField: str(data['slugField'])
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
return pages;
|
|
102
|
+
};
|
|
103
|
+
const withPages = async (records, hostId)=>{
|
|
104
|
+
if (!hostId || !records.length) return records;
|
|
105
|
+
const pages = await recordPages(hostId);
|
|
106
|
+
return records.map((one)=>{
|
|
107
|
+
var _pages_get;
|
|
108
|
+
return _extends({}, one, {
|
|
109
|
+
facts: _extends({}, one.facts, {
|
|
110
|
+
recordPage: (_pages_get = pages.get(one.id)) != null ? _pages_get : null
|
|
111
|
+
})
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
};
|
|
84
115
|
registerPluginRecordIndex('dataset', {
|
|
85
116
|
async list ({ orgId, hostId, limit }) {
|
|
86
117
|
if (!orgId) return {
|
|
@@ -90,14 +121,17 @@ function record(id, data) {
|
|
|
90
121
|
const snapshot = await datasets(orgId).get();
|
|
91
122
|
const records = snapshot.docs.map((doc)=>shared(record(doc.id, doc.data()), hostId)).filter((one)=>one !== null);
|
|
92
123
|
return {
|
|
93
|
-
records: records.slice(0, limit),
|
|
124
|
+
records: await withPages(records.slice(0, limit), hostId),
|
|
94
125
|
truncated: records.length > limit
|
|
95
126
|
};
|
|
96
127
|
},
|
|
97
128
|
async get ({ orgId, hostId, id }) {
|
|
98
129
|
if (!orgId) return null;
|
|
99
130
|
const snapshot = await datasets(orgId).doc(id).get();
|
|
100
|
-
|
|
131
|
+
const one = snapshot.exists ? shared(record(snapshot.id, snapshot.data()), hostId) : null;
|
|
132
|
+
return one ? (await withPages([
|
|
133
|
+
one
|
|
134
|
+
], hostId))[0] : null;
|
|
101
135
|
}
|
|
102
136
|
}, {
|
|
103
137
|
pluginId: OWNER
|