@aglyn/plugins-ai 1.0.0-beta.291 → 1.0.0-beta.293
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +17 -17
- package/src/lib/components/ai-site-import-job.component.js +5 -4
- package/src/lib/components/ai-site-import-job.component.js.map +1 -1
- package/src/lib/components/ai-site-import-start.component.js +3 -3
- package/src/lib/components/ai-site-import-start.component.js.map +1 -1
- package/src/lib/components/ai-site-start-card.component.d.ts +1 -1
- package/src/lib/components/ai-site-start-card.component.js +129 -29
- package/src/lib/components/ai-site-start-card.component.js.map +1 -1
- package/src/lib/components/ai-site-start-steps.component.d.ts +15 -3
- package/src/lib/components/ai-site-start-steps.component.js +42 -25
- package/src/lib/components/ai-site-start-steps.component.js.map +1 -1
- package/src/lib/components/ai-site-style-previews.d.ts +17 -0
- package/src/lib/components/ai-site-style-previews.js +26 -95
- package/src/lib/components/ai-site-style-previews.js.map +1 -1
- package/src/lib/declarations.server.js +22 -0
- package/src/lib/declarations.server.js.map +1 -1
- package/src/lib/jobs/ai-job-import-step.d.ts +21 -2
- package/src/lib/jobs/ai-job-import-step.js +179 -49
- package/src/lib/jobs/ai-job-import-step.js.map +1 -1
- package/src/lib/jobs/ai-job-layout-language.d.ts +27 -3
- package/src/lib/jobs/ai-job-layout-language.js +156 -113
- package/src/lib/jobs/ai-job-layout-language.js.map +1 -1
- package/src/lib/jobs/ai-job-layout-step.js +56 -17
- package/src/lib/jobs/ai-job-layout-step.js.map +1 -1
- package/src/lib/jobs/ai-job-page-language.d.ts +25 -15
- package/src/lib/jobs/ai-job-page-language.js +120 -66
- package/src/lib/jobs/ai-job-page-language.js.map +1 -1
- package/src/lib/jobs/ai-job-page-step.js +27 -7
- package/src/lib/jobs/ai-job-page-step.js.map +1 -1
- package/src/lib/jobs/ai-job-site-datasets.d.ts +12 -4
- package/src/lib/jobs/ai-job-site-datasets.js +36 -16
- package/src/lib/jobs/ai-job-site-datasets.js.map +1 -1
- package/src/lib/jobs/ai-jobs.js +6 -2
- package/src/lib/jobs/ai-jobs.js.map +1 -1
- package/src/lib/jobs/ai-layout-stock-photos.d.ts +50 -1
- package/src/lib/jobs/ai-layout-stock-photos.js +586 -35
- package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
- package/src/lib/jobs/ai-site-import-compile.d.ts +147 -0
- package/src/lib/jobs/ai-site-import-compile.js +1143 -0
- package/src/lib/jobs/ai-site-import-compile.js.map +1 -0
- package/src/lib/jobs/ai-site-import-document.d.ts +9 -36
- package/src/lib/jobs/ai-site-import-document.js +23 -489
- package/src/lib/jobs/ai-site-import-document.js.map +1 -1
- package/src/lib/jobs/ai-site-import-look.d.ts +54 -7
- package/src/lib/jobs/ai-site-import-look.js +163 -52
- package/src/lib/jobs/ai-site-import-look.js.map +1 -1
- package/src/lib/jobs/ai-site-import-publish.d.ts +7 -1
- package/src/lib/jobs/ai-site-import-publish.js +19 -5
- package/src/lib/jobs/ai-site-import-publish.js.map +1 -1
- package/src/lib/jobs/ai-site-late-photos.js +4 -2
- package/src/lib/jobs/ai-site-late-photos.js.map +1 -1
- package/src/lib/jobs/ai-site-publish.d.ts +10 -1
- package/src/lib/jobs/ai-site-publish.js +8 -3
- package/src/lib/jobs/ai-site-publish.js.map +1 -1
- package/src/lib/jobs/fixtures/ai-stock-photo-agl4092-1029-recordings.d.ts +23 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-agl4092-1029-recordings.js +677 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-agl4092-1029-recordings.js.map +1 -0
- package/src/lib/layout-language/ai-layout-compiler.d.ts +26 -0
- package/src/lib/layout-language/ai-layout-compiler.js +677 -33
- package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
- package/src/lib/layout-language/ai-layout-frame-hollows.js +5 -1
- package/src/lib/layout-language/ai-layout-frame-hollows.js.map +1 -1
- package/src/lib/layout-language/ai-layout-frame-source.d.ts +196 -0
- package/src/lib/layout-language/ai-layout-frame-source.js +726 -0
- package/src/lib/layout-language/ai-layout-frame-source.js.map +1 -0
- package/src/lib/layout-language/ai-layout-frame.d.ts +22 -0
- package/src/lib/layout-language/ai-layout-frame.js +121 -29
- package/src/lib/layout-language/ai-layout-frame.js.map +1 -1
- package/src/lib/layout-language/ai-layout-language.d.ts +103 -37
- package/src/lib/layout-language/ai-layout-language.js +85 -120
- package/src/lib/layout-language/ai-layout-language.js.map +1 -1
- package/src/lib/model/ai-credit-estimate.d.ts +11 -7
- package/src/lib/model/ai-credit-estimate.js +11 -7
- package/src/lib/model/ai-credit-estimate.js.map +1 -1
- package/src/lib/model/ai-site-import.d.ts +307 -13
- package/src/lib/model/ai-site-import.js +227 -19
- package/src/lib/model/ai-site-import.js.map +1 -1
- package/src/lib/model/ai-site-look.d.ts +65 -1
- package/src/lib/model/ai-site-look.js +141 -23
- package/src/lib/model/ai-site-look.js.map +1 -1
- package/src/lib/model/ai-site-photos.d.ts +2 -0
- package/src/lib/model/ai-site-photos.js.map +1 -1
- package/src/lib/model/ai-site-start-steps.d.ts +4 -3
- package/src/lib/model/ai-site-start-steps.js +11 -3
- package/src/lib/model/ai-site-start-steps.js.map +1 -1
- package/src/lib/model/ai-site-start.d.ts +23 -2
- package/src/lib/model/ai-site-start.js +25 -3
- package/src/lib/model/ai-site-start.js.map +1 -1
- package/src/lib/providers/routing.d.ts +11 -0
- package/src/lib/providers/routing.js +23 -0
- package/src/lib/providers/routing.js.map +1 -1
- package/src/lib/runtime/ai-doctrine-validators.d.ts +4 -2
- package/src/lib/runtime/ai-doctrine-validators.js +7 -2
- package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
- package/src/lib/runtime/ai-icon-library.d.ts +4 -0
- package/src/lib/runtime/ai-icon-library.js +4 -0
- package/src/lib/runtime/ai-icon-library.js.map +1 -1
- package/src/lib/runtime/ai-node-tree.js +5 -1
- package/src/lib/runtime/ai-node-tree.js.map +1 -1
- package/src/lib/runtime/ai-palette.d.ts +2 -0
- package/src/lib/runtime/ai-palette.generated.js +19 -3
- package/src/lib/runtime/ai-palette.generated.js.map +1 -1
- package/src/lib/runtime/ai-palette.js +2 -1
- package/src/lib/runtime/ai-palette.js.map +1 -1
- package/src/lib/site-import/fixtures/site-layout-recordings.d.ts +50 -0
- package/src/lib/site-import/fixtures/site-layout-recordings.js +86 -0
- package/src/lib/site-import/fixtures/site-layout-recordings.js.map +1 -0
- package/src/lib/site-import/site-import-crawl.d.ts +18 -1
- package/src/lib/site-import/site-import-crawl.js +70 -10
- package/src/lib/site-import/site-import-crawl.js.map +1 -1
- package/src/lib/site-import/site-import-css.d.ts +110 -0
- package/src/lib/site-import/site-import-css.js +826 -0
- package/src/lib/site-import/site-import-css.js.map +1 -0
- package/src/lib/site-import/site-import-html.d.ts +116 -1
- package/src/lib/site-import/site-import-html.js +538 -46
- package/src/lib/site-import/site-import-html.js.map +1 -1
- package/src/lib/site-import/site-import-layout.d.ts +79 -0
- package/src/lib/site-import/site-import-layout.js +1608 -0
- package/src/lib/site-import/site-import-layout.js.map +1 -0
- package/src/lib/site-import/site-import-nav.d.ts +40 -0
- package/src/lib/site-import/site-import-nav.js +437 -0
- package/src/lib/site-import/site-import-nav.js.map +1 -0
- package/src/lib/site-import/site-import-page.d.ts +8 -1
- package/src/lib/site-import/site-import-page.js +24 -2
- package/src/lib/site-import/site-import-page.js.map +1 -1
- package/src/lib/assets/site-styles/beauty.webp +0 -0
- package/src/lib/assets/site-styles/blog.webp +0 -0
- package/src/lib/assets/site-styles/business.webp +0 -0
- package/src/lib/assets/site-styles/coming-soon.webp +0 -0
- package/src/lib/assets/site-styles/education.webp +0 -0
- package/src/lib/assets/site-styles/events.webp +0 -0
- package/src/lib/assets/site-styles/fitness.webp +0 -0
- package/src/lib/assets/site-styles/landing.webp +0 -0
- package/src/lib/assets/site-styles/music.webp +0 -0
- package/src/lib/assets/site-styles/nonprofit.webp +0 -0
- package/src/lib/assets/site-styles/personal.webp +0 -0
- package/src/lib/assets/site-styles/photography.webp +0 -0
- package/src/lib/assets/site-styles/portfolio.webp +0 -0
- package/src/lib/assets/site-styles/professional.webp +0 -0
- package/src/lib/assets/site-styles/realestate.webp +0 -0
- package/src/lib/assets/site-styles/restaurant.webp +0 -0
- package/src/lib/assets/site-styles/store.webp +0 -0
- package/src/lib/assets/site-styles/studio.webp +0 -0
- package/src/lib/assets/site-styles/trades.webp +0 -0
- package/src/lib/assets/site-styles/wellness.webp +0 -0
- package/src/lib/assets/site-styles/yoga.webp +0 -0
- package/src/lib/site-import/site-import-review.d.ts +0 -40
- package/src/lib/site-import/site-import-review.js +0 -149
- package/src/lib/site-import/site-import-review.js.map +0 -1
|
@@ -94,7 +94,8 @@
|
|
|
94
94
|
headline: 120,
|
|
95
95
|
body: 2000,
|
|
96
96
|
button: 40,
|
|
97
|
-
/** A short label, title or caption. */ label: 200
|
|
97
|
+
/** A short label, title or caption. */ label: 200,
|
|
98
|
+
/** A table's grid, every cell's words (AGL-4107): a page brought in carries its source's tables whole. */ table: 20000
|
|
98
99
|
};
|
|
99
100
|
/**
|
|
100
101
|
* Prompt budget per surface catalog, in characters at four per token.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/runtime/ai-palette.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { JsonLinealOrder } from '@aglyn/aglyn/app-utils/lineal-order'\n\n/**\n * The element palette as a model sees it (AGL-2905).\n *\n * `ai-palette.generated.ts` is produced from the registered plugin bundles by\n * `libs/plugins/ai/scripts/generate-ai-palette.mts` and holds the data; this module\n * holds the shapes that data is typed against and the small readers over it,\n * so a regeneration never rewrites a type.\n */\n\n/** The document kinds a model is asked to compose. */\nexport type AiSurface = 'screen' | 'email' | 'form' | 'layout' | 'component'\n\nexport const AI_SURFACE_NAMES: readonly AiSurface[] = [\n 'screen',\n 'email',\n 'form',\n 'layout',\n 'component',\n]\n\n/**\n * The subset of JSON Schema a prop declaration is expressed in. Enough to\n * validate what an attribute field can persist; nothing the field editor\n * cannot itself model.\n */\nexport interface AiPropSchema {\n type?: 'string' | 'number' | 'integer' | 'boolean'\n enum?: readonly string[]\n minLength?: number\n maxLength?: number\n minimum?: number\n maximum?: number\n pattern?: string\n description?: string\n}\n\nexport interface AiPropsSchema {\n type: 'object'\n properties: Record<string, AiPropSchema>\n required: readonly string[]\n additionalProperties: false\n}\n\n/**\n * What a string prop MEANS, which decides how the validator checks it.\n *\n * - `text` — copy the visitor reads; held to a length ceiling.\n * - `url` — a navigable address: an `https:` URL or a root-relative path.\n * - `screen` — a screen id, checked against the screens the host has.\n * - `media` — an image or video source: a DAM reference or an `https:` URL.\n */\nexport type AiPropRole = 'text' | 'url' | 'screen' | 'media'\n\n/**\n * A `restrictChildren` / `restrictParent` directive as generated data: the\n * same tuple the schema declares, with the flag spelled as the string the\n * enum holds so a JSON emitter can write it.\n */\nexport type AiLinealOrder = JsonLinealOrder\n\nexport interface AiPaletteEntry {\n pluginId: string\n kind: 'element' | 'plaintext' | 'markdown'\n category: string\n displayName: string\n /** One sentence, from the schema's own description. */\n summary: string\n /** Whether the editor lets anything be dropped inside this element. */\n acceptsChildren: boolean\n restrictChildren?: AiLinealOrder\n restrictParent?: AiLinealOrder\n propsSchema: AiPropsSchema\n /** Roles of the string props the validator treats specially. */\n propRoles: Record<string, AiPropRole>\n /**\n * The attribute field kind each declared prop is edited with, as its\n * `FieldComponentType` value (`switch`, `select`, `screen-select`, …): what\n * the editor reads to decide which component properties a field can be\n * bound to (AGL-2908).\n */\n propFields: Record<string, string>\n /** Length ceilings for `text` props, in characters. */\n textLimits: Record<string, number>\n /** Names of the presets that place this component. */\n presets: readonly string[]\n}\n\n/**\n * What every NODE can do, as against what one component declares (AGL-3156).\n *\n * Generated from the node capabilities core declares, beside the per-component\n * schemas rather than out of them: repeating is not a Stack's feature, so no\n * component schema names it and a palette built from schemas alone advertises\n * it on nothing. Declared ONCE for the whole palette — a model may write these\n * props on any element the surface allows.\n */\nexport interface AiNodeCapability {\n /** Stable id, as core declares it. */\n id: string\n displayName: string\n /** One sentence, for the prompt catalog. */\n summary: string\n propsSchema: AiPropsSchema\n propRoles: Record<string, AiPropRole>\n propFields: Record<string, string>\n textLimits: Record<string, number>\n /** Props offered only on a node that holds a child list. */\n childrenOnlyProps: readonly string[]\n /**\n * Declared attributes the palette deliberately does not offer: a picker\n * that chooses a record the site holds, which a model cannot name from a\n * description. Recorded so a field left off on purpose reads differently\n * from one that went missing.\n */\n omittedProps: readonly string[]\n}\n\n/**\n * The capability props a node with this child contract may carry, merged into\n * one map the prop sanitizer can read beside the element's own declarations.\n */\nexport function nodeCapabilityProps(\n capabilities: Readonly<Record<string, AiNodeCapability>>,\n acceptsChildren: boolean,\n): {\n properties: Record<string, AiPropSchema>\n propRoles: Record<string, AiPropRole>\n textLimits: Record<string, number>\n} {\n const properties: Record<string, AiPropSchema> = {}\n const propRoles: Record<string, AiPropRole> = {}\n const textLimits: Record<string, number> = {}\n for (const capability of Object.values(capabilities)) {\n for (const [name, schema] of Object.entries(\n capability.propsSchema.properties,\n )) {\n if (!acceptsChildren && capability.childrenOnlyProps.includes(name))\n continue\n properties[name] = schema\n const role = capability.propRoles[name]\n if (role) propRoles[name] = role\n const limit = capability.textLimits[name]\n if (limit !== undefined) textLimits[name] = limit\n }\n }\n return { properties, propRoles, textLimits }\n}\n\n/**\n * What a node capability declared in core and one carried by the palette\n * disagree about (AGL-3156).\n *\n * `check:ai-palette` pins the generated file to the generator's output for its\n * inputs, which cannot notice a capability leaving those inputs: when repeat\n * stopped being a component's attribute, the generator regenerated cleanly and\n * the palette simply stopped mentioning it. This reads the core declarations\n * instead and asks whether the palette still accounts for each one, so the\n * next generic capability cannot vanish the same way.\n *\n * `offeredFieldKinds` is every field kind the palette DOES carry somewhere,\n * derived from the generated entries rather than restated here: a capability\n * attribute drawn with one of those kinds has no excuse for being omitted.\n */\nexport function auditNodeCapabilities(\n declared: ReadonlyArray<{\n id: string\n label: string\n attributes: ReadonlyArray<{ name: string; component: string }>\n }>,\n carried: Readonly<Record<string, AiNodeCapability>>,\n offeredFieldKinds: ReadonlySet<string>,\n): string[] {\n const problems: string[] = []\n for (const capability of declared) {\n const entry = carried[capability.id]\n if (!entry) {\n problems.push(\n `the palette carries no node capability \"${capability.id}\": a model cannot use it at all`,\n )\n continue\n }\n if (!Object.keys(entry.propsSchema.properties).length) {\n problems.push(\n `node capability \"${capability.id}\" is carried with no prop a model can write`,\n )\n }\n for (const attribute of capability.attributes) {\n if (entry.propsSchema.properties[attribute.name]) continue\n if (!entry.omittedProps.includes(attribute.name)) {\n problems.push(\n `node capability \"${capability.id}\" declares \"${attribute.name}\", which the palette neither offers nor records as omitted`,\n )\n continue\n }\n if (offeredFieldKinds.has(attribute.component)) {\n problems.push(\n `node capability \"${capability.id}\" omits \"${attribute.name}\", whose \"${attribute.component}\" field the palette offers elsewhere`,\n )\n }\n }\n }\n for (const id of Object.keys(carried)) {\n if (!declared.some((capability) => capability.id === id)) {\n problems.push(\n `the palette carries node capability \"${id}\", which core no longer declares`,\n )\n }\n }\n return problems\n}\n\nexport interface AiSurfaceDefinition {\n /** The component id at the top of a tree for this surface. */\n root: string\n /** Every component id a node in this surface may carry. */\n allow: readonly string[]\n /**\n * Component ids admitted only in a tree the platform's own code composed\n * (AGL-3660), never listed in the surface's catalog: see\n * `AiNodeTreeContext.codeBuilt`.\n */\n codeOnly?: readonly string[]\n}\n\n/**\n * Theme vocabulary an `sx` value may name, read off the theme sources so the\n * validator's allowlist is derived rather than typed by hand.\n */\nexport interface AiSxTokens {\n /** Dotted palette paths such as `primary.main`. */\n palette: readonly string[]\n /** Responsive object keys. */\n breakpoints: readonly string[]\n /** Values `typography` may take. */\n typographyVariants: readonly string[]\n}\n\n/**\n * Text ceilings per role of copy (AGL-2905). A headline is one line of a\n * hero, body copy is a paragraph or two, and a button label has to fit a\n * button.\n */\nexport const AI_TEXT_LIMITS = {\n headline: 120,\n body: 2_000,\n button: 40,\n /** A short label, title or caption. */\n label: 200,\n} as const\n\n/**\n * Prompt budget per surface catalog, in characters at four per token.\n * Asserted by a spec so a component that grows its description cannot\n * silently push a system prompt past what the route pays for.\n */\nexport const AI_PALETTE_CATALOG_MAX_TOKENS = 2_500\nexport const AI_PALETTE_CATALOG_MAX_CHARS = AI_PALETTE_CATALOG_MAX_TOKENS * 4\n\n/** The model's own token estimate: characters over four. */\nexport function estimateCatalogTokens(catalog: string): number {\n return Math.ceil(catalog.length / 4)\n}\n\n/**\n * The documents a generator emits (AGL-2935). Each is composed on one\n * surface of the palette and held to its own budget below.\n */\nexport type AiOutputKind =\n | 'page'\n | 'template'\n | 'component'\n | 'layout'\n | 'form'\n | 'email'\n\nexport const AI_OUTPUT_KINDS: readonly AiOutputKind[] = [\n 'page',\n 'template',\n 'component',\n 'layout',\n 'form',\n 'email',\n]\n\n/** The surface each output kind is composed on. */\nexport const AI_OUTPUT_SURFACE: Record<AiOutputKind, AiSurface> = {\n page: 'screen',\n template: 'screen',\n component: 'component',\n layout: 'layout',\n form: 'form',\n email: 'email',\n}\n\n/**\n * What the rule-17 scorer measures on an emitted tree:\n *\n * - `nodes` — how many elements the tree carries;\n * - `bytes` — the stored node map, in the unit the save ceiling uses\n * (`nodeMapBytes`, msgpack);\n * - `imageBytes` — what the placed library images request at the slot's\n * variant, from each asset's recorded size;\n * - `embeds` — third-party players and frames the output loads;\n * - `fontFamilies` — families named beyond the theme's own;\n * - `emailHtmlBytes` — an email's rendered HTML, which is what a mail\n * client clips.\n */\nexport type AiBudgetMetric =\n | 'nodes'\n | 'bytes'\n | 'imageBytes'\n | 'embeds'\n | 'fontFamilies'\n | 'emailHtmlBytes'\n\nexport type AiOutputBudget = Record<Exclude<AiBudgetMetric, 'emailHtmlBytes'>, number> & {\n emailHtmlBytes?: number\n}\n\n/**\n * The size past which Gmail clips a message and hides the rest behind a\n * \"View entire message\" link — the unsubscribe footer included.\n */\nexport const AI_EMAIL_CLIP_BYTES = 102_000\n\n/**\n * The budget per output kind (AGL-2935, rule 17), beside the catalog budget\n * above for the same reason: a generator that grows its output past what\n * its kind is for is caught by a number, not noticed by a reader.\n *\n * A page is a few hundred elements and tens of kilobytes of stored map; a\n * reusable component is one repeated block; a layout is the chrome every\n * page loads, so it carries the least imagery; a form is fields and a\n * button — each field a Form Control over a Form Label and its answer, a\n * choice a Choice Label over its Radio or Checkbox, so a form of a few dozen\n * questions is a hundred-odd elements; an email is held to the clipping threshold on its rendered HTML.\n * No kind names a font beyond the theme's.\n *\n * A third-party player is a page's or a component's to carry, and only where\n * a confirmed plan lists it (rule 16). A template is rendered for every\n * record it serves and a layout around every page, so neither carries one\n * (AGL-3433): an embed there loads a stranger's code on every page of a\n * collection or a site.\n */\nexport const AI_OUTPUT_BUDGETS: Record<AiOutputKind, AiOutputBudget> = {\n page: {\n nodes: 400,\n bytes: 60_000,\n imageBytes: 1_500_000,\n embeds: 1,\n fontFamilies: 0,\n },\n template: {\n nodes: 400,\n bytes: 60_000,\n imageBytes: 1_500_000,\n embeds: 0,\n fontFamilies: 0,\n },\n component: {\n nodes: 80,\n bytes: 12_000,\n imageBytes: 500_000,\n embeds: 1,\n fontFamilies: 0,\n },\n layout: {\n nodes: 160,\n bytes: 24_000,\n imageBytes: 200_000,\n embeds: 0,\n fontFamilies: 0,\n },\n form: {\n nodes: 150,\n bytes: 15_000,\n imageBytes: 0,\n embeds: 0,\n fontFamilies: 0,\n },\n email: {\n nodes: 150,\n bytes: 24_000,\n imageBytes: 1_000_000,\n embeds: 0,\n fontFamilies: 0,\n emailHtmlBytes: AI_EMAIL_CLIP_BYTES,\n },\n}\n\n/**\n * What a first visit to a generated output is estimated to transfer — the\n * weight the proposal shows before anything is applied (rule 17). A page\n * starts from the platform's measured page weight; a component or a layout\n * reports only what it adds to a page; an email is not a page load.\n */\nexport interface AiLoadEstimate {\n /** The measured weight of a published page before this output's own bytes; 0 for a fragment. */\n pageBytes: number\n /** The stored node map, in the save's own bytes. */\n documentBytes: number\n /** What the placed library images request at their slots. */\n imageBytes: number\n /** Images the estimate could not size: no recorded size, or not from the library. */\n imagesUnmeasured: number\n embeds: number\n totalBytes: number\n}\n\n/** Typography variants that read as headlines, for the `children` ceiling. */\nexport function isHeadlineVariant(variant: unknown): boolean {\n return typeof variant === 'string' && /^(display\\w*|h[1-6]|heading)$/.test(variant)\n}\n"],"names":["AI_SURFACE_NAMES","nodeCapabilityProps","capabilities","acceptsChildren","properties","propRoles","textLimits","capability","Object","values","name","schema","entries","propsSchema","childrenOnlyProps","includes","role","limit","undefined","auditNodeCapabilities","declared","carried","offeredFieldKinds","problems","entry","id","push","keys","length","attribute","attributes","omittedProps","has","component","some","AI_TEXT_LIMITS","headline","body","button","label","AI_PALETTE_CATALOG_MAX_TOKENS","AI_PALETTE_CATALOG_MAX_CHARS","estimateCatalogTokens","catalog","Math","ceil","AI_OUTPUT_KINDS","AI_OUTPUT_SURFACE","page","template","layout","form","email","AI_EMAIL_CLIP_BYTES","AI_OUTPUT_BUDGETS","nodes","bytes","imageBytes","embeds","fontFamilies","emailHtmlBytes","isHeadlineVariant","variant","test"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/runtime/ai-palette.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { JsonLinealOrder } from '@aglyn/aglyn/app-utils/lineal-order'\n\n/**\n * The element palette as a model sees it (AGL-2905).\n *\n * `ai-palette.generated.ts` is produced from the registered plugin bundles by\n * `libs/plugins/ai/scripts/generate-ai-palette.mts` and holds the data; this module\n * holds the shapes that data is typed against and the small readers over it,\n * so a regeneration never rewrites a type.\n */\n\n/** The document kinds a model is asked to compose. */\nexport type AiSurface = 'screen' | 'email' | 'form' | 'layout' | 'component'\n\nexport const AI_SURFACE_NAMES: readonly AiSurface[] = [\n 'screen',\n 'email',\n 'form',\n 'layout',\n 'component',\n]\n\n/**\n * The subset of JSON Schema a prop declaration is expressed in. Enough to\n * validate what an attribute field can persist; nothing the field editor\n * cannot itself model.\n */\nexport interface AiPropSchema {\n type?: 'string' | 'number' | 'integer' | 'boolean'\n enum?: readonly string[]\n minLength?: number\n maxLength?: number\n minimum?: number\n maximum?: number\n pattern?: string\n description?: string\n}\n\nexport interface AiPropsSchema {\n type: 'object'\n properties: Record<string, AiPropSchema>\n required: readonly string[]\n additionalProperties: false\n}\n\n/**\n * What a string prop MEANS, which decides how the validator checks it.\n *\n * - `text` — copy the visitor reads; held to a length ceiling.\n * - `url` — a navigable address: an `https:` URL or a root-relative path.\n * - `screen` — a screen id, checked against the screens the host has.\n * - `media` — an image or video source: a DAM reference or an `https:` URL.\n */\nexport type AiPropRole = 'text' | 'url' | 'screen' | 'media'\n\n/**\n * A `restrictChildren` / `restrictParent` directive as generated data: the\n * same tuple the schema declares, with the flag spelled as the string the\n * enum holds so a JSON emitter can write it.\n */\nexport type AiLinealOrder = JsonLinealOrder\n\nexport interface AiPaletteEntry {\n pluginId: string\n kind: 'element' | 'plaintext' | 'markdown'\n category: string\n displayName: string\n /** One sentence, from the schema's own description. */\n summary: string\n /** Whether the editor lets anything be dropped inside this element. */\n acceptsChildren: boolean\n restrictChildren?: AiLinealOrder\n restrictParent?: AiLinealOrder\n propsSchema: AiPropsSchema\n /** Roles of the string props the validator treats specially. */\n propRoles: Record<string, AiPropRole>\n /**\n * The attribute field kind each declared prop is edited with, as its\n * `FieldComponentType` value (`switch`, `select`, `screen-select`, …): what\n * the editor reads to decide which component properties a field can be\n * bound to (AGL-2908).\n */\n propFields: Record<string, string>\n /** Length ceilings for `text` props, in characters. */\n textLimits: Record<string, number>\n /** Names of the presets that place this component. */\n presets: readonly string[]\n}\n\n/**\n * What every NODE can do, as against what one component declares (AGL-3156).\n *\n * Generated from the node capabilities core declares, beside the per-component\n * schemas rather than out of them: repeating is not a Stack's feature, so no\n * component schema names it and a palette built from schemas alone advertises\n * it on nothing. Declared ONCE for the whole palette — a model may write these\n * props on any element the surface allows.\n */\nexport interface AiNodeCapability {\n /** Stable id, as core declares it. */\n id: string\n displayName: string\n /** One sentence, for the prompt catalog. */\n summary: string\n propsSchema: AiPropsSchema\n propRoles: Record<string, AiPropRole>\n propFields: Record<string, string>\n textLimits: Record<string, number>\n /** Props offered only on a node that holds a child list. */\n childrenOnlyProps: readonly string[]\n /**\n * Declared attributes the palette deliberately does not offer: a picker\n * that chooses a record the site holds, which a model cannot name from a\n * description. Recorded so a field left off on purpose reads differently\n * from one that went missing.\n */\n omittedProps: readonly string[]\n}\n\n/**\n * The capability props a node with this child contract may carry, merged into\n * one map the prop sanitizer can read beside the element's own declarations.\n */\nexport function nodeCapabilityProps(\n capabilities: Readonly<Record<string, AiNodeCapability>>,\n acceptsChildren: boolean,\n): {\n properties: Record<string, AiPropSchema>\n propRoles: Record<string, AiPropRole>\n textLimits: Record<string, number>\n} {\n const properties: Record<string, AiPropSchema> = {}\n const propRoles: Record<string, AiPropRole> = {}\n const textLimits: Record<string, number> = {}\n for (const capability of Object.values(capabilities)) {\n for (const [name, schema] of Object.entries(\n capability.propsSchema.properties,\n )) {\n if (!acceptsChildren && capability.childrenOnlyProps.includes(name))\n continue\n properties[name] = schema\n const role = capability.propRoles[name]\n if (role) propRoles[name] = role\n const limit = capability.textLimits[name]\n if (limit !== undefined) textLimits[name] = limit\n }\n }\n return { properties, propRoles, textLimits }\n}\n\n/**\n * What a node capability declared in core and one carried by the palette\n * disagree about (AGL-3156).\n *\n * `check:ai-palette` pins the generated file to the generator's output for its\n * inputs, which cannot notice a capability leaving those inputs: when repeat\n * stopped being a component's attribute, the generator regenerated cleanly and\n * the palette simply stopped mentioning it. This reads the core declarations\n * instead and asks whether the palette still accounts for each one, so the\n * next generic capability cannot vanish the same way.\n *\n * `offeredFieldKinds` is every field kind the palette DOES carry somewhere,\n * derived from the generated entries rather than restated here: a capability\n * attribute drawn with one of those kinds has no excuse for being omitted.\n */\nexport function auditNodeCapabilities(\n declared: ReadonlyArray<{\n id: string\n label: string\n attributes: ReadonlyArray<{ name: string; component: string }>\n }>,\n carried: Readonly<Record<string, AiNodeCapability>>,\n offeredFieldKinds: ReadonlySet<string>,\n): string[] {\n const problems: string[] = []\n for (const capability of declared) {\n const entry = carried[capability.id]\n if (!entry) {\n problems.push(\n `the palette carries no node capability \"${capability.id}\": a model cannot use it at all`,\n )\n continue\n }\n if (!Object.keys(entry.propsSchema.properties).length) {\n problems.push(\n `node capability \"${capability.id}\" is carried with no prop a model can write`,\n )\n }\n for (const attribute of capability.attributes) {\n if (entry.propsSchema.properties[attribute.name]) continue\n if (!entry.omittedProps.includes(attribute.name)) {\n problems.push(\n `node capability \"${capability.id}\" declares \"${attribute.name}\", which the palette neither offers nor records as omitted`,\n )\n continue\n }\n if (offeredFieldKinds.has(attribute.component)) {\n problems.push(\n `node capability \"${capability.id}\" omits \"${attribute.name}\", whose \"${attribute.component}\" field the palette offers elsewhere`,\n )\n }\n }\n }\n for (const id of Object.keys(carried)) {\n if (!declared.some((capability) => capability.id === id)) {\n problems.push(\n `the palette carries node capability \"${id}\", which core no longer declares`,\n )\n }\n }\n return problems\n}\n\nexport interface AiSurfaceDefinition {\n /** The component id at the top of a tree for this surface. */\n root: string\n /** Every component id a node in this surface may carry. */\n allow: readonly string[]\n /**\n * Component ids admitted only in a tree the platform's own code composed\n * (AGL-3660), never listed in the surface's catalog: see\n * `AiNodeTreeContext.codeBuilt`.\n */\n codeOnly?: readonly string[]\n}\n\n/**\n * Theme vocabulary an `sx` value may name, read off the theme sources so the\n * validator's allowlist is derived rather than typed by hand.\n */\nexport interface AiSxTokens {\n /** Dotted palette paths such as `primary.main`. */\n palette: readonly string[]\n /** Responsive object keys. */\n breakpoints: readonly string[]\n /** Values `typography` may take. */\n typographyVariants: readonly string[]\n}\n\n/**\n * Text ceilings per role of copy (AGL-2905). A headline is one line of a\n * hero, body copy is a paragraph or two, and a button label has to fit a\n * button.\n */\nexport const AI_TEXT_LIMITS = {\n headline: 120,\n body: 2_000,\n button: 40,\n /** A short label, title or caption. */\n label: 200,\n /** A table's grid, every cell's words (AGL-4107): a page brought in carries its source's tables whole. */\n table: 20_000,\n} as const\n\n/**\n * Prompt budget per surface catalog, in characters at four per token.\n * Asserted by a spec so a component that grows its description cannot\n * silently push a system prompt past what the route pays for.\n */\nexport const AI_PALETTE_CATALOG_MAX_TOKENS = 2_500\nexport const AI_PALETTE_CATALOG_MAX_CHARS = AI_PALETTE_CATALOG_MAX_TOKENS * 4\n\n/** The model's own token estimate: characters over four. */\nexport function estimateCatalogTokens(catalog: string): number {\n return Math.ceil(catalog.length / 4)\n}\n\n/**\n * The documents a generator emits (AGL-2935). Each is composed on one\n * surface of the palette and held to its own budget below.\n */\nexport type AiOutputKind =\n | 'page'\n | 'template'\n | 'component'\n | 'layout'\n | 'form'\n | 'email'\n\nexport const AI_OUTPUT_KINDS: readonly AiOutputKind[] = [\n 'page',\n 'template',\n 'component',\n 'layout',\n 'form',\n 'email',\n]\n\n/** The surface each output kind is composed on. */\nexport const AI_OUTPUT_SURFACE: Record<AiOutputKind, AiSurface> = {\n page: 'screen',\n template: 'screen',\n component: 'component',\n layout: 'layout',\n form: 'form',\n email: 'email',\n}\n\n/**\n * What the rule-17 scorer measures on an emitted tree:\n *\n * - `nodes` — how many elements the tree carries;\n * - `bytes` — the stored node map, in the unit the save ceiling uses\n * (`nodeMapBytes`, msgpack);\n * - `imageBytes` — what the placed library images request at the slot's\n * variant, from each asset's recorded size;\n * - `embeds` — third-party players and frames the output loads;\n * - `fontFamilies` — families named beyond the theme's own;\n * - `emailHtmlBytes` — an email's rendered HTML, which is what a mail\n * client clips.\n */\nexport type AiBudgetMetric =\n | 'nodes'\n | 'bytes'\n | 'imageBytes'\n | 'embeds'\n | 'fontFamilies'\n | 'emailHtmlBytes'\n\nexport type AiOutputBudget = Record<Exclude<AiBudgetMetric, 'emailHtmlBytes'>, number> & {\n emailHtmlBytes?: number\n}\n\n/**\n * The size past which Gmail clips a message and hides the rest behind a\n * \"View entire message\" link — the unsubscribe footer included.\n */\nexport const AI_EMAIL_CLIP_BYTES = 102_000\n\n/**\n * The budget per output kind (AGL-2935, rule 17), beside the catalog budget\n * above for the same reason: a generator that grows its output past what\n * its kind is for is caught by a number, not noticed by a reader.\n *\n * A page is a few hundred elements and tens of kilobytes of stored map; a\n * reusable component is one repeated block; a layout is the chrome every\n * page loads, so it carries the least imagery; a form is fields and a\n * button — each field a Form Control over a Form Label and its answer, a\n * choice a Choice Label over its Radio or Checkbox, so a form of a few dozen\n * questions is a hundred-odd elements; an email is held to the clipping threshold on its rendered HTML.\n * No kind names a font beyond the theme's.\n *\n * A third-party player is a page's or a component's to carry, and only where\n * a confirmed plan lists it (rule 16). A template is rendered for every\n * record it serves and a layout around every page, so neither carries one\n * (AGL-3433): an embed there loads a stranger's code on every page of a\n * collection or a site.\n */\nexport const AI_OUTPUT_BUDGETS: Record<AiOutputKind, AiOutputBudget> = {\n page: {\n nodes: 400,\n bytes: 60_000,\n imageBytes: 1_500_000,\n embeds: 1,\n fontFamilies: 0,\n },\n template: {\n nodes: 400,\n bytes: 60_000,\n imageBytes: 1_500_000,\n embeds: 0,\n fontFamilies: 0,\n },\n component: {\n nodes: 80,\n bytes: 12_000,\n imageBytes: 500_000,\n embeds: 1,\n fontFamilies: 0,\n },\n layout: {\n nodes: 160,\n bytes: 24_000,\n imageBytes: 200_000,\n embeds: 0,\n fontFamilies: 0,\n },\n form: {\n nodes: 150,\n bytes: 15_000,\n imageBytes: 0,\n embeds: 0,\n fontFamilies: 0,\n },\n email: {\n nodes: 150,\n bytes: 24_000,\n imageBytes: 1_000_000,\n embeds: 0,\n fontFamilies: 0,\n emailHtmlBytes: AI_EMAIL_CLIP_BYTES,\n },\n}\n\n/**\n * What a first visit to a generated output is estimated to transfer — the\n * weight the proposal shows before anything is applied (rule 17). A page\n * starts from the platform's measured page weight; a component or a layout\n * reports only what it adds to a page; an email is not a page load.\n */\nexport interface AiLoadEstimate {\n /** The measured weight of a published page before this output's own bytes; 0 for a fragment. */\n pageBytes: number\n /** The stored node map, in the save's own bytes. */\n documentBytes: number\n /** What the placed library images request at their slots. */\n imageBytes: number\n /** Images the estimate could not size: no recorded size, or not from the library. */\n imagesUnmeasured: number\n embeds: number\n totalBytes: number\n}\n\n/** Typography variants that read as headlines, for the `children` ceiling. */\nexport function isHeadlineVariant(variant: unknown): boolean {\n return typeof variant === 'string' && /^(display\\w*|h[1-6]|heading)$/.test(variant)\n}\n"],"names":["AI_SURFACE_NAMES","nodeCapabilityProps","capabilities","acceptsChildren","properties","propRoles","textLimits","capability","Object","values","name","schema","entries","propsSchema","childrenOnlyProps","includes","role","limit","undefined","auditNodeCapabilities","declared","carried","offeredFieldKinds","problems","entry","id","push","keys","length","attribute","attributes","omittedProps","has","component","some","AI_TEXT_LIMITS","headline","body","button","label","table","AI_PALETTE_CATALOG_MAX_TOKENS","AI_PALETTE_CATALOG_MAX_CHARS","estimateCatalogTokens","catalog","Math","ceil","AI_OUTPUT_KINDS","AI_OUTPUT_SURFACE","page","template","layout","form","email","AI_EMAIL_CLIP_BYTES","AI_OUTPUT_BUDGETS","nodes","bytes","imageBytes","embeds","fontFamilies","emailHtmlBytes","isHeadlineVariant","variant","test"],"mappings":"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"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
export declare const SITE_LAYOUT_AGLYN_ORIGIN = "https://aglyn.com";
|
|
18
|
+
export declare const SITE_LAYOUT_POTTERY_ORIGIN = "https://www.tidewater-pottery.example";
|
|
19
|
+
/** The recorded pages, by name: the file and the address it was read from. */
|
|
20
|
+
export declare const SITE_LAYOUT_PAGES: {
|
|
21
|
+
readonly home: {
|
|
22
|
+
readonly file: "aglyn-home.html";
|
|
23
|
+
readonly url: "https://aglyn.com/";
|
|
24
|
+
};
|
|
25
|
+
readonly product: {
|
|
26
|
+
readonly file: "aglyn-product.html";
|
|
27
|
+
readonly url: "https://aglyn.com/product";
|
|
28
|
+
};
|
|
29
|
+
readonly pricing: {
|
|
30
|
+
readonly file: "aglyn-pricing.html";
|
|
31
|
+
readonly url: "https://aglyn.com/pricing";
|
|
32
|
+
};
|
|
33
|
+
readonly contact: {
|
|
34
|
+
readonly file: "aglyn-contact.html";
|
|
35
|
+
readonly url: "https://aglyn.com/contact";
|
|
36
|
+
};
|
|
37
|
+
readonly post: {
|
|
38
|
+
readonly file: "aglyn-blog-post.html";
|
|
39
|
+
readonly url: "https://aglyn.com/blog/multi-site-cost-sheet-2026";
|
|
40
|
+
};
|
|
41
|
+
readonly pottery: {
|
|
42
|
+
readonly file: "squarespace-pottery.html";
|
|
43
|
+
readonly url: "https://www.tidewater-pottery.example/";
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
export type SiteLayoutPageName = keyof typeof SITE_LAYOUT_PAGES;
|
|
47
|
+
/** One recorded page's markup. */
|
|
48
|
+
export declare function siteLayoutRecording(name: SiteLayoutPageName): string;
|
|
49
|
+
/** Whether an address is on the recorded page's own site. */
|
|
50
|
+
export declare function siteLayoutSameSite(name: SiteLayoutPageName): (url: URL) => boolean;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ /**
|
|
17
|
+
* Pages recorded with their layout (AGL-4106), for the specs of the layout,
|
|
18
|
+
* menu and stylesheet readers, and for the compile and fidelity specs that
|
|
19
|
+
* read what they find.
|
|
20
|
+
*
|
|
21
|
+
* `site-layout/` holds five pages of aglyn.com as AglynBot read them on
|
|
22
|
+
* 2026-10-28 and 2026-10-29 (home, product, pricing, contact, one blog post)
|
|
23
|
+
* and one page in the markup a Squarespace site is built with, written for
|
|
24
|
+
* these specs about a studio that does not exist, so the readers are never
|
|
25
|
+
* tuned to one site's markup.
|
|
26
|
+
*
|
|
27
|
+
* The aglyn.com recordings keep what the `aglyn-com/` ones dropped and the
|
|
28
|
+
* layout is made of: class names, inline icons and the page's own stylesheet.
|
|
29
|
+
* Each was trimmed before it entered the repo: script bodies are gone (one
|
|
30
|
+
* small inline script with a hostile line stands in for them; the
|
|
31
|
+
* structured data stays), so are preload links, `data-*` attributes and
|
|
32
|
+
* `srcset`s; the stylesheet keeps only the rules for classes the page uses
|
|
33
|
+
* and the properties the readers read; class names the kept rules do not name
|
|
34
|
+
* are dropped, a grid framework's own hints kept; the pricing page keeps the
|
|
35
|
+
* first 14 rows of its plan table, and the product, pricing, contact and blog
|
|
36
|
+
* pages their header menu without its panels and their footer's © line. The
|
|
37
|
+
* home page keeps its header and footer whole, for the menu reader. Every
|
|
38
|
+
* address, phone number, author and profile is a placeholder.
|
|
39
|
+
*/ import { readFileSync } from "node:fs";
|
|
40
|
+
import { join } from "node:path";
|
|
41
|
+
const DIR = join(__dirname, 'site-layout');
|
|
42
|
+
export const SITE_LAYOUT_AGLYN_ORIGIN = 'https://aglyn.com';
|
|
43
|
+
export const SITE_LAYOUT_POTTERY_ORIGIN = 'https://www.tidewater-pottery.example';
|
|
44
|
+
/** The recorded pages, by name: the file and the address it was read from. */ export const SITE_LAYOUT_PAGES = {
|
|
45
|
+
home: {
|
|
46
|
+
file: 'aglyn-home.html',
|
|
47
|
+
url: `${SITE_LAYOUT_AGLYN_ORIGIN}/`
|
|
48
|
+
},
|
|
49
|
+
product: {
|
|
50
|
+
file: 'aglyn-product.html',
|
|
51
|
+
url: `${SITE_LAYOUT_AGLYN_ORIGIN}/product`
|
|
52
|
+
},
|
|
53
|
+
pricing: {
|
|
54
|
+
file: 'aglyn-pricing.html',
|
|
55
|
+
url: `${SITE_LAYOUT_AGLYN_ORIGIN}/pricing`
|
|
56
|
+
},
|
|
57
|
+
contact: {
|
|
58
|
+
file: 'aglyn-contact.html',
|
|
59
|
+
url: `${SITE_LAYOUT_AGLYN_ORIGIN}/contact`
|
|
60
|
+
},
|
|
61
|
+
post: {
|
|
62
|
+
file: 'aglyn-blog-post.html',
|
|
63
|
+
url: `${SITE_LAYOUT_AGLYN_ORIGIN}/blog/multi-site-cost-sheet-2026`
|
|
64
|
+
},
|
|
65
|
+
pottery: {
|
|
66
|
+
file: 'squarespace-pottery.html',
|
|
67
|
+
url: `${SITE_LAYOUT_POTTERY_ORIGIN}/`
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
const cache = new Map();
|
|
71
|
+
/** One recorded page's markup. */ export function siteLayoutRecording(name) {
|
|
72
|
+
const { file } = SITE_LAYOUT_PAGES[name];
|
|
73
|
+
const held = cache.get(file);
|
|
74
|
+
if (held !== undefined) return held;
|
|
75
|
+
const text = readFileSync(join(DIR, file), 'utf8');
|
|
76
|
+
cache.set(file, text);
|
|
77
|
+
return text;
|
|
78
|
+
}
|
|
79
|
+
/** Whether an address is on the recorded page's own site. */ export function siteLayoutSameSite(name) {
|
|
80
|
+
const origin = new URL(SITE_LAYOUT_PAGES[name].url).origin;
|
|
81
|
+
const bare = origin.replace('://www.', '://');
|
|
82
|
+
const www = origin.replace('://', '://www.').replace('://www.www.', '://www.');
|
|
83
|
+
return (url)=>url.origin === origin || url.origin === bare || url.origin === www;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
//# sourceMappingURL=site-layout-recordings.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/plugins/ai/src/lib/site-import/fixtures/site-layout-recordings.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Pages recorded with their layout (AGL-4106), for the specs of the layout,\n * menu and stylesheet readers, and for the compile and fidelity specs that\n * read what they find.\n *\n * `site-layout/` holds five pages of aglyn.com as AglynBot read them on\n * 2026-10-28 and 2026-10-29 (home, product, pricing, contact, one blog post)\n * and one page in the markup a Squarespace site is built with, written for\n * these specs about a studio that does not exist, so the readers are never\n * tuned to one site's markup.\n *\n * The aglyn.com recordings keep what the `aglyn-com/` ones dropped and the\n * layout is made of: class names, inline icons and the page's own stylesheet.\n * Each was trimmed before it entered the repo: script bodies are gone (one\n * small inline script with a hostile line stands in for them; the\n * structured data stays), so are preload links, `data-*` attributes and\n * `srcset`s; the stylesheet keeps only the rules for classes the page uses\n * and the properties the readers read; class names the kept rules do not name\n * are dropped, a grid framework's own hints kept; the pricing page keeps the\n * first 14 rows of its plan table, and the product, pricing, contact and blog\n * pages their header menu without its panels and their footer's © line. The\n * home page keeps its header and footer whole, for the menu reader. Every\n * address, phone number, author and profile is a placeholder.\n */\n\nimport { readFileSync } from 'node:fs'\nimport { join } from 'node:path'\n\nconst DIR = join(__dirname, 'site-layout')\n\nexport const SITE_LAYOUT_AGLYN_ORIGIN = 'https://aglyn.com'\nexport const SITE_LAYOUT_POTTERY_ORIGIN = 'https://www.tidewater-pottery.example'\n\n/** The recorded pages, by name: the file and the address it was read from. */\nexport const SITE_LAYOUT_PAGES = {\n home: { file: 'aglyn-home.html', url: `${SITE_LAYOUT_AGLYN_ORIGIN}/` },\n product: { file: 'aglyn-product.html', url: `${SITE_LAYOUT_AGLYN_ORIGIN}/product` },\n pricing: { file: 'aglyn-pricing.html', url: `${SITE_LAYOUT_AGLYN_ORIGIN}/pricing` },\n contact: { file: 'aglyn-contact.html', url: `${SITE_LAYOUT_AGLYN_ORIGIN}/contact` },\n post: { file: 'aglyn-blog-post.html', url: `${SITE_LAYOUT_AGLYN_ORIGIN}/blog/multi-site-cost-sheet-2026` },\n pottery: { file: 'squarespace-pottery.html', url: `${SITE_LAYOUT_POTTERY_ORIGIN}/` },\n} as const\n\nexport type SiteLayoutPageName = keyof typeof SITE_LAYOUT_PAGES\n\nconst cache = new Map<string, string>()\n\n/** One recorded page's markup. */\nexport function siteLayoutRecording(name: SiteLayoutPageName): string {\n const { file } = SITE_LAYOUT_PAGES[name]\n const held = cache.get(file)\n if (held !== undefined) return held\n const text = readFileSync(join(DIR, file), 'utf8')\n cache.set(file, text)\n return text\n}\n\n/** Whether an address is on the recorded page's own site. */\nexport function siteLayoutSameSite(name: SiteLayoutPageName): (url: URL) => boolean {\n const origin = new URL(SITE_LAYOUT_PAGES[name].url).origin\n const bare = origin.replace('://www.', '://')\n const www = origin.replace('://', '://www.').replace('://www.www.', '://www.')\n return (url) => url.origin === origin || url.origin === bare || url.origin === www\n}\n"],"names":["readFileSync","join","DIR","__dirname","SITE_LAYOUT_AGLYN_ORIGIN","SITE_LAYOUT_POTTERY_ORIGIN","SITE_LAYOUT_PAGES","home","file","url","product","pricing","contact","post","pottery","cache","Map","siteLayoutRecording","name","held","get","undefined","text","set","siteLayoutSameSite","origin","URL","bare","replace","www"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;CAuBC,GAED,SAASA,YAAY,QAAQ,UAAS;AACtC,SAASC,IAAI,QAAQ,YAAW;AAEhC,MAAMC,MAAMD,KAAKE,WAAW;AAE5B,OAAO,MAAMC,2BAA2B,oBAAmB;AAC3D,OAAO,MAAMC,6BAA6B,wCAAuC;AAEjF,4EAA4E,GAC5E,OAAO,MAAMC,oBAAoB;IAC/BC,MAAM;QAAEC,MAAM;QAAmBC,KAAK,GAAGL,yBAAyB,CAAC,CAAC;IAAC;IACrEM,SAAS;QAAEF,MAAM;QAAsBC,KAAK,GAAGL,yBAAyB,QAAQ,CAAC;IAAC;IAClFO,SAAS;QAAEH,MAAM;QAAsBC,KAAK,GAAGL,yBAAyB,QAAQ,CAAC;IAAC;IAClFQ,SAAS;QAAEJ,MAAM;QAAsBC,KAAK,GAAGL,yBAAyB,QAAQ,CAAC;IAAC;IAClFS,MAAM;QAAEL,MAAM;QAAwBC,KAAK,GAAGL,yBAAyB,gCAAgC,CAAC;IAAC;IACzGU,SAAS;QAAEN,MAAM;QAA4BC,KAAK,GAAGJ,2BAA2B,CAAC,CAAC;IAAC;AACrF,EAAU;AAIV,MAAMU,QAAQ,IAAIC;AAElB,gCAAgC,GAChC,OAAO,SAASC,oBAAoBC,IAAwB;IAC1D,MAAM,EAAEV,IAAI,EAAE,GAAGF,iBAAiB,CAACY,KAAK;IACxC,MAAMC,OAAOJ,MAAMK,GAAG,CAACZ;IACvB,IAAIW,SAASE,WAAW,OAAOF;IAC/B,MAAMG,OAAOtB,aAAaC,KAAKC,KAAKM,OAAO;IAC3CO,MAAMQ,GAAG,CAACf,MAAMc;IAChB,OAAOA;AACT;AAEA,2DAA2D,GAC3D,OAAO,SAASE,mBAAmBN,IAAwB;IACzD,MAAMO,SAAS,IAAIC,IAAIpB,iBAAiB,CAACY,KAAK,CAACT,GAAG,EAAEgB,MAAM;IAC1D,MAAME,OAAOF,OAAOG,OAAO,CAAC,WAAW;IACvC,MAAMC,MAAMJ,OAAOG,OAAO,CAAC,OAAO,WAAWA,OAAO,CAAC,eAAe;IACpE,OAAO,CAACnB,MAAQA,IAAIgB,MAAM,KAAKA,UAAUhB,IAAIgB,MAAM,KAAKE,QAAQlB,IAAIgB,MAAM,KAAKI;AACjF"}
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
17
|
import { type PublicFetchEdge, type PublicFetchOptions, type PublicFetchResult } from '../jobs/ai-theme-brand-inputs';
|
|
18
|
+
import type { SiteImportFooter, SiteImportLayout, SiteImportLookFacts, SiteImportNavTree } from '../model/ai-site-import';
|
|
18
19
|
import { type SiteImportBlock, type SiteImportBrandMark, type SiteImportForm, type SiteImportIcon, type SiteImportImage, type SiteImportNavLink, type SiteImportOrganization } from './site-import-html';
|
|
19
20
|
/**
|
|
20
21
|
* Where the bot's user agent points a site owner who wants to know what it is
|
|
@@ -32,7 +33,10 @@ export declare const AGLYN_BOT_INFO_URL: string;
|
|
|
32
33
|
export declare const AGLYN_BOT_USER_AGENT: string;
|
|
33
34
|
export declare const SITE_IMPORT_DEFAULTS: {
|
|
34
35
|
readonly maxPageBytes: number;
|
|
35
|
-
/**
|
|
36
|
+
/**
|
|
37
|
+
* A whole site's worth (AGL-4083): 54 MB for aglyn.com's 123 pages, with
|
|
38
|
+
* room; 400 pages at its 440 KB average are 176 MB (AGL-4106).
|
|
39
|
+
*/
|
|
36
40
|
readonly maxTotalBytes: number;
|
|
37
41
|
readonly timeBudgetMs: 60000;
|
|
38
42
|
/** One request's own clock, inside the whole crawl's. */
|
|
@@ -102,6 +106,12 @@ export interface SiteImportPage {
|
|
|
102
106
|
removed: number;
|
|
103
107
|
};
|
|
104
108
|
bytes: number;
|
|
109
|
+
/** How the source lays the page out (AGL-4106), on a read asked for it (`layout`): a wave's. */
|
|
110
|
+
layout?: SiteImportLayout | null;
|
|
111
|
+
/** The home page's menu, footer and look, as its source builds them (AGL-4106). */
|
|
112
|
+
nav?: SiteImportNavTree | null;
|
|
113
|
+
footer?: SiteImportFooter | null;
|
|
114
|
+
look?: SiteImportLookFacts | null;
|
|
105
115
|
}
|
|
106
116
|
export type SiteImportStop = 'page-budget' | 'total-bytes' | 'time'
|
|
107
117
|
/** robots.txt could not be read, so nothing was asked of the site. */
|
|
@@ -149,6 +159,13 @@ export interface SiteImportCrawlOptions {
|
|
|
149
159
|
* as for any crawl, and every other rule holds.
|
|
150
160
|
*/
|
|
151
161
|
only?: readonly string[];
|
|
162
|
+
/**
|
|
163
|
+
* Read how each page is laid out (AGL-4106): its structure, through its own
|
|
164
|
+
* stylesheets (the first two it links, each asked once a crawl). On by
|
|
165
|
+
* default for a wave's read (`only`), whose pages are built; the home page's
|
|
166
|
+
* menu, footer and look are read on every crawl.
|
|
167
|
+
*/
|
|
168
|
+
layout?: boolean;
|
|
152
169
|
}
|
|
153
170
|
export interface SiteImportCrawlSeams {
|
|
154
171
|
fetch?: (url: string, options: PublicFetchOptions) => Promise<PublicFetchResult>;
|
|
@@ -62,10 +62,12 @@ var _process_env_SITE_IMPORT_BOT_INFO_URL;
|
|
|
62
62
|
* reach the inventory (site-import-html.ts).
|
|
63
63
|
*=========================================*/ import { lookalikeBrandForHost } from "@aglyn/shared-util-email/outbound-phishing-screen";
|
|
64
64
|
import { fenceUntrustedText } from "@aglyn/shared-util-tools/untrusted-text";
|
|
65
|
-
import { fetchPublicTextDetailed } from "../jobs/ai-theme-brand-inputs.js";
|
|
65
|
+
import { AI_THEME_REFERENCE_STYLESHEETS, AI_THEME_STYLESHEET_MAX_BYTES, fetchPublicTextDetailed } from "../jobs/ai-theme-brand-inputs.js";
|
|
66
66
|
import { extractSiteImportPage, readMetaValues } from "./site-import-html.js";
|
|
67
67
|
import { AGLYN_BOT_TOKEN, parseRobotsTxt, parseSitemap, robotsAllows, robotsRulesFor } from "./site-import-robots.js";
|
|
68
68
|
import { NO_SITE_IMPORT_HEADERS, siteImportEdgeHeadersFor } from "./site-import-edge.js";
|
|
69
|
+
import { siteImportLayout, siteImportLookOf } from "./site-import-layout.js";
|
|
70
|
+
import { siteImportFooterColumns, siteImportNavTree } from "./site-import-nav.js";
|
|
69
71
|
/**
|
|
70
72
|
* Where the bot's user agent points a site owner who wants to know what it is
|
|
71
73
|
* (AGL-3983): the operator's own page about it. `SITE_IMPORT_BOT_INFO_URL` on
|
|
@@ -80,7 +82,10 @@ import { NO_SITE_IMPORT_HEADERS, siteImportEdgeHeadersFor } from "./site-import-
|
|
|
80
82
|
/** What every crawler request says it is. */ export const AGLYN_BOT_USER_AGENT = `AglynBot/1.0 (+${AGLYN_BOT_INFO_URL})`;
|
|
81
83
|
export const SITE_IMPORT_DEFAULTS = {
|
|
82
84
|
maxPageBytes: 2 * 1024 * 1024,
|
|
83
|
-
/**
|
|
85
|
+
/**
|
|
86
|
+
* A whole site's worth (AGL-4083): 54 MB for aglyn.com's 123 pages, with
|
|
87
|
+
* room; 400 pages at its 440 KB average are 176 MB (AGL-4106).
|
|
88
|
+
*/ maxTotalBytes: 320 * 1024 * 1024,
|
|
84
89
|
timeBudgetMs: 60000,
|
|
85
90
|
/** One request's own clock, inside the whole crawl's. */ requestTimeoutMs: 8000,
|
|
86
91
|
robotsMaxBytes: 256 * 1024,
|
|
@@ -92,6 +97,12 @@ export const SITE_IMPORT_DEFAULTS = {
|
|
|
92
97
|
};
|
|
93
98
|
/** Never a page: files a link may point at that are not HTML. */ const NON_PAGE_EXTENSION = /\.(?:pdf|jpe?g|png|gif|webp|avif|svg|ico|bmp|tiff?|zip|gz|tgz|rar|7z|mp4|m4v|mov|webm|mp3|wav|ogg|m4a|docx?|xlsx?|pptx?|csv|txt|css|js|mjs|json|xml|rss|atom|woff2?|ttf|otf|eot|exe|dmg|apk)$/i;
|
|
94
99
|
const TRACKING_PARAM = /^(?:utm_[a-z]+|fbclid|gclid|msclkid|mc_cid|mc_eid|_ga|ref)$/i;
|
|
100
|
+
/**
|
|
101
|
+
* Never a page, whatever its extension (AGL-4106): an API's or a CDN's path
|
|
102
|
+
* (aglyn.com's press kit links 136 `/api/media/cdn/…?download=1` files) and
|
|
103
|
+
* any address that asks for a download.
|
|
104
|
+
*/ const NON_PAGE_PATH = /^\/(?:api|cdn-cgi|wp-json|_next\/static|_next\/image)(?:\/|$)/i;
|
|
105
|
+
const DOWNLOAD_PARAM = /^(?:download|dl|attachment)$/i;
|
|
95
106
|
const DOMAIN_PATTERN = /^(?!-)[a-z0-9-]{1,63}(\.[a-z0-9-]{1,63})+$/;
|
|
96
107
|
/**
|
|
97
108
|
* The per-request headers of a crawl of `domain`: the seam's, else the edge's,
|
|
@@ -124,7 +135,10 @@ const DOMAIN_PATTERN = /^(?!-)[a-z0-9-]{1,63}(\.[a-z0-9-]{1,63})+$/;
|
|
|
124
135
|
if (url.protocol !== 'https:' || url.username || url.password) return null;
|
|
125
136
|
if (url.port && url.port !== '443') return null;
|
|
126
137
|
if (!origins.includes(url.origin)) return null;
|
|
127
|
-
if (NON_PAGE_EXTENSION.test(url.pathname)) return null;
|
|
138
|
+
if (NON_PAGE_EXTENSION.test(url.pathname) || NON_PAGE_PATH.test(url.pathname)) return null;
|
|
139
|
+
if ([
|
|
140
|
+
...url.searchParams.keys()
|
|
141
|
+
].some((key)=>DOWNLOAD_PARAM.test(key))) return null;
|
|
128
142
|
const clean = new URL(url.toString());
|
|
129
143
|
clean.hash = '';
|
|
130
144
|
for (const key of [
|
|
@@ -216,7 +230,7 @@ function skipReasonFor(result) {
|
|
|
216
230
|
else inventory.skippedOverflow += 1;
|
|
217
231
|
};
|
|
218
232
|
try {
|
|
219
|
-
var _robots_status, _robots_status1;
|
|
233
|
+
var _robots_status, _robots_status1, _options_layout;
|
|
220
234
|
// robots.txt, before anything else is asked of the site.
|
|
221
235
|
const home = `${origins[0]}/`;
|
|
222
236
|
let rules = [];
|
|
@@ -347,7 +361,21 @@ function skipReasonFor(result) {
|
|
|
347
361
|
if (remainingBytes() - reserved <= 0) return 'wait';
|
|
348
362
|
return null;
|
|
349
363
|
};
|
|
350
|
-
|
|
364
|
+
const readLayout = (_options_layout = options.layout) != null ? _options_layout : !!only;
|
|
365
|
+
/** Linked stylesheets, each asked once a crawl. */ const sheets = new Map();
|
|
366
|
+
const stylesheet = (url)=>{
|
|
367
|
+
const held = sheets.get(url);
|
|
368
|
+
if (held) return held;
|
|
369
|
+
const room = Math.min(AI_THEME_STYLESHEET_MAX_BYTES, remainingBytes() - reserved);
|
|
370
|
+
const asked = room <= 0 || outOfTime() ? Promise.resolve(null) : request(url, /text\/css|text\/plain/i, 'text/css,*/*;q=0.1', room).then((answer)=>{
|
|
371
|
+
if (!answer.ok) return null;
|
|
372
|
+
inventory.bytesRead += answer.bytes;
|
|
373
|
+
return answer.text;
|
|
374
|
+
}, ()=>null);
|
|
375
|
+
sheets.set(url, asked);
|
|
376
|
+
return asked;
|
|
377
|
+
};
|
|
378
|
+
/** Takes one asked page in: the inventory's next page, a skip, or the reason the read must stop. */ const takeIn = async ({ next, key, cap, page })=>{
|
|
351
379
|
var _siteImportPageKey;
|
|
352
380
|
const foundBy = next.foundBy;
|
|
353
381
|
if (foundBy === 'home') homeIn = true;
|
|
@@ -360,6 +388,8 @@ function skipReasonFor(result) {
|
|
|
360
388
|
cutOff.push(key);
|
|
361
389
|
return 'time';
|
|
362
390
|
}
|
|
391
|
+
// A file is not a page: it takes nothing from the page budget's attempts (AGL-4106).
|
|
392
|
+
if (page.refusal === 'content-type') attempts = Math.max(0, attempts - 1);
|
|
363
393
|
skip(_extends({
|
|
364
394
|
url: key,
|
|
365
395
|
reason: skipReasonFor(page)
|
|
@@ -386,7 +416,11 @@ function skipReasonFor(result) {
|
|
|
386
416
|
return null;
|
|
387
417
|
}
|
|
388
418
|
}
|
|
389
|
-
const
|
|
419
|
+
const isHome = foundBy === 'home';
|
|
420
|
+
const structured = readLayout || isHome;
|
|
421
|
+
const extract = extractSiteImportPage(page.text, page.url, sameSite, {
|
|
422
|
+
structure: structured
|
|
423
|
+
});
|
|
390
424
|
if (extract.loginForm) {
|
|
391
425
|
skip({
|
|
392
426
|
url: finalKey,
|
|
@@ -407,7 +441,33 @@ function skipReasonFor(result) {
|
|
|
407
441
|
});
|
|
408
442
|
}
|
|
409
443
|
}
|
|
410
|
-
|
|
444
|
+
// How the page is built (AGL-4106), through its own stylesheets.
|
|
445
|
+
let built = {};
|
|
446
|
+
if (extract.structure) {
|
|
447
|
+
const linked = await Promise.all(extract.structure.stylesheets.filter((url)=>!/fonts\.googleapis\.com/i.test(url)).slice(0, AI_THEME_REFERENCE_STYLESHEETS).map(stylesheet));
|
|
448
|
+
const css = linked.filter((text)=>typeof text === 'string');
|
|
449
|
+
const source = {
|
|
450
|
+
blocks: extract.blocks,
|
|
451
|
+
organization: extract.organization,
|
|
452
|
+
structure: extract.structure
|
|
453
|
+
};
|
|
454
|
+
try {
|
|
455
|
+
built = _extends({}, readLayout ? {
|
|
456
|
+
layout: siteImportLayout(source, css)
|
|
457
|
+
} : {}, isHome ? {
|
|
458
|
+
nav: siteImportNavTree(source, css),
|
|
459
|
+
footer: siteImportFooterColumns(source, css),
|
|
460
|
+
look: siteImportLookOf(source, css)
|
|
461
|
+
} : {});
|
|
462
|
+
} catch (error) {
|
|
463
|
+
// A page the reader cannot lay out is still its words.
|
|
464
|
+
console.warn('[site-import] the layout could not be read', {
|
|
465
|
+
url: page.url,
|
|
466
|
+
error
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
inventory.pages.push(_extends({}, built, {
|
|
411
471
|
url: finalKey,
|
|
412
472
|
path: new URL(finalKey).pathname,
|
|
413
473
|
foundBy,
|
|
@@ -428,11 +488,11 @@ function skipReasonFor(result) {
|
|
|
428
488
|
],
|
|
429
489
|
forms: extract.forms,
|
|
430
490
|
bytes: page.bytes
|
|
431
|
-
});
|
|
491
|
+
}));
|
|
432
492
|
return null;
|
|
433
493
|
};
|
|
434
494
|
for(;;){
|
|
435
|
-
var
|
|
495
|
+
var _ref;
|
|
436
496
|
const width = homeIn ? SITE_IMPORT_DEFAULTS.concurrency : 1;
|
|
437
497
|
while(!stop && queue.length && window.length < width){
|
|
438
498
|
const why = held();
|
|
@@ -466,7 +526,7 @@ function skipReasonFor(result) {
|
|
|
466
526
|
if (!window.length) break;
|
|
467
527
|
const asked = await window.shift();
|
|
468
528
|
reserved -= asked.cap;
|
|
469
|
-
stop = (
|
|
529
|
+
stop = (_ref = await takeIn(asked)) != null ? _ref : stop;
|
|
470
530
|
}
|
|
471
531
|
inventory.stoppedBy = stop;
|
|
472
532
|
// Whatever a stop left unread is said, never dropped; a wave's own read
|