@aglyn/plugins-ai 1.0.0-beta.182 → 1.0.0-beta.183

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aglyn/plugins-ai",
3
- "version": "1.0.0-beta.182",
3
+ "version": "1.0.0-beta.183",
4
4
  "license": "Apache-2.0",
5
5
  "homepage": "https://aglyn.com",
6
6
  "repository": {
@@ -25,22 +25,22 @@
25
25
  "./package.json": "./package.json"
26
26
  },
27
27
  "dependencies": {
28
- "@aglyn/aglyn": "1.0.0-beta.182",
29
- "@aglyn/shared-data-enums": "1.0.0-beta.182",
30
- "@aglyn/shared-data-mdi": "1.0.0-beta.182",
31
- "@aglyn/shared-data-types": "1.0.0-beta.182",
32
- "@aglyn/shared-ui-email-campaigns": "1.0.0-beta.182",
33
- "@aglyn/shared-ui-jsx": "1.0.0-beta.182",
34
- "@aglyn/shared-ui-snackstack": "1.0.0-beta.182",
35
- "@aglyn/shared-ui-theme": "1.0.0-beta.182",
36
- "@aglyn/shared-util-email": "1.0.0-beta.182",
37
- "@aglyn/shared-util-errors": "1.0.0-beta.182",
38
- "@aglyn/shared-util-http": "1.0.0-beta.182",
39
- "@aglyn/shared-util-timestamp": "1.0.0-beta.182",
40
- "@aglyn/shared-util-tools": "1.0.0-beta.182",
41
- "@aglyn/tenant-data-admin": "1.0.0-beta.182",
42
- "@aglyn/tenant-feature-instance": "1.0.0-beta.182",
43
- "@aglyn/tenant-runtime": "1.0.0-beta.182",
28
+ "@aglyn/aglyn": "1.0.0-beta.183",
29
+ "@aglyn/shared-data-enums": "1.0.0-beta.183",
30
+ "@aglyn/shared-data-mdi": "1.0.0-beta.183",
31
+ "@aglyn/shared-data-types": "1.0.0-beta.183",
32
+ "@aglyn/shared-ui-email-campaigns": "1.0.0-beta.183",
33
+ "@aglyn/shared-ui-jsx": "1.0.0-beta.183",
34
+ "@aglyn/shared-ui-snackstack": "1.0.0-beta.183",
35
+ "@aglyn/shared-ui-theme": "1.0.0-beta.183",
36
+ "@aglyn/shared-util-email": "1.0.0-beta.183",
37
+ "@aglyn/shared-util-errors": "1.0.0-beta.183",
38
+ "@aglyn/shared-util-http": "1.0.0-beta.183",
39
+ "@aglyn/shared-util-timestamp": "1.0.0-beta.183",
40
+ "@aglyn/shared-util-tools": "1.0.0-beta.183",
41
+ "@aglyn/tenant-data-admin": "1.0.0-beta.183",
42
+ "@aglyn/tenant-feature-instance": "1.0.0-beta.183",
43
+ "@aglyn/tenant-runtime": "1.0.0-beta.183",
44
44
  "@swc/helpers": "0.5.23"
45
45
  },
46
46
  "peerDependencies": {
@@ -16,7 +16,7 @@
16
16
  */ 'use client';
17
17
  import { _ as _extends } from "@swc/helpers/_/_extends";
18
18
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
19
- import { canvas, PLATFORM_BRAND_NAME, REUSABLE_INSTANCE_COMPONENT_ID } from "@aglyn/aglyn";
19
+ import { canvas, PLATFORM_BRAND_NAME, REUSABLE_COMPONENT_KIND_EMAIL, REUSABLE_INSTANCE_COMPONENT_ID, reusableComponentKindForRoot } from "@aglyn/aglyn";
20
20
  import { openEditorSession } from "@aglyn/aglyn/plugin-manager/editor-sessions";
21
21
  import { authorizedFetch } from "@aglyn/shared-util-http/authorized-token";
22
22
  import { useHostResourceApi, useUser } from "@aglyn/tenant-feature-instance";
@@ -92,6 +92,11 @@ export function AiSaveAsComponent(props) {
92
92
  ]);
93
93
  const writer = {
94
94
  createComponent: async (input)=>{
95
+ // A block saved out of an email is an email block (AGL-3287), so it is
96
+ // offered in emails and never on a page. Read off the element being
97
+ // saved, because this panel sees the canvas and not the editor's view;
98
+ // an email offers email blocks alone, so the two cannot disagree.
99
+ const kind = reusableComponentKindForRoot(input.nodes[input.rootId]);
95
100
  const created = await createHostResource({
96
101
  hostId: String(hostId),
97
102
  resource: 'reusableComponent',
@@ -101,6 +106,8 @@ export function AiSaveAsComponent(props) {
101
106
  nodes: input.nodes
102
107
  }, input.props.length ? {
103
108
  props: input.props
109
+ } : {}, kind === REUSABLE_COMPONENT_KIND_EMAIL ? {
110
+ kind
104
111
  } : {})
105
112
  });
106
113
  return created.id;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/components/ai-save-as-component.component.tsx"],"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'use client'\n\nimport {\n canvas,\n PLATFORM_BRAND_NAME,\n REUSABLE_INSTANCE_COMPONENT_ID,\n type NodeSchema,\n} from '@aglyn/aglyn'\nimport { openEditorSession } from '@aglyn/aglyn/plugin-manager/editor-sessions'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport { useHostResourceApi, useUser } from '@aglyn/tenant-feature-instance'\nimport { Alert, Button, CircularProgress, FormControl, Stack, TextField } from '@mui/material'\nimport { usePathname } from 'next/navigation'\nimport { useCallback, useState } from 'react'\nimport {\n assistEditDocumentOf,\n isAssistEditProposal,\n type AssistEditAppliedReport,\n type AssistEditProposal,\n} from '../model/assist-edit'\nimport {\n applyAssistEditSavingComponent,\n describeAssistEditCanvas,\n type AssistEditComponentWriter,\n} from './assist-edit-canvas'\nimport { AssistEditCard } from './assist-edit-card.component'\n\n/**\n * Save the selection as a reusable component, with AI (AGL-2908) — the\n * besigner half of the component job, in the `besignerInspector` zone under\n * the selected element's own fields.\n *\n * The person names the component, the door reads the outline of what is\n * open and proposes which values become properties, and AGL-2906's own card\n * asks before anything happens. Applying creates the component through the\n * host resources route and swaps the selection for an instance of it, on the\n * open draft, as one undo step.\n *\n * ## Where it is drawn\n *\n * On a versioned besigner — a screen, component or layout, which is what\n * `assistEditDocumentOf` answers for — for an element the panel says this\n * editor may change in place (`editable`, AGL-2908), which is not the canvas\n * root and not locked layout chrome. Never on an instance: one is already a\n * component, and promoting it would make a component of a placeholder.\n *\n * The widget's own gates (`aiGenerative`, `ai.generate`) are the zone's, so a\n * reader without them never sees the button. The door decides again.\n */\n\nexport interface AiSaveAsComponentProps {\n /** The workspace the editor is in; the door gates and bills on it. */\n orgId?: string | null\n /** The site the editor names; the zone reads it from the editor's route. */\n hostId?: string | null\n /** The selected element, as the Attributes panel resolved it. */\n node?: NodeSchema<any> | null\n /** Whether this editor may change that element in place. */\n editable?: boolean\n}\n\n/** The name the promote dialog opens with: the element's own, else a plain one. */\nfunction suggestedName(node: NodeSchema<any> | null | undefined): string {\n const named = typeof (node as { name?: unknown })?.name === 'string' ? String((node as { name?: string }).name) : ''\n return named || String(node?.componentSchema?.displayName ?? 'Component')\n}\n\nexport function AiSaveAsComponent(props: AiSaveAsComponentProps) {\n const { orgId, hostId, node, editable } = props\n const { data: user } = useUser()\n const createHostResource = useHostResourceApi()\n const pathname = usePathname()\n const [name, setName] = useState<string | null>(null)\n const [busy, setBusy] = useState(false)\n const [proposal, setProposal] = useState<AssistEditProposal | null>(null)\n const [exchangeId, setExchangeId] = useState<string | null>(null)\n const [failure, setFailure] = useState<string | null>(null)\n\n const document = assistEditDocumentOf(pathname ?? '')\n\n const propose = useCallback(async () => {\n const asked = (name ?? '').trim()\n if (!asked || !hostId || !orgId) return\n setBusy(true)\n setFailure(null)\n try {\n const session = openEditorSession()\n const outline = session ? describeAssistEditCanvas(canvas, node?.$id ?? null) : null\n if (!outline) {\n setFailure('The editor did not describe what is open. Reopen it and try again.')\n return\n }\n const response = await authorizedFetch(user, '/api/ai/generate/component', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n orgId,\n hostId,\n route: pathname ?? '',\n canvas: outline,\n name: asked,\n }),\n })\n const answer = await response.json().catch(() => ({}))\n if (!response.ok || !isAssistEditProposal(answer?.edit)) {\n setFailure(String(answer?.error ?? 'The component could not be proposed.'))\n return\n }\n setProposal(answer.edit)\n setExchangeId(typeof answer.exchangeId === 'string' ? answer.exchangeId : null)\n } catch {\n setFailure('The component could not be proposed. Try again.')\n } finally {\n setBusy(false)\n }\n }, [name, hostId, orgId, node, pathname, user])\n\n const writer: AssistEditComponentWriter = {\n createComponent: async (input) => {\n const created = await createHostResource({\n hostId: String(hostId),\n resource: 'reusableComponent',\n data: {\n displayName: input.name,\n rootId: input.rootId,\n nodes: input.nodes,\n ...(input.props.length ? { props: input.props } : {}),\n },\n })\n return created.id\n },\n }\n\n const apply = useCallback(async () => {\n if (!proposal || !orgId) return\n setBusy(true)\n setFailure(null)\n try {\n const session = openEditorSession()\n const result = await applyAssistEditSavingComponent(canvas as never, proposal, session, writer)\n if (result.ok === false) {\n setFailure(result.message)\n return\n }\n setProposal(null)\n setName(null)\n if (!exchangeId || !session) return\n const report: AssistEditAppliedReport = {\n orgId,\n hostId: proposal.target.hostId,\n exchangeId,\n documentKind: proposal.target.kind,\n documentId: proposal.target.documentId,\n versionId: session.versionId,\n opCounts: result.opCounts,\n }\n // Fire and forget: the component exists and the canvas has changed, so\n // a record that fails to land must not read as a save that failed.\n void authorizedFetch(user, '/api/assist/edit-applied', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(report),\n }).catch(() => undefined)\n } finally {\n setBusy(false)\n }\n }, [proposal, orgId, exchangeId, user, writer])\n\n if (!document || !hostId || !orgId || !editable || !node) return null\n // An instance already follows a component; promoting it would make a\n // component of a placeholder.\n if (node.componentId === REUSABLE_INSTANCE_COMPONENT_ID) return null\n\n if (proposal) {\n return (\n <AssistEditCard\n proposal={proposal}\n brand={PLATFORM_BRAND_NAME}\n session={openEditorSession()}\n failure={failure}\n onApply={() => void apply()}\n onCreateVersion={() => openEditorSession()?.createVersion?.()}\n onDismiss={() => {\n setProposal(null)\n setFailure(null)\n }}\n />\n )\n }\n\n if (name === null) {\n return (\n <FormControl margin=\"none\" fullWidth>\n <Button color=\"primary\" fullWidth onClick={() => setName(suggestedName(node))}>\n {'Make a reusable component with AI'}\n </Button>\n </FormControl>\n )\n }\n\n return (\n <Stack spacing={1} sx={{ mt: 1 }}>\n <TextField\n size=\"small\"\n label=\"Component name\"\n value={name}\n onChange={(event) => setName(event.target.value)}\n disabled={busy}\n fullWidth\n />\n {failure ? <Alert severity=\"warning\">{failure}</Alert> : null}\n <Stack direction=\"row\" spacing={1}>\n <Button\n variant=\"contained\"\n size=\"small\"\n disabled={busy || !name.trim()}\n onClick={() => void propose()}\n startIcon={busy ? <CircularProgress size={14} /> : undefined}\n >\n {busy ? 'Reading the section…' : 'Suggest properties'}\n </Button>\n <Button size=\"small\" color=\"inherit\" disabled={busy} onClick={() => setName(null)}>\n {'Cancel'}\n </Button>\n </Stack>\n </Stack>\n )\n}\nAiSaveAsComponent.displayName = 'AiSaveAsComponent'\n\nexport default AiSaveAsComponent\n"],"names":["canvas","PLATFORM_BRAND_NAME","REUSABLE_INSTANCE_COMPONENT_ID","openEditorSession","authorizedFetch","useHostResourceApi","useUser","Alert","Button","CircularProgress","FormControl","Stack","TextField","usePathname","useCallback","useState","assistEditDocumentOf","isAssistEditProposal","applyAssistEditSavingComponent","describeAssistEditCanvas","AssistEditCard","suggestedName","node","named","name","String","componentSchema","displayName","AiSaveAsComponent","props","orgId","hostId","editable","data","user","createHostResource","pathname","setName","busy","setBusy","proposal","setProposal","exchangeId","setExchangeId","failure","setFailure","document","propose","asked","trim","session","outline","$id","response","method","headers","body","JSON","stringify","route","answer","json","catch","ok","edit","error","writer","createComponent","input","created","resource","rootId","nodes","length","id","apply","result","message","report","target","documentKind","kind","documentId","versionId","opCounts","undefined","componentId","brand","onApply","onCreateVersion","createVersion","onDismiss","margin","fullWidth","color","onClick","spacing","sx","mt","size","label","value","onChange","event","disabled","severity","direction","variant","startIcon"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/components/ai-save-as-component.component.tsx"],"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'use client'\n\nimport {\n canvas,\n PLATFORM_BRAND_NAME,\n REUSABLE_COMPONENT_KIND_EMAIL,\n REUSABLE_INSTANCE_COMPONENT_ID,\n reusableComponentKindForRoot,\n type NodeSchema,\n} from '@aglyn/aglyn'\nimport { openEditorSession } from '@aglyn/aglyn/plugin-manager/editor-sessions'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport { useHostResourceApi, useUser } from '@aglyn/tenant-feature-instance'\nimport { Alert, Button, CircularProgress, FormControl, Stack, TextField } from '@mui/material'\nimport { usePathname } from 'next/navigation'\nimport { useCallback, useState } from 'react'\nimport {\n assistEditDocumentOf,\n isAssistEditProposal,\n type AssistEditAppliedReport,\n type AssistEditProposal,\n} from '../model/assist-edit'\nimport {\n applyAssistEditSavingComponent,\n describeAssistEditCanvas,\n type AssistEditComponentWriter,\n} from './assist-edit-canvas'\nimport { AssistEditCard } from './assist-edit-card.component'\n\n/**\n * Save the selection as a reusable component, with AI (AGL-2908) — the\n * besigner half of the component job, in the `besignerInspector` zone under\n * the selected element's own fields.\n *\n * The person names the component, the door reads the outline of what is\n * open and proposes which values become properties, and AGL-2906's own card\n * asks before anything happens. Applying creates the component through the\n * host resources route and swaps the selection for an instance of it, on the\n * open draft, as one undo step.\n *\n * ## Where it is drawn\n *\n * On a versioned besigner — a screen, component or layout, which is what\n * `assistEditDocumentOf` answers for — for an element the panel says this\n * editor may change in place (`editable`, AGL-2908), which is not the canvas\n * root and not locked layout chrome. Never on an instance: one is already a\n * component, and promoting it would make a component of a placeholder.\n *\n * The widget's own gates (`aiGenerative`, `ai.generate`) are the zone's, so a\n * reader without them never sees the button. The door decides again.\n */\n\nexport interface AiSaveAsComponentProps {\n /** The workspace the editor is in; the door gates and bills on it. */\n orgId?: string | null\n /** The site the editor names; the zone reads it from the editor's route. */\n hostId?: string | null\n /** The selected element, as the Attributes panel resolved it. */\n node?: NodeSchema<any> | null\n /** Whether this editor may change that element in place. */\n editable?: boolean\n}\n\n/** The name the promote dialog opens with: the element's own, else a plain one. */\nfunction suggestedName(node: NodeSchema<any> | null | undefined): string {\n const named = typeof (node as { name?: unknown })?.name === 'string' ? String((node as { name?: string }).name) : ''\n return named || String(node?.componentSchema?.displayName ?? 'Component')\n}\n\nexport function AiSaveAsComponent(props: AiSaveAsComponentProps) {\n const { orgId, hostId, node, editable } = props\n const { data: user } = useUser()\n const createHostResource = useHostResourceApi()\n const pathname = usePathname()\n const [name, setName] = useState<string | null>(null)\n const [busy, setBusy] = useState(false)\n const [proposal, setProposal] = useState<AssistEditProposal | null>(null)\n const [exchangeId, setExchangeId] = useState<string | null>(null)\n const [failure, setFailure] = useState<string | null>(null)\n\n const document = assistEditDocumentOf(pathname ?? '')\n\n const propose = useCallback(async () => {\n const asked = (name ?? '').trim()\n if (!asked || !hostId || !orgId) return\n setBusy(true)\n setFailure(null)\n try {\n const session = openEditorSession()\n const outline = session ? describeAssistEditCanvas(canvas, node?.$id ?? null) : null\n if (!outline) {\n setFailure('The editor did not describe what is open. Reopen it and try again.')\n return\n }\n const response = await authorizedFetch(user, '/api/ai/generate/component', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n orgId,\n hostId,\n route: pathname ?? '',\n canvas: outline,\n name: asked,\n }),\n })\n const answer = await response.json().catch(() => ({}))\n if (!response.ok || !isAssistEditProposal(answer?.edit)) {\n setFailure(String(answer?.error ?? 'The component could not be proposed.'))\n return\n }\n setProposal(answer.edit)\n setExchangeId(typeof answer.exchangeId === 'string' ? answer.exchangeId : null)\n } catch {\n setFailure('The component could not be proposed. Try again.')\n } finally {\n setBusy(false)\n }\n }, [name, hostId, orgId, node, pathname, user])\n\n const writer: AssistEditComponentWriter = {\n createComponent: async (input) => {\n // A block saved out of an email is an email block (AGL-3287), so it is\n // offered in emails and never on a page. Read off the element being\n // saved, because this panel sees the canvas and not the editor's view;\n // an email offers email blocks alone, so the two cannot disagree.\n const kind = reusableComponentKindForRoot(\n (input.nodes as Record<string, { pluginId?: unknown } | undefined>)[\n input.rootId\n ],\n )\n const created = await createHostResource({\n hostId: String(hostId),\n resource: 'reusableComponent',\n data: {\n displayName: input.name,\n rootId: input.rootId,\n nodes: input.nodes,\n ...(input.props.length ? { props: input.props } : {}),\n ...(kind === REUSABLE_COMPONENT_KIND_EMAIL ? { kind } : {}),\n },\n })\n return created.id\n },\n }\n\n const apply = useCallback(async () => {\n if (!proposal || !orgId) return\n setBusy(true)\n setFailure(null)\n try {\n const session = openEditorSession()\n const result = await applyAssistEditSavingComponent(canvas as never, proposal, session, writer)\n if (result.ok === false) {\n setFailure(result.message)\n return\n }\n setProposal(null)\n setName(null)\n if (!exchangeId || !session) return\n const report: AssistEditAppliedReport = {\n orgId,\n hostId: proposal.target.hostId,\n exchangeId,\n documentKind: proposal.target.kind,\n documentId: proposal.target.documentId,\n versionId: session.versionId,\n opCounts: result.opCounts,\n }\n // Fire and forget: the component exists and the canvas has changed, so\n // a record that fails to land must not read as a save that failed.\n void authorizedFetch(user, '/api/assist/edit-applied', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(report),\n }).catch(() => undefined)\n } finally {\n setBusy(false)\n }\n }, [proposal, orgId, exchangeId, user, writer])\n\n if (!document || !hostId || !orgId || !editable || !node) return null\n // An instance already follows a component; promoting it would make a\n // component of a placeholder.\n if (node.componentId === REUSABLE_INSTANCE_COMPONENT_ID) return null\n\n if (proposal) {\n return (\n <AssistEditCard\n proposal={proposal}\n brand={PLATFORM_BRAND_NAME}\n session={openEditorSession()}\n failure={failure}\n onApply={() => void apply()}\n onCreateVersion={() => openEditorSession()?.createVersion?.()}\n onDismiss={() => {\n setProposal(null)\n setFailure(null)\n }}\n />\n )\n }\n\n if (name === null) {\n return (\n <FormControl margin=\"none\" fullWidth>\n <Button color=\"primary\" fullWidth onClick={() => setName(suggestedName(node))}>\n {'Make a reusable component with AI'}\n </Button>\n </FormControl>\n )\n }\n\n return (\n <Stack spacing={1} sx={{ mt: 1 }}>\n <TextField\n size=\"small\"\n label=\"Component name\"\n value={name}\n onChange={(event) => setName(event.target.value)}\n disabled={busy}\n fullWidth\n />\n {failure ? <Alert severity=\"warning\">{failure}</Alert> : null}\n <Stack direction=\"row\" spacing={1}>\n <Button\n variant=\"contained\"\n size=\"small\"\n disabled={busy || !name.trim()}\n onClick={() => void propose()}\n startIcon={busy ? <CircularProgress size={14} /> : undefined}\n >\n {busy ? 'Reading the section…' : 'Suggest properties'}\n </Button>\n <Button size=\"small\" color=\"inherit\" disabled={busy} onClick={() => setName(null)}>\n {'Cancel'}\n </Button>\n </Stack>\n </Stack>\n )\n}\nAiSaveAsComponent.displayName = 'AiSaveAsComponent'\n\nexport default AiSaveAsComponent\n"],"names":["canvas","PLATFORM_BRAND_NAME","REUSABLE_COMPONENT_KIND_EMAIL","REUSABLE_INSTANCE_COMPONENT_ID","reusableComponentKindForRoot","openEditorSession","authorizedFetch","useHostResourceApi","useUser","Alert","Button","CircularProgress","FormControl","Stack","TextField","usePathname","useCallback","useState","assistEditDocumentOf","isAssistEditProposal","applyAssistEditSavingComponent","describeAssistEditCanvas","AssistEditCard","suggestedName","node","named","name","String","componentSchema","displayName","AiSaveAsComponent","props","orgId","hostId","editable","data","user","createHostResource","pathname","setName","busy","setBusy","proposal","setProposal","exchangeId","setExchangeId","failure","setFailure","document","propose","asked","trim","session","outline","$id","response","method","headers","body","JSON","stringify","route","answer","json","catch","ok","edit","error","writer","createComponent","input","kind","nodes","rootId","created","resource","length","id","apply","result","message","report","target","documentKind","documentId","versionId","opCounts","undefined","componentId","brand","onApply","onCreateVersion","createVersion","onDismiss","margin","fullWidth","color","onClick","spacing","sx","mt","size","label","value","onChange","event","disabled","severity","direction","variant","startIcon"],"mappings":"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"}
@@ -38,7 +38,9 @@ import { aiUnrestrictedPlanCapabilities } from "../model/ai-plan-capabilities.js
38
38
  'rootId',
39
39
  'props',
40
40
  'slug',
41
- 'seo'
41
+ 'seo',
42
+ // A component template's own kind (AGL-3287); a job never sends one.
43
+ 'componentKind'
42
44
  ],
43
45
  form: [
44
46
  'displayName',
@@ -50,12 +52,15 @@ import { aiUnrestrictedPlanCapabilities } from "../model/ai-plan-capabilities.js
50
52
  'rootId',
51
53
  'nodes'
52
54
  ],
55
+ // `kind` marks a reusable email block (AGL-3287); a job makes page
56
+ // components and never sends one.
53
57
  component: [
54
58
  'displayName',
55
59
  'description',
56
60
  'rootId',
57
61
  'nodes',
58
- 'props'
62
+ 'props',
63
+ 'kind'
59
64
  ],
60
65
  // `kind` is on the route's list for the email composer; a page has none, and
61
66
  // the writer never sends one.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-drafts.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 { createResourceUid } from '@aglyn/aglyn/app-utils/create-resource-uid'\nimport { uniqueDuplicateName } from '@aglyn/aglyn/app-utils/duplicate-resource'\nimport {\n FORM_COMPONENT_ID,\n normalizeFormSlug,\n type FormFieldDecl,\n type FormRouting,\n} from '@aglyn/aglyn/app-utils/forms'\nimport { nameSearchKey } from '@aglyn/aglyn/app-utils/name-search'\nimport {\n checkDatasetQuota,\n checkEntitlement,\n checkQuota,\n resolveEffectivePlan,\n resolveOrgEntitlements,\n} from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport {\n billableScreenIds,\n normalizeScreenSlug,\n reservedScreenRouteSegment,\n SCREEN_ROOT_PATH,\n} from '@aglyn/aglyn/app-utils/screen-route'\nimport { decodeStoredNodes, encodeStoredNodes } from '@aglyn/aglyn/app-utils/stored-nodes'\nimport type {\n AglynOrgBilling,\n OrgFeatureFlags,\n} from '@aglyn/aglyn/foundation/definitions/org-billing.types'\nimport type { ReusableComponentProp } from '@aglyn/aglyn/foundation/definitions/platform.types'\nimport type { NodesMap } from '@aglyn/aglyn/types/nodes'\nimport { resolveOrgIdForHost } from '@aglyn/tenant-data-admin/server/organizations'\nimport {\n aiUnrestrictedPlanCapabilities,\n type AiPlanCapabilities,\n type AiPlanCreation,\n} from '../model/ai-plan-capabilities'\nimport type { AiJobAdmissionRefusal } from './ai-job-admission'\n\n/**\n * Where a generation job's layout, page template, form, reusable component\n * or page lands (AGL-2909, AGL-2913, AGL-2908, AGL-2907): a new draft, made\n * as the console's host resources route makes one.\n *\n * ── The document the create route makes ─────────────────────────────────\n *\n * `AI_DRAFT_FIELDS` is that route's allow-list for each kind, and a spec\n * reads the route's source to hold the two together. Nothing outside the\n * list is written except the route's own stamps: `createdAt`, `updatedAt`,\n * `createdBy`, a screen's `nameLower`, and a template's `source`, which is\n * `authored` — a template a member's job generated is theirs, never a starter\n * or a listing. A layout's or a screen's first version carries the keys the\n * versions route seeds one with. A form is the document the Forms page's\n * Create sends: its design canvas-shaped under `rootId` with the form node\n * bound to the form's id and captioned with its name, a slug read from that\n * name, and no version — the form's page mints the first one when someone\n * opens it. Where Create sends an empty `fields`, a generated form sends the\n * declaration read off its own design, so the two agree from the start. A\n * component carries its tree and the props it declares on its own document,\n * as Use template writes one, and gets its first version where every\n * component does: when a member opens it.\n *\n * ── Counted like a create ────────────────────────────────────────────────\n *\n * The band is met INSIDE the transaction that writes (AGL-2231), with the\n * route's arithmetic: every layout document counts against\n * `sharedLayoutsPerHost`; a template counts against `templatesPerHost` when it\n * records a source other than a platform starter — the route's `!=` query,\n * which never matches a document with no `source.type` at all; a screen counts\n * against `screensPerHost` by `billableScreenIds`, over the screens and the\n * host's routing map, which counts an unrouted screen too (AGL-1445); and a\n * form needs the plan to include the route's `entitlement` first, refused in\n * the route's own words, after which every form document counts against\n * `formsPerHost`. A reusable component counts against no allowance at all:\n * the route admits one on a plan with the `reusableComponents` entitlement\n * and refuses it otherwise, and so does the writer.\n *\n * ── Applied to nothing ───────────────────────────────────────────────────\n *\n * A layout renders only around a screen or layout that names it, or as the\n * built-in page layout the host document names; a template is inert until a\n * member uses it; a form renders only where a page places it; a component\n * renders only where an instance places it, and a new one has no instance;\n * and a screen serves nothing until the host's routing map names it, which is\n * what publishing writes (`publishScreenRoute`). So the writer touches the new\n * documents and nothing else — no host document, no routing map, no\n * collection, store setting, other layout or component — and never a\n * `publishedAt` or a publish schedule. What the job hands back is a link to\n * the draft, never a binding.\n *\n * ── One draft per recorded id ────────────────────────────────────────────\n *\n * The caller names the document id: the console resource id the job recorded\n * for the draft before its first write (`ai-job-draft-ids.ts`). A step run\n * again after its draft was written — a process cut off before the machine\n * recorded the step — reads the same id, finds that draft and reports it,\n * rather than writing a second.\n */\n\ntype Firestore = FirebaseFirestore.Firestore\n\nexport type AiDraftKind = 'layout' | 'template' | 'form' | 'component' | 'screen'\n\n/** The draft kinds that keep their tree in versions, as the besigner opens them. */\nexport type AiVersionedDraftKind = Extract<AiDraftKind, 'layout' | 'screen'>\n\n/** The console host resources route's allow-list for each kind. */\nexport const AI_DRAFT_FIELDS: Readonly<Record<AiDraftKind, readonly string[]>> = {\n layout: ['displayName', 'description', 'versionId'],\n template: [\n 'kind',\n 'displayName',\n 'description',\n 'placeholders',\n 'nodes',\n 'rootId',\n 'props',\n 'slug',\n 'seo',\n ],\n form: [\n 'displayName',\n 'slug',\n 'fields',\n 'consentFieldName',\n 'routing',\n 'legacyMatch',\n 'rootId',\n 'nodes',\n ],\n component: ['displayName', 'description', 'rootId', 'nodes', 'props'],\n // `kind` is on the route's list for the email composer; a page has none, and\n // the writer never sends one.\n screen: ['displayName', 'description', 'slug', 'seo', 'kind', 'versionId'],\n}\n\n/** The keys a layout's first version is seeded with, all on the versions route's list. */\nexport const AI_DRAFT_VERSION_FIELDS: readonly string[] = ['layoutId', 'hostId', 'displayName', 'nodes']\n\n/**\n * The keys a screen's first version is seeded with, all on the versions\n * route's list: the screen it belongs to, the site, the label, the tree, and\n * the layout it renders inside when the plan names one the site has.\n */\nexport const AI_DRAFT_SCREEN_VERSION_FIELDS: readonly string[] = [\n 'screenId',\n 'hostId',\n 'displayName',\n 'nodes',\n 'layoutId',\n]\n\n/** The label on a generated draft's first version, as on one Use template makes. */\nexport const AI_DRAFT_VERSION_NAME = 'Initial version'\n\n/** The route's refusal when the site's plan does not include the kind at all. */\nexport const AI_DRAFT_ENTITLEMENT_REFUSAL = 'This feature is not included in your plan — see Billing'\n\nexport interface AiDraftBand {\n collection: 'layouts' | 'templates' | 'forms' | 'components' | 'screens'\n /** The allowance the route counts the kind against, where it counts one. */\n quotaKey?:\n | 'sharedLayoutsPerHost'\n | 'templatesPerHost'\n | 'formsPerHost'\n | 'screensPerHost'\n /** The feature the plan must include before any document of the kind counts. */\n entitlement?: keyof OrgFeatureFlags\n /** The route's plural label, in the refusal it gives. */\n label: string\n}\n\n/** Each kind's collection, counter, feature and label, as the create route declares them. */\nexport const AI_DRAFT_BANDS: Readonly<Record<AiDraftKind, AiDraftBand>> = {\n layout: { collection: 'layouts', quotaKey: 'sharedLayoutsPerHost', label: 'shared layouts' },\n template: { collection: 'templates', quotaKey: 'templatesPerHost', label: 'templates' },\n form: {\n collection: 'forms',\n quotaKey: 'formsPerHost',\n entitlement: 'reusableComponents',\n label: 'forms',\n },\n component: {\n collection: 'components',\n entitlement: 'reusableComponents',\n label: 'reusable components',\n },\n screen: { collection: 'screens', quotaKey: 'screensPerHost', label: 'screens' },\n}\n\ninterface SiblingRow {\n id: string\n name: string\n slug: string\n kind: unknown\n sourceType: unknown\n deletedAt: unknown\n deleted: boolean\n}\n\n/** A host's routing map: screen id → the path it is published at. */\ntype RoutingMap = Record<string, unknown> | null | undefined\n\nfunction draftCollection(firestore: Firestore, hostId: string, kind: AiDraftKind) {\n return firestore.collection('hosts').doc(hostId).collection(AI_DRAFT_BANDS[kind].collection)\n}\n\n/** The fields the band, the name, the address and a deletion are read from. */\nfunction siblingsOf(collection: FirebaseFirestore.CollectionReference) {\n return collection.select('displayName', 'slug', 'kind', 'source.type', 'deletedAt')\n}\n\nfunction siblingRows(snapshot: FirebaseFirestore.QuerySnapshot): SiblingRow[] {\n return snapshot.docs.map((doc) => ({\n id: doc.id,\n name: String(doc.get('displayName') ?? ''),\n slug: String(doc.get('slug') ?? ''),\n kind: doc.get('kind'),\n sourceType: doc.get('source.type'),\n deletedAt: doc.get('deletedAt'),\n deleted: doc.get('deletedAt') != null,\n }))\n}\n\nfunction subdomainOf(host: FirebaseFirestore.DocumentSnapshot): string | null {\n const subdomain = host.get('subdomain')\n return typeof subdomain === 'string' && subdomain ? subdomain : null\n}\n\n/**\n * The route's refusal when the site's plan does not include the kind, or\n * holds no more of it; `null` while it does. The feature is asked first, as\n * the route asks it before it counts.\n */\nexport function aiDraftBandRefusal(\n kind: AiDraftKind,\n rows: ReadonlyArray<Pick<SiblingRow, 'id' | 'kind' | 'sourceType' | 'deletedAt'>>,\n org: Partial<AglynOrgBilling> | null,\n routingMap?: RoutingMap,\n): string | null {\n const band = AI_DRAFT_BANDS[kind]\n if (band.entitlement && !checkEntitlement(org, band.entitlement)) {\n return AI_DRAFT_ENTITLEMENT_REFUSAL\n }\n if (!band.quotaKey) return null\n const used =\n kind === 'template'\n ? rows.filter((row) => row.sourceType !== undefined && row.sourceType !== 'starter').length\n : kind === 'screen'\n ? billableScreenIds(\n rows.map((row) => ({ id: row.id, kind: row.kind, deletedAt: row.deletedAt })),\n (routingMap ?? undefined) as never,\n ).size\n : rows.length\n const quota = checkQuota(org, band.quotaKey, used)\n return quota.allowed\n ? null\n : `Your plan includes ${quota.limit} ${band.label} — upgrade in Billing for more`\n}\n\n/** The draft kinds a plan's creations land as, whose band a plan is told about (AGL-3030). */\nconst AI_PLAN_DRAFT_KINDS = ['component', 'form', 'layout', 'template'] as const\n\ntype AiPlanDraftKind = (typeof AI_PLAN_DRAFT_KINDS)[number]\n\n/** Every draft kind's plural label made singular, for a band of one. */\nfunction bandLabel(label: string, count: number): string {\n return count === 1 ? label.replace(/s$/, '') : label\n}\n\n/**\n * Whether the site may take `used + 1` of a kind, in the create route's\n * arithmetic: the feature first, then the allowance. `null` for a kind whose\n * allowance is unlimited or counts nothing, which needs no rows read.\n */\nfunction planCreation(\n kind: AiPlanDraftKind,\n org: Partial<AglynOrgBilling> | null,\n rows: ReadonlyArray<Pick<SiblingRow, 'id' | 'kind' | 'sourceType' | 'deletedAt'>> | null,\n): AiPlanCreation {\n const band = AI_DRAFT_BANDS[kind]\n if (band.entitlement && !checkEntitlement(org, band.entitlement)) {\n return {\n allowed: false,\n left: 0,\n reason: `this workspace's plan does not include ${kind === 'form' ? 'saved forms' : band.label}`,\n }\n }\n const limit = band.quotaKey ? resolveOrgEntitlements(org)[band.quotaKey] : Number.POSITIVE_INFINITY\n if (!rows || !Number.isFinite(limit)) return { allowed: true, left: null, reason: null }\n // The route's own arithmetic decides; the count beside it is what is left.\n const refusal = aiDraftBandRefusal(kind, rows, org)\n const used =\n kind === 'template'\n ? rows.filter((row) => row.sourceType !== undefined && row.sourceType !== 'starter').length\n : rows.length\n return refusal\n ? {\n allowed: false,\n left: 0,\n reason: `this site already holds the ${limit} ${bandLabel(band.label, limit)} its plan includes`,\n }\n : { allowed: true, left: Math.max(0, limit - used), reason: null }\n}\n\n/** Whether a kind's rows are counted at all on this plan: included, and under a finite allowance. */\nfunction countsRows(kind: AiPlanDraftKind, org: Partial<AglynOrgBilling> | null): boolean {\n const band = AI_DRAFT_BANDS[kind]\n if (!band.quotaKey) return false\n if (band.entitlement && !checkEntitlement(org, band.entitlement)) return false\n return Number.isFinite(resolveOrgEntitlements(org)[band.quotaKey])\n}\n\n/**\n * What a plan may create on this site (AGL-3030), from the workspace's plan\n * and the rows each counted kind already holds — pure, so the specs and the\n * eval harness hold the arithmetic without a Firestore. A kind whose rows\n * are not given is counted as having room, which is what an unlimited\n * allowance means.\n *\n * A theme change is a proposal a member applies, never a draft, so it counts\n * against nothing. A dataset is org data that no job writes; a plan may name\n * one only where the workspace's plan includes datasets at all. An email\n * design is written by the email plugin, whose own gate decides.\n *\n * A workspace on the Free plan spends the Free taste, a wall of monthly\n * credits, so its plan is held to the sections that wall pays for (AGL-3070).\n * The effective plan decides, as it decides the taste's own allowance: a\n * workspace whose subscription died is Free again.\n */\nexport function aiPlanCapabilitiesFrom(\n org: Partial<AglynOrgBilling> | null,\n rows: Partial<Record<AiPlanDraftKind, ReadonlyArray<Pick<SiblingRow, 'id' | 'kind' | 'sourceType' | 'deletedAt'>>>> = {},\n): AiPlanCapabilities {\n const unrestricted = aiUnrestrictedPlanCapabilities()\n const datasets = checkDatasetQuota(org, 0).limit > 0\n return {\n reusableComponents: checkEntitlement(org, 'reusableComponents'),\n create: {\n ...unrestricted.create,\n component: planCreation('component', org, rows.component ?? null),\n form: planCreation('form', org, rows.form ?? null),\n layout: planCreation('layout', org, rows.layout ?? null),\n template: planCreation('template', org, rows.template ?? null),\n dataset: datasets\n ? unrestricted.create.dataset\n : { allowed: false, left: 0, reason: \"this workspace's plan does not include datasets\" },\n },\n ...(resolveEffectivePlan(org) === 'free' ? { freeTaste: true } : {}),\n }\n}\n\n/**\n * What a plan may create on this site, read: the rows of each kind whose\n * allowance the workspace's plan counts, as the draft writer reads them, and\n * nothing for a kind the plan leaves out or leaves unlimited.\n */\nexport async function readAiPlanCapabilities(\n firestore: Firestore,\n input: { hostId: string; org: Partial<AglynOrgBilling> | null },\n): Promise<AiPlanCapabilities> {\n const counted = AI_PLAN_DRAFT_KINDS.filter((kind) => countsRows(kind, input.org))\n const snapshots = await Promise.all(\n counted.map((kind) => siblingsOf(draftCollection(firestore, input.hostId, kind)).get()),\n )\n return aiPlanCapabilitiesFrom(\n input.org,\n Object.fromEntries(counted.map((kind, index) => [kind, siblingRows(snapshots[index])])),\n )\n}\n\n/**\n * Whether the site can take one more draft of the kind: a read, for a door\n * that asks before it spends. The write asks again inside its transaction.\n */\nexport async function aiDraftAllowanceRefusal(\n firestore: Firestore,\n input: { kind: AiDraftKind; hostId: string; org: Partial<AglynOrgBilling> | null },\n): Promise<string | null> {\n const [snapshot, host] = await Promise.all([\n siblingsOf(draftCollection(firestore, input.hostId, input.kind)).get(),\n input.kind === 'screen'\n ? firestore.collection('hosts').doc(input.hostId).get()\n : Promise.resolve(null),\n ])\n return aiDraftBandRefusal(\n input.kind,\n siblingRows(snapshot),\n input.org,\n host?.get('screens') as RoutingMap,\n )\n}\n\n/**\n * A draft screen's one-segment address: the one asked for, else one from its\n * name, never a reserved segment, and never an address a live screen keeps\n * or the routing map already serves — so the member's publish at it later\n * takes nothing from a live page. The root is asked for only while no screen\n * holds it.\n */\nexport function aiDraftScreenSlug(\n requested: string | null | undefined,\n name: string,\n rows: ReadonlyArray<Pick<SiblingRow, 'slug' | 'deleted'>>,\n routingMap: RoutingMap,\n): string {\n const taken = new Set<string>([\n ...rows.filter((row) => !row.deleted && row.slug).map((row) => row.slug),\n ...Object.values(routingMap ?? {}).filter((path): path is string => typeof path === 'string'),\n ])\n const usable = (value: string | undefined) =>\n value && !reservedScreenRouteSegment(value) && !(value === SCREEN_ROOT_PATH && taken.has(value))\n ? value\n : undefined\n const base = usable(normalizeScreenSlug(requested ?? undefined)) ?? usable(normalizeScreenSlug(name)) ?? 'page'\n if (base === SCREEN_ROOT_PATH) return base\n let slug = base\n let attempt = 2\n while (taken.has(slug)) slug = `${base}-${attempt++}`\n return slug\n}\n\nexport interface AiDraftRecord {\n id: string\n /** A layout's or a screen's first version; `null` for every other kind, which the writer gives none. */\n versionId: string | null\n name: string\n hostSubdomain: string | null\n /** A screen's search listing as the draft holds it now; absent on other kinds. */\n seo?: { title?: string; description?: string } | null\n /** Whether a member deleted the draft since it was written. */\n deleted?: boolean\n}\n\nfunction recordOf(\n id: string,\n draft: FirebaseFirestore.DocumentSnapshot,\n host: FirebaseFirestore.DocumentSnapshot,\n): AiDraftRecord {\n const versionId = draft.get('versionId')\n const seo = draft.get('seo')\n return {\n id,\n versionId: typeof versionId === 'string' && versionId ? versionId : null,\n name: String(draft.get('displayName') ?? ''),\n hostSubdomain: subdomainOf(host),\n ...(seo && typeof seo === 'object' ? { seo: seo as AiDraftRecord['seo'] } : {}),\n ...(draft.get('deletedAt') != null ? { deleted: true } : {}),\n }\n}\n\n/** The draft a job already wrote under `id`, when a run of its step got that far. */\nexport async function readAiDraft(\n firestore: Firestore,\n input: { kind: AiDraftKind; hostId: string; id: string },\n): Promise<AiDraftRecord | null> {\n const draft = await draftCollection(firestore, input.hostId, input.kind).doc(input.id).get()\n if (!draft.exists) return null\n return recordOf(input.id, draft, await firestore.collection('hosts').doc(input.hostId).get())\n}\n\n/**\n * The tree a template draft holds now, decoded; `null` when there is none.\n *\n * A template is not a versioned draft — its nodes live on the document\n * itself, which is why `readAiDraftNodes` refuses its kind.\n */\nexport async function readAiTemplateDraftNodes(\n firestore: Firestore,\n input: { hostId: string; id: string },\n): Promise<NodesMap | null> {\n const draft = await draftCollection(firestore, input.hostId, 'template').doc(input.id).get()\n if (!draft.exists) return null\n return decodeStoredNodes<NodesMap>(draft.get('nodes')) ?? null\n}\n\n/**\n * The property names a component draft declares now; `null` when there is no\n * such draft. Read from the draft document rather than its version, because a\n * component's props live beside its tree and a copy carries its source's.\n */\nexport async function readAiDraftComponentProps(\n firestore: Firestore,\n input: { hostId: string; id: string },\n): Promise<string[] | null> {\n const draft = await draftCollection(firestore, input.hostId, 'component').doc(input.id).get()\n if (!draft.exists) return null\n const props = draft.get('props')\n if (!Array.isArray(props)) return []\n return props\n .map((prop) => (prop && typeof prop === 'object' ? (prop as Record<string, unknown>)['name'] : null))\n .filter((name): name is string => typeof name === 'string' && name.length > 0)\n}\n\n/** The tree a versioned draft's first version holds now, decoded; `null` when there is none. */\nexport async function readAiDraftNodes(\n firestore: Firestore,\n input: { kind: AiVersionedDraftKind; hostId: string; id: string },\n): Promise<{ versionId: string; nodes: NodesMap } | null> {\n const draftRef = draftCollection(firestore, input.hostId, input.kind).doc(input.id)\n const draft = await draftRef.get()\n const versionId = draft.exists ? draft.get('versionId') : null\n if (typeof versionId !== 'string' || !versionId) return null\n const version = await draftRef.collection('versions').doc(versionId).get()\n const nodes = version.exists ? decodeStoredNodes<NodesMap>(version.get('nodes')) : null\n return nodes ? { versionId, nodes } : null\n}\n\nexport interface AiDraftInput {\n kind: AiDraftKind\n hostId: string\n /** The draft's document id: the id the job recorded for this draft. */\n id: string\n /** Whose work the draft is: the job's creator, whose brief it built. */\n uid: string\n org: Partial<AglynOrgBilling> | null\n /** The name asked for; a live sibling already called that gets a number. */\n name: string\n nodes: NodesMap\n /** A page template's suggested address, or a screen's one-segment address. */\n slug?: string | null\n /** A form's declaration and the ids its design hangs from; required for a form. */\n form?: AiFormDraftDeclaration\n /** A component's root, which the node map holds; required for a component. */\n rootId?: string\n /** The properties a component declares, which its tree binds. */\n props?: readonly ReusableComponentProp[]\n /** A screen's layout, for its first version to render inside; a layout the site has. */\n layoutId?: string | null\n now: Date\n}\n\n/** What a form draft declares beside its design, read off that design. */\nexport interface AiFormDraftDeclaration {\n /** The canvas root `nodes` is stored under, as the Forms page's Create stores it. */\n rootId: string\n /** The form node, which carries the form's id and is captioned with the draft's name. */\n formNodeId: string\n fields: FormFieldDecl[]\n consentFieldName: string | null\n routing: FormRouting | null\n}\n\nexport type AiDraftWrite =\n | (AiDraftRecord & { ok: true; replayed: boolean })\n | { ok: false; status: 403 | 404; error: string }\n\n/** Keep only the keys the create route admits for the kind. */\nfunction allowListed(kind: AiDraftKind, data: Record<string, unknown>): Record<string, unknown> {\n const allowed = new Set(AI_DRAFT_FIELDS[kind])\n return Object.fromEntries(\n Object.entries(data).filter(([key, value]) => allowed.has(key) && value !== undefined),\n )\n}\n\n/** The node map with its form node captioned with `name`, as Create captions a new form. */\nfunction withFormCaption(nodes: NodesMap, formNodeId: string, name: string): NodesMap {\n const node = nodes[formNodeId]\n if (!node || node.componentId !== FORM_COMPONENT_ID) return nodes\n return { ...nodes, [formNodeId]: { ...node, props: { ...(node.props ?? {}), formName: name } } }\n}\n\n/**\n * Write one draft: the band met, the name made unique among the live\n * siblings, the node map stored as msgpack, all in one transaction. A\n * refusal is returned, never thrown; a Firestore failure propagates, so the\n * machine's retry applies.\n */\nexport async function writeAiDraft(firestore: Firestore, input: AiDraftInput): Promise<AiDraftWrite> {\n const packed = encodeStoredNodes(input.nodes)\n if (!packed) throw new Error('an AI draft needs a node map')\n if (input.kind === 'form' && !input.form) throw new Error('an AI form draft needs its declaration')\n if (input.kind === 'component' && !(input.rootId && input.nodes[input.rootId])) {\n throw new Error('a component draft needs a root its node map holds')\n }\n const hostRef = firestore.collection('hosts').doc(input.hostId)\n const collection = draftCollection(firestore, input.hostId, input.kind)\n const draftRef = collection.doc(input.id)\n return firestore.runTransaction(async (tx): Promise<AiDraftWrite> => {\n // Every read before any write, which Firestore requires.\n const [host, existing, siblings] = await Promise.all([\n tx.get(hostRef),\n tx.get(draftRef),\n tx.get(siblingsOf(collection)),\n ])\n if (!host.exists) return { ok: false, status: 404, error: 'Unknown site' }\n if (existing.exists) {\n return { ok: true, replayed: true, ...recordOf(input.id, existing, host) }\n }\n const rows = siblingRows(siblings)\n // Read, never written: the routing map decides which screens count.\n const routingMap = host.get('screens') as RoutingMap\n const refusal = aiDraftBandRefusal(input.kind, rows, input.org, routingMap)\n if (refusal) return { ok: false, status: 403, error: refusal }\n const name = uniqueDuplicateName(\n input.name,\n rows.filter((row) => !row.deleted).map((row) => row.name),\n )\n const stamps = { createdAt: input.now, updatedAt: input.now, createdBy: input.uid }\n const nodes = Buffer.from(packed)\n let versionId: string | null = null\n if (input.kind === 'layout') {\n versionId = createResourceUid()\n tx.create(draftRef, { ...allowListed('layout', { displayName: name, versionId }), ...stamps })\n tx.create(draftRef.collection('versions').doc(versionId), {\n layoutId: input.id,\n hostId: input.hostId,\n displayName: AI_DRAFT_VERSION_NAME,\n nodes,\n ...stamps,\n })\n } else if (input.kind === 'screen') {\n versionId = createResourceUid()\n const slug = aiDraftScreenSlug(input.slug, name, rows, routingMap)\n tx.create(draftRef, {\n ...allowListed('screen', { displayName: name, slug, versionId }),\n nameLower: nameSearchKey(name),\n ...stamps,\n })\n tx.create(draftRef.collection('versions').doc(versionId), {\n screenId: input.id,\n hostId: input.hostId,\n displayName: AI_DRAFT_VERSION_NAME,\n nodes,\n ...(input.layoutId ? { layoutId: input.layoutId } : {}),\n ...stamps,\n })\n } else if (input.kind === 'template') {\n tx.create(draftRef, {\n ...allowListed('template', {\n kind: 'page',\n displayName: name,\n nodes,\n slug: input.slug || undefined,\n }),\n source: { type: 'authored' },\n ...stamps,\n })\n } else if (input.kind === 'component') {\n tx.create(draftRef, {\n ...allowListed('component', {\n displayName: name,\n rootId: input.rootId,\n nodes,\n props: input.props?.length ? [...input.props] : undefined,\n }),\n ...stamps,\n })\n } else {\n const form = input.form as AiFormDraftDeclaration\n const captioned = encodeStoredNodes(withFormCaption(input.nodes, form.formNodeId, name)) ?? packed\n tx.create(draftRef, {\n ...allowListed('form', {\n displayName: name,\n slug: normalizeFormSlug(name) || input.id,\n fields: form.fields,\n consentFieldName: form.consentFieldName || undefined,\n routing: form.routing ?? undefined,\n rootId: form.rootId,\n nodes: Buffer.from(captioned),\n }),\n ...stamps,\n })\n }\n return {\n ok: true,\n replayed: false,\n id: input.id,\n versionId,\n name,\n hostSubdomain: subdomainOf(host),\n }\n })\n}\n\nexport type AiDraftNodesUpdate =\n | { ok: true; changed: boolean }\n | { ok: false; status: 404; error: string }\n\n/**\n * Change the tree of a versioned draft the job wrote, in one transaction:\n * read the first version as it is stored NOW — a member may have opened and\n * saved it — hand it to `update`, and store what comes back with a fresh\n * `updatedAt`, which is what the besigner's save guard compares. `update`\n * answering `null` changes nothing: a pass run again after its write.\n */\nexport async function updateAiDraftNodes(\n firestore: Firestore,\n input: {\n kind: AiVersionedDraftKind\n hostId: string\n id: string\n now: Date\n update: (nodes: NodesMap) => NodesMap | null\n },\n): Promise<AiDraftNodesUpdate> {\n const draftRef = draftCollection(firestore, input.hostId, input.kind).doc(input.id)\n return firestore.runTransaction(async (tx): Promise<AiDraftNodesUpdate> => {\n const draft = await tx.get(draftRef)\n const versionId = draft.exists && draft.get('deletedAt') == null ? draft.get('versionId') : null\n if (typeof versionId !== 'string' || !versionId) {\n return { ok: false, status: 404, error: 'The draft is no longer on the site' }\n }\n const versionRef = draftRef.collection('versions').doc(versionId)\n const version = await tx.get(versionRef)\n const nodes = version.exists ? decodeStoredNodes<NodesMap>(version.get('nodes')) : null\n if (!nodes) return { ok: false, status: 404, error: 'The draft is no longer on the site' }\n const next = input.update(nodes)\n if (!next) return { ok: true, changed: false }\n const packed = encodeStoredNodes(next)\n if (!packed) throw new Error('an AI draft needs a node map')\n tx.update(versionRef, { nodes: Buffer.from(packed), updatedAt: input.now })\n return { ok: true, changed: true }\n })\n}\n\n/**\n * Put a search title and description on a draft screen the job wrote, each\n * only where the draft has none: a value a member typed since is theirs.\n */\nexport async function writeAiDraftScreenSeo(\n firestore: Firestore,\n input: {\n hostId: string\n id: string\n seo: { title?: string | null; description?: string | null }\n now: Date\n },\n): Promise<void> {\n const ref = draftCollection(firestore, input.hostId, 'screen').doc(input.id)\n await firestore.runTransaction(async (tx) => {\n const screen = await tx.get(ref)\n if (!screen.exists || screen.get('deletedAt') != null) return\n const patch: Record<string, unknown> = {}\n for (const key of ['title', 'description'] as const) {\n const value = String(input.seo[key] ?? '').trim()\n if (value && !String(screen.get(`seo.${key}`) ?? '').trim()) patch[`seo.${key}`] = value\n }\n if (Object.keys(patch).length) tx.update(ref, { ...patch, updatedAt: input.now })\n })\n}\n\n/** The site's subdomain, which a console link names the site by. */\nexport async function aiSiteSubdomain(firestore: Firestore, hostId: string): Promise<string | null> {\n return subdomainOf(await firestore.collection('hosts').doc(hostId).get())\n}\n\n/**\n * What a door says before a job that writes a draft of this kind exists: a\n * site is named, and it is the job's own org's; whatever the kind checks of\n * its own; then the allowance. `null` admits the job.\n */\nexport async function aiDraftAdmissionRefusal(\n firestore: Firestore,\n input: {\n orgId: string\n hostId: string | null\n kind: AiDraftKind\n /** What the refusal calls the job's output; the kind when absent. */\n noun?: string\n org: object | null\n /** The kind's own check, asked once the site is known to be the org's. */\n ownCheck?: (hostId: string) => Promise<AiJobAdmissionRefusal | null>\n },\n): Promise<AiJobAdmissionRefusal | null> {\n if (!input.hostId) {\n return {\n status: 400,\n error: `Open the site the ${input.noun ?? input.kind} is for before starting the job`,\n }\n }\n const owner = await resolveOrgIdForHost(input.hostId)\n if (!owner || owner !== input.orgId) return { status: 404, error: 'Unknown site' }\n const own = await input.ownCheck?.(input.hostId)\n if (own) return own\n const refusal = await aiDraftAllowanceRefusal(firestore, {\n kind: input.kind,\n hostId: input.hostId,\n org: input.org as Partial<AglynOrgBilling> | null,\n })\n return refusal ? { status: 403, error: refusal } : null\n}\n"],"names":["createResourceUid","uniqueDuplicateName","FORM_COMPONENT_ID","normalizeFormSlug","nameSearchKey","checkDatasetQuota","checkEntitlement","checkQuota","resolveEffectivePlan","resolveOrgEntitlements","billableScreenIds","normalizeScreenSlug","reservedScreenRouteSegment","SCREEN_ROOT_PATH","decodeStoredNodes","encodeStoredNodes","resolveOrgIdForHost","aiUnrestrictedPlanCapabilities","AI_DRAFT_FIELDS","layout","template","form","component","screen","AI_DRAFT_VERSION_FIELDS","AI_DRAFT_SCREEN_VERSION_FIELDS","AI_DRAFT_VERSION_NAME","AI_DRAFT_ENTITLEMENT_REFUSAL","AI_DRAFT_BANDS","collection","quotaKey","label","entitlement","draftCollection","firestore","hostId","kind","doc","siblingsOf","select","siblingRows","snapshot","docs","map","id","name","String","get","slug","sourceType","deletedAt","deleted","subdomainOf","host","subdomain","aiDraftBandRefusal","rows","org","routingMap","band","used","filter","row","undefined","length","size","quota","allowed","limit","AI_PLAN_DRAFT_KINDS","bandLabel","count","replace","planCreation","left","reason","Number","POSITIVE_INFINITY","isFinite","refusal","Math","max","countsRows","aiPlanCapabilitiesFrom","unrestricted","datasets","reusableComponents","create","dataset","freeTaste","readAiPlanCapabilities","input","counted","snapshots","Promise","all","Object","fromEntries","index","aiDraftAllowanceRefusal","resolve","aiDraftScreenSlug","requested","usable","taken","Set","values","path","value","has","base","attempt","recordOf","draft","versionId","seo","hostSubdomain","readAiDraft","exists","readAiTemplateDraftNodes","readAiDraftComponentProps","props","Array","isArray","prop","readAiDraftNodes","draftRef","version","nodes","allowListed","data","entries","key","withFormCaption","formNodeId","node","componentId","formName","writeAiDraft","packed","Error","rootId","hostRef","runTransaction","tx","existing","siblings","ok","status","error","replayed","stamps","createdAt","now","updatedAt","createdBy","uid","Buffer","from","displayName","layoutId","nameLower","screenId","source","type","captioned","fields","consentFieldName","routing","updateAiDraftNodes","versionRef","next","update","changed","writeAiDraftScreenSeo","ref","patch","trim","keys","aiSiteSubdomain","aiDraftAdmissionRefusal","noun","owner","orgId","own","ownCheck"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,iBAAiB,QAAQ,6CAA4C;AAC9E,SAASC,mBAAmB,QAAQ,4CAA2C;AAC/E,SACEC,iBAAiB,EACjBC,iBAAiB,QAGZ,+BAA8B;AACrC,SAASC,aAAa,QAAQ,qCAAoC;AAClE,SACEC,iBAAiB,EACjBC,gBAAgB,EAChBC,UAAU,EACVC,oBAAoB,EACpBC,sBAAsB,QACjB,2CAA0C;AACjD,SACEC,iBAAiB,EACjBC,mBAAmB,EACnBC,0BAA0B,EAC1BC,gBAAgB,QACX,sCAAqC;AAC5C,SAASC,iBAAiB,EAAEC,iBAAiB,QAAQ,sCAAqC;AAO1F,SAASC,mBAAmB,QAAQ,gDAA+C;AACnF,SACEC,8BAA8B,QAGzB,mCAA+B;AAsEtC,iEAAiE,GACjE,OAAO,MAAMC,kBAAoE;IAC/EC,QAAQ;QAAC;QAAe;QAAe;KAAY;IACnDC,UAAU;QACR;QACA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;KACD;IACDC,MAAM;QACJ;QACA;QACA;QACA;QACA;QACA;QACA;QACA;KACD;IACDC,WAAW;QAAC;QAAe;QAAe;QAAU;QAAS;KAAQ;IACrE,6EAA6E;IAC7E,8BAA8B;IAC9BC,QAAQ;QAAC;QAAe;QAAe;QAAQ;QAAO;QAAQ;KAAY;AAC5E,EAAC;AAED,wFAAwF,GACxF,OAAO,MAAMC,0BAA6C;IAAC;IAAY;IAAU;IAAe;CAAQ,CAAA;AAExG;;;;CAIC,GACD,OAAO,MAAMC,iCAAoD;IAC/D;IACA;IACA;IACA;IACA;CACD,CAAA;AAED,kFAAkF,GAClF,OAAO,MAAMC,wBAAwB,kBAAiB;AAEtD,+EAA+E,GAC/E,OAAO,MAAMC,+BAA+B,0DAAyD;AAgBrG,2FAA2F,GAC3F,OAAO,MAAMC,iBAA6D;IACxET,QAAQ;QAAEU,YAAY;QAAWC,UAAU;QAAwBC,OAAO;IAAiB;IAC3FX,UAAU;QAAES,YAAY;QAAaC,UAAU;QAAoBC,OAAO;IAAY;IACtFV,MAAM;QACJQ,YAAY;QACZC,UAAU;QACVE,aAAa;QACbD,OAAO;IACT;IACAT,WAAW;QACTO,YAAY;QACZG,aAAa;QACbD,OAAO;IACT;IACAR,QAAQ;QAAEM,YAAY;QAAWC,UAAU;QAAkBC,OAAO;IAAU;AAChF,EAAC;AAeD,SAASE,gBAAgBC,SAAoB,EAAEC,MAAc,EAAEC,IAAiB;IAC9E,OAAOF,UAAUL,UAAU,CAAC,SAASQ,GAAG,CAACF,QAAQN,UAAU,CAACD,cAAc,CAACQ,KAAK,CAACP,UAAU;AAC7F;AAEA,6EAA6E,GAC7E,SAASS,WAAWT,UAAiD;IACnE,OAAOA,WAAWU,MAAM,CAAC,eAAe,QAAQ,QAAQ,eAAe;AACzE;AAEA,SAASC,YAAYC,QAAyC;IAC5D,OAAOA,SAASC,IAAI,CAACC,GAAG,CAAC,CAACN;YAEXA,UACAA;eAHoB;YACjCO,IAAIP,IAAIO,EAAE;YACVC,MAAMC,QAAOT,WAAAA,IAAIU,GAAG,CAAC,0BAARV,WAA0B;YACvCW,MAAMF,QAAOT,YAAAA,IAAIU,GAAG,CAAC,mBAARV,YAAmB;YAChCD,MAAMC,IAAIU,GAAG,CAAC;YACdE,YAAYZ,IAAIU,GAAG,CAAC;YACpBG,WAAWb,IAAIU,GAAG,CAAC;YACnBI,SAASd,IAAIU,GAAG,CAAC,gBAAgB;QACnC;;AACF;AAEA,SAASK,YAAYC,IAAwC;IAC3D,MAAMC,YAAYD,KAAKN,GAAG,CAAC;IAC3B,OAAO,OAAOO,cAAc,YAAYA,YAAYA,YAAY;AAClE;AAEA;;;;CAIC,GACD,OAAO,SAASC,mBACdnB,IAAiB,EACjBoB,IAAiF,EACjFC,GAAoC,EACpCC,UAAuB;IAEvB,MAAMC,OAAO/B,cAAc,CAACQ,KAAK;IACjC,IAAIuB,KAAK3B,WAAW,IAAI,CAAC1B,iBAAiBmD,KAAKE,KAAK3B,WAAW,GAAG;QAChE,OAAOL;IACT;IACA,IAAI,CAACgC,KAAK7B,QAAQ,EAAE,OAAO;IAC3B,MAAM8B,OACJxB,SAAS,aACLoB,KAAKK,MAAM,CAAC,CAACC,MAAQA,IAAIb,UAAU,KAAKc,aAAaD,IAAIb,UAAU,KAAK,WAAWe,MAAM,GACzF5B,SAAS,WACP1B,kBACE8C,KAAKb,GAAG,CAAC,CAACmB,MAAS,CAAA;YAAElB,IAAIkB,IAAIlB,EAAE;YAAER,MAAM0B,IAAI1B,IAAI;YAAEc,WAAWY,IAAIZ,SAAS;QAAC,CAAA,IACzEQ,qBAAAA,aAAcK,WACfE,IAAI,GACNT,KAAKQ,MAAM;IACnB,MAAME,QAAQ3D,WAAWkD,KAAKE,KAAK7B,QAAQ,EAAE8B;IAC7C,OAAOM,MAAMC,OAAO,GAChB,OACA,CAAC,mBAAmB,EAAED,MAAME,KAAK,CAAC,CAAC,EAAET,KAAK5B,KAAK,CAAC,8BAA8B,CAAC;AACrF;AAEA,4FAA4F,GAC5F,MAAMsC,sBAAsB;IAAC;IAAa;IAAQ;IAAU;CAAW;AAIvE,sEAAsE,GACtE,SAASC,UAAUvC,KAAa,EAAEwC,KAAa;IAC7C,OAAOA,UAAU,IAAIxC,MAAMyC,OAAO,CAAC,MAAM,MAAMzC;AACjD;AAEA;;;;CAIC,GACD,SAAS0C,aACPrC,IAAqB,EACrBqB,GAAoC,EACpCD,IAAwF;IAExF,MAAMG,OAAO/B,cAAc,CAACQ,KAAK;IACjC,IAAIuB,KAAK3B,WAAW,IAAI,CAAC1B,iBAAiBmD,KAAKE,KAAK3B,WAAW,GAAG;QAChE,OAAO;YACLmC,SAAS;YACTO,MAAM;YACNC,QAAQ,CAAC,uCAAuC,EAAEvC,SAAS,SAAS,gBAAgBuB,KAAK5B,KAAK,EAAE;QAClG;IACF;IACA,MAAMqC,QAAQT,KAAK7B,QAAQ,GAAGrB,uBAAuBgD,IAAI,CAACE,KAAK7B,QAAQ,CAAC,GAAG8C,OAAOC,iBAAiB;IACnG,IAAI,CAACrB,QAAQ,CAACoB,OAAOE,QAAQ,CAACV,QAAQ,OAAO;QAAED,SAAS;QAAMO,MAAM;QAAMC,QAAQ;IAAK;IACvF,2EAA2E;IAC3E,MAAMI,UAAUxB,mBAAmBnB,MAAMoB,MAAMC;IAC/C,MAAMG,OACJxB,SAAS,aACLoB,KAAKK,MAAM,CAAC,CAACC,MAAQA,IAAIb,UAAU,KAAKc,aAAaD,IAAIb,UAAU,KAAK,WAAWe,MAAM,GACzFR,KAAKQ,MAAM;IACjB,OAAOe,UACH;QACEZ,SAAS;QACTO,MAAM;QACNC,QAAQ,CAAC,4BAA4B,EAAEP,MAAM,CAAC,EAAEE,UAAUX,KAAK5B,KAAK,EAAEqC,OAAO,kBAAkB,CAAC;IAClG,IACA;QAAED,SAAS;QAAMO,MAAMM,KAAKC,GAAG,CAAC,GAAGb,QAAQR;QAAOe,QAAQ;IAAK;AACrE;AAEA,mGAAmG,GACnG,SAASO,WAAW9C,IAAqB,EAAEqB,GAAoC;IAC7E,MAAME,OAAO/B,cAAc,CAACQ,KAAK;IACjC,IAAI,CAACuB,KAAK7B,QAAQ,EAAE,OAAO;IAC3B,IAAI6B,KAAK3B,WAAW,IAAI,CAAC1B,iBAAiBmD,KAAKE,KAAK3B,WAAW,GAAG,OAAO;IACzE,OAAO4C,OAAOE,QAAQ,CAACrE,uBAAuBgD,IAAI,CAACE,KAAK7B,QAAQ,CAAC;AACnE;AAEA;;;;;;;;;;;;;;;;CAgBC,GACD,OAAO,SAASqD,uBACd1B,GAAoC,EACpCD,OAAsH,CAAC,CAAC;QAQ1EA,iBACVA,YACIA,cACIA;IAT5C,MAAM4B,eAAenE;IACrB,MAAMoE,WAAWhF,kBAAkBoD,KAAK,GAAGW,KAAK,GAAG;IACnD,OAAO;QACLkB,oBAAoBhF,iBAAiBmD,KAAK;QAC1C8B,QAAQ,aACHH,aAAaG,MAAM;YACtBjE,WAAWmD,aAAa,aAAahB,MAAKD,kBAAAA,KAAKlC,SAAS,YAAdkC,kBAAkB;YAC5DnC,MAAMoD,aAAa,QAAQhB,MAAKD,aAAAA,KAAKnC,IAAI,YAATmC,aAAa;YAC7CrC,QAAQsD,aAAa,UAAUhB,MAAKD,eAAAA,KAAKrC,MAAM,YAAXqC,eAAe;YACnDpC,UAAUqD,aAAa,YAAYhB,MAAKD,iBAAAA,KAAKpC,QAAQ,YAAboC,iBAAiB;YACzDgC,SAASH,WACLD,aAAaG,MAAM,CAACC,OAAO,GAC3B;gBAAErB,SAAS;gBAAOO,MAAM;gBAAGC,QAAQ;YAAkD;;OAEvFnE,qBAAqBiD,SAAS,SAAS;QAAEgC,WAAW;IAAK,IAAI,CAAC;AAEtE;AAEA;;;;CAIC,GACD,OAAO,eAAeC,uBACpBxD,SAAoB,EACpByD,KAA+D;IAE/D,MAAMC,UAAUvB,oBAAoBR,MAAM,CAAC,CAACzB,OAAS8C,WAAW9C,MAAMuD,MAAMlC,GAAG;IAC/E,MAAMoC,YAAY,MAAMC,QAAQC,GAAG,CACjCH,QAAQjD,GAAG,CAAC,CAACP,OAASE,WAAWL,gBAAgBC,WAAWyD,MAAMxD,MAAM,EAAEC,OAAOW,GAAG;IAEtF,OAAOoC,uBACLQ,MAAMlC,GAAG,EACTuC,OAAOC,WAAW,CAACL,QAAQjD,GAAG,CAAC,CAACP,MAAM8D,QAAU;YAAC9D;YAAMI,YAAYqD,SAAS,CAACK,MAAM;SAAE;AAEzF;AAEA;;;CAGC,GACD,OAAO,eAAeC,wBACpBjE,SAAoB,EACpByD,KAAkF;IAElF,MAAM,CAAClD,UAAUY,KAAK,GAAG,MAAMyC,QAAQC,GAAG,CAAC;QACzCzD,WAAWL,gBAAgBC,WAAWyD,MAAMxD,MAAM,EAAEwD,MAAMvD,IAAI,GAAGW,GAAG;QACpE4C,MAAMvD,IAAI,KAAK,WACXF,UAAUL,UAAU,CAAC,SAASQ,GAAG,CAACsD,MAAMxD,MAAM,EAAEY,GAAG,KACnD+C,QAAQM,OAAO,CAAC;KACrB;IACD,OAAO7C,mBACLoC,MAAMvD,IAAI,EACVI,YAAYC,WACZkD,MAAMlC,GAAG,EACTJ,wBAAAA,KAAMN,GAAG,CAAC;AAEd;AAEA;;;;;;CAMC,GACD,OAAO,SAASsD,kBACdC,SAAoC,EACpCzD,IAAY,EACZW,IAAyD,EACzDE,UAAsB;QAUT6C,MAAAA;IARb,MAAMC,QAAQ,IAAIC,IAAY;WACzBjD,KAAKK,MAAM,CAAC,CAACC,MAAQ,CAACA,IAAIX,OAAO,IAAIW,IAAId,IAAI,EAAEL,GAAG,CAAC,CAACmB,MAAQA,IAAId,IAAI;WACpEgD,OAAOU,MAAM,CAAChD,qBAAAA,aAAc,CAAC,GAAGG,MAAM,CAAC,CAAC8C,OAAyB,OAAOA,SAAS;KACrF;IACD,MAAMJ,SAAS,CAACK,QACdA,SAAS,CAAChG,2BAA2BgG,UAAU,CAAEA,CAAAA,UAAU/F,oBAAoB2F,MAAMK,GAAG,CAACD,MAAK,IAC1FA,QACA7C;IACN,MAAM+C,QAAOP,QAAAA,UAAAA,OAAO5F,oBAAoB2F,oBAAAA,YAAavC,uBAAxCwC,UAAuDA,OAAO5F,oBAAoBkC,kBAAlF0D,OAA4F;IACzG,IAAIO,SAASjG,kBAAkB,OAAOiG;IACtC,IAAI9D,OAAO8D;IACX,IAAIC,UAAU;IACd,MAAOP,MAAMK,GAAG,CAAC7D,MAAOA,OAAO,GAAG8D,KAAK,CAAC,EAAEC,WAAW;IACrD,OAAO/D;AACT;AAcA,SAASgE,SACPpE,EAAU,EACVqE,KAAyC,EACzC5D,IAAwC;QAOzB4D;IALf,MAAMC,YAAYD,MAAMlE,GAAG,CAAC;IAC5B,MAAMoE,MAAMF,MAAMlE,GAAG,CAAC;IACtB,OAAO;QACLH;QACAsE,WAAW,OAAOA,cAAc,YAAYA,YAAYA,YAAY;QACpErE,MAAMC,QAAOmE,aAAAA,MAAMlE,GAAG,CAAC,0BAAVkE,aAA4B;QACzCG,eAAehE,YAAYC;OACvB8D,OAAO,OAAOA,QAAQ,WAAW;QAAEA,KAAKA;IAA4B,IAAI,CAAC,GACzEF,MAAMlE,GAAG,CAAC,gBAAgB,OAAO;QAAEI,SAAS;IAAK,IAAI,CAAC;AAE9D;AAEA,mFAAmF,GACnF,OAAO,eAAekE,YACpBnF,SAAoB,EACpByD,KAAwD;IAExD,MAAMsB,QAAQ,MAAMhF,gBAAgBC,WAAWyD,MAAMxD,MAAM,EAAEwD,MAAMvD,IAAI,EAAEC,GAAG,CAACsD,MAAM/C,EAAE,EAAEG,GAAG;IAC1F,IAAI,CAACkE,MAAMK,MAAM,EAAE,OAAO;IAC1B,OAAON,SAASrB,MAAM/C,EAAE,EAAEqE,OAAO,MAAM/E,UAAUL,UAAU,CAAC,SAASQ,GAAG,CAACsD,MAAMxD,MAAM,EAAEY,GAAG;AAC5F;AAEA;;;;;CAKC,GACD,OAAO,eAAewE,yBACpBrF,SAAoB,EACpByD,KAAqC;QAI9B7E;IAFP,MAAMmG,QAAQ,MAAMhF,gBAAgBC,WAAWyD,MAAMxD,MAAM,EAAE,YAAYE,GAAG,CAACsD,MAAM/C,EAAE,EAAEG,GAAG;IAC1F,IAAI,CAACkE,MAAMK,MAAM,EAAE,OAAO;IAC1B,QAAOxG,qBAAAA,kBAA4BmG,MAAMlE,GAAG,CAAC,qBAAtCjC,qBAAmD;AAC5D;AAEA;;;;CAIC,GACD,OAAO,eAAe0G,0BACpBtF,SAAoB,EACpByD,KAAqC;IAErC,MAAMsB,QAAQ,MAAMhF,gBAAgBC,WAAWyD,MAAMxD,MAAM,EAAE,aAAaE,GAAG,CAACsD,MAAM/C,EAAE,EAAEG,GAAG;IAC3F,IAAI,CAACkE,MAAMK,MAAM,EAAE,OAAO;IAC1B,MAAMG,QAAQR,MAAMlE,GAAG,CAAC;IACxB,IAAI,CAAC2E,MAAMC,OAAO,CAACF,QAAQ,OAAO,EAAE;IACpC,OAAOA,MACJ9E,GAAG,CAAC,CAACiF,OAAUA,QAAQ,OAAOA,SAAS,WAAW,AAACA,IAAgC,CAAC,OAAO,GAAG,MAC9F/D,MAAM,CAAC,CAAChB,OAAyB,OAAOA,SAAS,YAAYA,KAAKmB,MAAM,GAAG;AAChF;AAEA,8FAA8F,GAC9F,OAAO,eAAe6D,iBACpB3F,SAAoB,EACpByD,KAAiE;IAEjE,MAAMmC,WAAW7F,gBAAgBC,WAAWyD,MAAMxD,MAAM,EAAEwD,MAAMvD,IAAI,EAAEC,GAAG,CAACsD,MAAM/C,EAAE;IAClF,MAAMqE,QAAQ,MAAMa,SAAS/E,GAAG;IAChC,MAAMmE,YAAYD,MAAMK,MAAM,GAAGL,MAAMlE,GAAG,CAAC,eAAe;IAC1D,IAAI,OAAOmE,cAAc,YAAY,CAACA,WAAW,OAAO;IACxD,MAAMa,UAAU,MAAMD,SAASjG,UAAU,CAAC,YAAYQ,GAAG,CAAC6E,WAAWnE,GAAG;IACxE,MAAMiF,QAAQD,QAAQT,MAAM,GAAGxG,kBAA4BiH,QAAQhF,GAAG,CAAC,YAAY;IACnF,OAAOiF,QAAQ;QAAEd;QAAWc;IAAM,IAAI;AACxC;AAyCA,6DAA6D,GAC7D,SAASC,YAAY7F,IAAiB,EAAE8F,IAA6B;IACnE,MAAM/D,UAAU,IAAIsC,IAAIvF,eAAe,CAACkB,KAAK;IAC7C,OAAO4D,OAAOC,WAAW,CACvBD,OAAOmC,OAAO,CAACD,MAAMrE,MAAM,CAAC,CAAC,CAACuE,KAAKxB,MAAM,GAAKzC,QAAQ0C,GAAG,CAACuB,QAAQxB,UAAU7C;AAEhF;AAEA,0FAA0F,GAC1F,SAASsE,gBAAgBL,KAAe,EAAEM,UAAkB,EAAEzF,IAAY;QAGf0F;IAFzD,MAAMA,OAAOP,KAAK,CAACM,WAAW;IAC9B,IAAI,CAACC,QAAQA,KAAKC,WAAW,KAAKtI,mBAAmB,OAAO8H;IAC5D,OAAO,aAAKA;QAAO,CAACM,WAAW,EAAE,aAAKC;YAAMd,OAAO,cAAMc,cAAAA,KAAKd,KAAK,YAAVc,cAAc,CAAC;gBAAIE,UAAU5F;;;;AACxF;AAEA;;;;;CAKC,GACD,OAAO,eAAe6F,aAAaxG,SAAoB,EAAEyD,KAAmB;IAC1E,MAAMgD,SAAS5H,kBAAkB4E,MAAMqC,KAAK;IAC5C,IAAI,CAACW,QAAQ,MAAM,IAAIC,MAAM;IAC7B,IAAIjD,MAAMvD,IAAI,KAAK,UAAU,CAACuD,MAAMtE,IAAI,EAAE,MAAM,IAAIuH,MAAM;IAC1D,IAAIjD,MAAMvD,IAAI,KAAK,eAAe,CAAEuD,CAAAA,MAAMkD,MAAM,IAAIlD,MAAMqC,KAAK,CAACrC,MAAMkD,MAAM,CAAC,AAAD,GAAI;QAC9E,MAAM,IAAID,MAAM;IAClB;IACA,MAAME,UAAU5G,UAAUL,UAAU,CAAC,SAASQ,GAAG,CAACsD,MAAMxD,MAAM;IAC9D,MAAMN,aAAaI,gBAAgBC,WAAWyD,MAAMxD,MAAM,EAAEwD,MAAMvD,IAAI;IACtE,MAAM0F,WAAWjG,WAAWQ,GAAG,CAACsD,MAAM/C,EAAE;IACxC,OAAOV,UAAU6G,cAAc,CAAC,OAAOC;QACrC,yDAAyD;QACzD,MAAM,CAAC3F,MAAM4F,UAAUC,SAAS,GAAG,MAAMpD,QAAQC,GAAG,CAAC;YACnDiD,GAAGjG,GAAG,CAAC+F;YACPE,GAAGjG,GAAG,CAAC+E;YACPkB,GAAGjG,GAAG,CAACT,WAAWT;SACnB;QACD,IAAI,CAACwB,KAAKiE,MAAM,EAAE,OAAO;YAAE6B,IAAI;YAAOC,QAAQ;YAAKC,OAAO;QAAe;QACzE,IAAIJ,SAAS3B,MAAM,EAAE;YACnB,OAAO;gBAAE6B,IAAI;gBAAMG,UAAU;eAAStC,SAASrB,MAAM/C,EAAE,EAAEqG,UAAU5F;QACrE;QACA,MAAMG,OAAOhB,YAAY0G;QACzB,oEAAoE;QACpE,MAAMxF,aAAaL,KAAKN,GAAG,CAAC;QAC5B,MAAMgC,UAAUxB,mBAAmBoC,MAAMvD,IAAI,EAAEoB,MAAMmC,MAAMlC,GAAG,EAAEC;QAChE,IAAIqB,SAAS,OAAO;YAAEoE,IAAI;YAAOC,QAAQ;YAAKC,OAAOtE;QAAQ;QAC7D,MAAMlC,OAAO5C,oBACX0F,MAAM9C,IAAI,EACVW,KAAKK,MAAM,CAAC,CAACC,MAAQ,CAACA,IAAIX,OAAO,EAAER,GAAG,CAAC,CAACmB,MAAQA,IAAIjB,IAAI;QAE1D,MAAM0G,SAAS;YAAEC,WAAW7D,MAAM8D,GAAG;YAAEC,WAAW/D,MAAM8D,GAAG;YAAEE,WAAWhE,MAAMiE,GAAG;QAAC;QAClF,MAAM5B,QAAQ6B,OAAOC,IAAI,CAACnB;QAC1B,IAAIzB,YAA2B;QAC/B,IAAIvB,MAAMvD,IAAI,KAAK,UAAU;YAC3B8E,YAAYlH;YACZgJ,GAAGzD,MAAM,CAACuC,UAAU,aAAKG,YAAY,UAAU;gBAAE8B,aAAalH;gBAAMqE;YAAU,IAAOqC;YACrFP,GAAGzD,MAAM,CAACuC,SAASjG,UAAU,CAAC,YAAYQ,GAAG,CAAC6E,YAAY;gBACxD8C,UAAUrE,MAAM/C,EAAE;gBAClBT,QAAQwD,MAAMxD,MAAM;gBACpB4H,aAAarI;gBACbsG;eACGuB;QAEP,OAAO,IAAI5D,MAAMvD,IAAI,KAAK,UAAU;YAClC8E,YAAYlH;YACZ,MAAMgD,OAAOqD,kBAAkBV,MAAM3C,IAAI,EAAEH,MAAMW,MAAME;YACvDsF,GAAGzD,MAAM,CAACuC,UAAU,aACfG,YAAY,UAAU;gBAAE8B,aAAalH;gBAAMG;gBAAMkE;YAAU;gBAC9D+C,WAAW7J,cAAcyC;eACtB0G;YAELP,GAAGzD,MAAM,CAACuC,SAASjG,UAAU,CAAC,YAAYQ,GAAG,CAAC6E,YAAY;gBACxDgD,UAAUvE,MAAM/C,EAAE;gBAClBT,QAAQwD,MAAMxD,MAAM;gBACpB4H,aAAarI;gBACbsG;eACIrC,MAAMqE,QAAQ,GAAG;gBAAEA,UAAUrE,MAAMqE,QAAQ;YAAC,IAAI,CAAC,GAClDT;QAEP,OAAO,IAAI5D,MAAMvD,IAAI,KAAK,YAAY;YACpC4G,GAAGzD,MAAM,CAACuC,UAAU,aACfG,YAAY,YAAY;gBACzB7F,MAAM;gBACN2H,aAAalH;gBACbmF;gBACAhF,MAAM2C,MAAM3C,IAAI,IAAIe;YACtB;gBACAoG,QAAQ;oBAAEC,MAAM;gBAAW;eACxBb;QAEP,OAAO,IAAI5D,MAAMvD,IAAI,KAAK,aAAa;gBAM1BuD;YALXqD,GAAGzD,MAAM,CAACuC,UAAU,aACfG,YAAY,aAAa;gBAC1B8B,aAAalH;gBACbgG,QAAQlD,MAAMkD,MAAM;gBACpBb;gBACAP,OAAO9B,EAAAA,eAAAA,MAAM8B,KAAK,qBAAX9B,aAAa3B,MAAM,IAAG;uBAAI2B,MAAM8B,KAAK;iBAAC,GAAG1D;YAClD,IACGwF;QAEP,OAAO;gBAEaxI,oBAOLM;YARb,MAAMA,OAAOsE,MAAMtE,IAAI;YACvB,MAAMgJ,aAAYtJ,qBAAAA,kBAAkBsH,gBAAgB1C,MAAMqC,KAAK,EAAE3G,KAAKiH,UAAU,EAAEzF,kBAAhE9B,qBAA0E4H;YAC5FK,GAAGzD,MAAM,CAACuC,UAAU,aACfG,YAAY,QAAQ;gBACrB8B,aAAalH;gBACbG,MAAM7C,kBAAkB0C,SAAS8C,MAAM/C,EAAE;gBACzC0H,QAAQjJ,KAAKiJ,MAAM;gBACnBC,kBAAkBlJ,KAAKkJ,gBAAgB,IAAIxG;gBAC3CyG,OAAO,GAAEnJ,gBAAAA,KAAKmJ,OAAO,YAAZnJ,gBAAgB0C;gBACzB8E,QAAQxH,KAAKwH,MAAM;gBACnBb,OAAO6B,OAAOC,IAAI,CAACO;YACrB,IACGd;QAEP;QACA,OAAO;YACLJ,IAAI;YACJG,UAAU;YACV1G,IAAI+C,MAAM/C,EAAE;YACZsE;YACArE;YACAuE,eAAehE,YAAYC;QAC7B;IACF;AACF;AAMA;;;;;;CAMC,GACD,OAAO,eAAeoH,mBACpBvI,SAAoB,EACpByD,KAMC;IAED,MAAMmC,WAAW7F,gBAAgBC,WAAWyD,MAAMxD,MAAM,EAAEwD,MAAMvD,IAAI,EAAEC,GAAG,CAACsD,MAAM/C,EAAE;IAClF,OAAOV,UAAU6G,cAAc,CAAC,OAAOC;QACrC,MAAM/B,QAAQ,MAAM+B,GAAGjG,GAAG,CAAC+E;QAC3B,MAAMZ,YAAYD,MAAMK,MAAM,IAAIL,MAAMlE,GAAG,CAAC,gBAAgB,OAAOkE,MAAMlE,GAAG,CAAC,eAAe;QAC5F,IAAI,OAAOmE,cAAc,YAAY,CAACA,WAAW;YAC/C,OAAO;gBAAEiC,IAAI;gBAAOC,QAAQ;gBAAKC,OAAO;YAAqC;QAC/E;QACA,MAAMqB,aAAa5C,SAASjG,UAAU,CAAC,YAAYQ,GAAG,CAAC6E;QACvD,MAAMa,UAAU,MAAMiB,GAAGjG,GAAG,CAAC2H;QAC7B,MAAM1C,QAAQD,QAAQT,MAAM,GAAGxG,kBAA4BiH,QAAQhF,GAAG,CAAC,YAAY;QACnF,IAAI,CAACiF,OAAO,OAAO;YAAEmB,IAAI;YAAOC,QAAQ;YAAKC,OAAO;QAAqC;QACzF,MAAMsB,OAAOhF,MAAMiF,MAAM,CAAC5C;QAC1B,IAAI,CAAC2C,MAAM,OAAO;YAAExB,IAAI;YAAM0B,SAAS;QAAM;QAC7C,MAAMlC,SAAS5H,kBAAkB4J;QACjC,IAAI,CAAChC,QAAQ,MAAM,IAAIC,MAAM;QAC7BI,GAAG4B,MAAM,CAACF,YAAY;YAAE1C,OAAO6B,OAAOC,IAAI,CAACnB;YAASe,WAAW/D,MAAM8D,GAAG;QAAC;QACzE,OAAO;YAAEN,IAAI;YAAM0B,SAAS;QAAK;IACnC;AACF;AAEA;;;CAGC,GACD,OAAO,eAAeC,sBACpB5I,SAAoB,EACpByD,KAKC;IAED,MAAMoF,MAAM9I,gBAAgBC,WAAWyD,MAAMxD,MAAM,EAAE,UAAUE,GAAG,CAACsD,MAAM/C,EAAE;IAC3E,MAAMV,UAAU6G,cAAc,CAAC,OAAOC;QACpC,MAAMzH,SAAS,MAAMyH,GAAGjG,GAAG,CAACgI;QAC5B,IAAI,CAACxJ,OAAO+F,MAAM,IAAI/F,OAAOwB,GAAG,CAAC,gBAAgB,MAAM;QACvD,MAAMiI,QAAiC,CAAC;QACxC,KAAK,MAAM5C,OAAO;YAAC;YAAS;SAAc,CAAW;gBAC9BzC,gBACApE;YADrB,MAAMqF,QAAQ9D,QAAO6C,iBAAAA,MAAMwB,GAAG,CAACiB,IAAI,YAAdzC,iBAAkB,IAAIsF,IAAI;YAC/C,IAAIrE,SAAS,CAAC9D,QAAOvB,cAAAA,OAAOwB,GAAG,CAAC,CAAC,IAAI,EAAEqF,KAAK,aAAvB7G,cAA4B,IAAI0J,IAAI,IAAID,KAAK,CAAC,CAAC,IAAI,EAAE5C,KAAK,CAAC,GAAGxB;QACrF;QACA,IAAIZ,OAAOkF,IAAI,CAACF,OAAOhH,MAAM,EAAEgF,GAAG4B,MAAM,CAACG,KAAK,aAAKC;YAAOtB,WAAW/D,MAAM8D,GAAG;;IAChF;AACF;AAEA,kEAAkE,GAClE,OAAO,eAAe0B,gBAAgBjJ,SAAoB,EAAEC,MAAc;IACxE,OAAOiB,YAAY,MAAMlB,UAAUL,UAAU,CAAC,SAASQ,GAAG,CAACF,QAAQY,GAAG;AACxE;AAEA;;;;CAIC,GACD,OAAO,eAAeqI,wBACpBlJ,SAAoB,EACpByD,KASC;IAED,IAAI,CAACA,MAAMxD,MAAM,EAAE;YAGawD;QAF9B,OAAO;YACLyD,QAAQ;YACRC,OAAO,CAAC,kBAAkB,GAAE1D,cAAAA,MAAM0F,IAAI,YAAV1F,cAAcA,MAAMvD,IAAI,CAAC,+BAA+B,CAAC;QACvF;IACF;IACA,MAAMkJ,QAAQ,MAAMtK,oBAAoB2E,MAAMxD,MAAM;IACpD,IAAI,CAACmJ,SAASA,UAAU3F,MAAM4F,KAAK,EAAE,OAAO;QAAEnC,QAAQ;QAAKC,OAAO;IAAe;IACjF,MAAMmC,MAAM,OAAM7F,MAAM8F,QAAQ,oBAAd9F,MAAM8F,QAAQ,MAAd9F,OAAiBA,MAAMxD,MAAM;IAC/C,IAAIqJ,KAAK,OAAOA;IAChB,MAAMzG,UAAU,MAAMoB,wBAAwBjE,WAAW;QACvDE,MAAMuD,MAAMvD,IAAI;QAChBD,QAAQwD,MAAMxD,MAAM;QACpBsB,KAAKkC,MAAMlC,GAAG;IAChB;IACA,OAAOsB,UAAU;QAAEqE,QAAQ;QAAKC,OAAOtE;IAAQ,IAAI;AACrD"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-drafts.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 { createResourceUid } from '@aglyn/aglyn/app-utils/create-resource-uid'\nimport { uniqueDuplicateName } from '@aglyn/aglyn/app-utils/duplicate-resource'\nimport {\n FORM_COMPONENT_ID,\n normalizeFormSlug,\n type FormFieldDecl,\n type FormRouting,\n} from '@aglyn/aglyn/app-utils/forms'\nimport { nameSearchKey } from '@aglyn/aglyn/app-utils/name-search'\nimport {\n checkDatasetQuota,\n checkEntitlement,\n checkQuota,\n resolveEffectivePlan,\n resolveOrgEntitlements,\n} from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport {\n billableScreenIds,\n normalizeScreenSlug,\n reservedScreenRouteSegment,\n SCREEN_ROOT_PATH,\n} from '@aglyn/aglyn/app-utils/screen-route'\nimport { decodeStoredNodes, encodeStoredNodes } from '@aglyn/aglyn/app-utils/stored-nodes'\nimport type {\n AglynOrgBilling,\n OrgFeatureFlags,\n} from '@aglyn/aglyn/foundation/definitions/org-billing.types'\nimport type { ReusableComponentProp } from '@aglyn/aglyn/foundation/definitions/platform.types'\nimport type { NodesMap } from '@aglyn/aglyn/types/nodes'\nimport { resolveOrgIdForHost } from '@aglyn/tenant-data-admin/server/organizations'\nimport {\n aiUnrestrictedPlanCapabilities,\n type AiPlanCapabilities,\n type AiPlanCreation,\n} from '../model/ai-plan-capabilities'\nimport type { AiJobAdmissionRefusal } from './ai-job-admission'\n\n/**\n * Where a generation job's layout, page template, form, reusable component\n * or page lands (AGL-2909, AGL-2913, AGL-2908, AGL-2907): a new draft, made\n * as the console's host resources route makes one.\n *\n * ── The document the create route makes ─────────────────────────────────\n *\n * `AI_DRAFT_FIELDS` is that route's allow-list for each kind, and a spec\n * reads the route's source to hold the two together. Nothing outside the\n * list is written except the route's own stamps: `createdAt`, `updatedAt`,\n * `createdBy`, a screen's `nameLower`, and a template's `source`, which is\n * `authored` — a template a member's job generated is theirs, never a starter\n * or a listing. A layout's or a screen's first version carries the keys the\n * versions route seeds one with. A form is the document the Forms page's\n * Create sends: its design canvas-shaped under `rootId` with the form node\n * bound to the form's id and captioned with its name, a slug read from that\n * name, and no version — the form's page mints the first one when someone\n * opens it. Where Create sends an empty `fields`, a generated form sends the\n * declaration read off its own design, so the two agree from the start. A\n * component carries its tree and the props it declares on its own document,\n * as Use template writes one, and gets its first version where every\n * component does: when a member opens it.\n *\n * ── Counted like a create ────────────────────────────────────────────────\n *\n * The band is met INSIDE the transaction that writes (AGL-2231), with the\n * route's arithmetic: every layout document counts against\n * `sharedLayoutsPerHost`; a template counts against `templatesPerHost` when it\n * records a source other than a platform starter — the route's `!=` query,\n * which never matches a document with no `source.type` at all; a screen counts\n * against `screensPerHost` by `billableScreenIds`, over the screens and the\n * host's routing map, which counts an unrouted screen too (AGL-1445); and a\n * form needs the plan to include the route's `entitlement` first, refused in\n * the route's own words, after which every form document counts against\n * `formsPerHost`. A reusable component counts against no allowance at all:\n * the route admits one on a plan with the `reusableComponents` entitlement\n * and refuses it otherwise, and so does the writer.\n *\n * ── Applied to nothing ───────────────────────────────────────────────────\n *\n * A layout renders only around a screen or layout that names it, or as the\n * built-in page layout the host document names; a template is inert until a\n * member uses it; a form renders only where a page places it; a component\n * renders only where an instance places it, and a new one has no instance;\n * and a screen serves nothing until the host's routing map names it, which is\n * what publishing writes (`publishScreenRoute`). So the writer touches the new\n * documents and nothing else — no host document, no routing map, no\n * collection, store setting, other layout or component — and never a\n * `publishedAt` or a publish schedule. What the job hands back is a link to\n * the draft, never a binding.\n *\n * ── One draft per recorded id ────────────────────────────────────────────\n *\n * The caller names the document id: the console resource id the job recorded\n * for the draft before its first write (`ai-job-draft-ids.ts`). A step run\n * again after its draft was written — a process cut off before the machine\n * recorded the step — reads the same id, finds that draft and reports it,\n * rather than writing a second.\n */\n\ntype Firestore = FirebaseFirestore.Firestore\n\nexport type AiDraftKind = 'layout' | 'template' | 'form' | 'component' | 'screen'\n\n/** The draft kinds that keep their tree in versions, as the besigner opens them. */\nexport type AiVersionedDraftKind = Extract<AiDraftKind, 'layout' | 'screen'>\n\n/** The console host resources route's allow-list for each kind. */\nexport const AI_DRAFT_FIELDS: Readonly<Record<AiDraftKind, readonly string[]>> = {\n layout: ['displayName', 'description', 'versionId'],\n template: [\n 'kind',\n 'displayName',\n 'description',\n 'placeholders',\n 'nodes',\n 'rootId',\n 'props',\n 'slug',\n 'seo',\n // A component template's own kind (AGL-3287); a job never sends one.\n 'componentKind',\n ],\n form: [\n 'displayName',\n 'slug',\n 'fields',\n 'consentFieldName',\n 'routing',\n 'legacyMatch',\n 'rootId',\n 'nodes',\n ],\n // `kind` marks a reusable email block (AGL-3287); a job makes page\n // components and never sends one.\n component: ['displayName', 'description', 'rootId', 'nodes', 'props', 'kind'],\n // `kind` is on the route's list for the email composer; a page has none, and\n // the writer never sends one.\n screen: ['displayName', 'description', 'slug', 'seo', 'kind', 'versionId'],\n}\n\n/** The keys a layout's first version is seeded with, all on the versions route's list. */\nexport const AI_DRAFT_VERSION_FIELDS: readonly string[] = ['layoutId', 'hostId', 'displayName', 'nodes']\n\n/**\n * The keys a screen's first version is seeded with, all on the versions\n * route's list: the screen it belongs to, the site, the label, the tree, and\n * the layout it renders inside when the plan names one the site has.\n */\nexport const AI_DRAFT_SCREEN_VERSION_FIELDS: readonly string[] = [\n 'screenId',\n 'hostId',\n 'displayName',\n 'nodes',\n 'layoutId',\n]\n\n/** The label on a generated draft's first version, as on one Use template makes. */\nexport const AI_DRAFT_VERSION_NAME = 'Initial version'\n\n/** The route's refusal when the site's plan does not include the kind at all. */\nexport const AI_DRAFT_ENTITLEMENT_REFUSAL = 'This feature is not included in your plan — see Billing'\n\nexport interface AiDraftBand {\n collection: 'layouts' | 'templates' | 'forms' | 'components' | 'screens'\n /** The allowance the route counts the kind against, where it counts one. */\n quotaKey?:\n | 'sharedLayoutsPerHost'\n | 'templatesPerHost'\n | 'formsPerHost'\n | 'screensPerHost'\n /** The feature the plan must include before any document of the kind counts. */\n entitlement?: keyof OrgFeatureFlags\n /** The route's plural label, in the refusal it gives. */\n label: string\n}\n\n/** Each kind's collection, counter, feature and label, as the create route declares them. */\nexport const AI_DRAFT_BANDS: Readonly<Record<AiDraftKind, AiDraftBand>> = {\n layout: { collection: 'layouts', quotaKey: 'sharedLayoutsPerHost', label: 'shared layouts' },\n template: { collection: 'templates', quotaKey: 'templatesPerHost', label: 'templates' },\n form: {\n collection: 'forms',\n quotaKey: 'formsPerHost',\n entitlement: 'reusableComponents',\n label: 'forms',\n },\n component: {\n collection: 'components',\n entitlement: 'reusableComponents',\n label: 'reusable components',\n },\n screen: { collection: 'screens', quotaKey: 'screensPerHost', label: 'screens' },\n}\n\ninterface SiblingRow {\n id: string\n name: string\n slug: string\n kind: unknown\n sourceType: unknown\n deletedAt: unknown\n deleted: boolean\n}\n\n/** A host's routing map: screen id → the path it is published at. */\ntype RoutingMap = Record<string, unknown> | null | undefined\n\nfunction draftCollection(firestore: Firestore, hostId: string, kind: AiDraftKind) {\n return firestore.collection('hosts').doc(hostId).collection(AI_DRAFT_BANDS[kind].collection)\n}\n\n/** The fields the band, the name, the address and a deletion are read from. */\nfunction siblingsOf(collection: FirebaseFirestore.CollectionReference) {\n return collection.select('displayName', 'slug', 'kind', 'source.type', 'deletedAt')\n}\n\nfunction siblingRows(snapshot: FirebaseFirestore.QuerySnapshot): SiblingRow[] {\n return snapshot.docs.map((doc) => ({\n id: doc.id,\n name: String(doc.get('displayName') ?? ''),\n slug: String(doc.get('slug') ?? ''),\n kind: doc.get('kind'),\n sourceType: doc.get('source.type'),\n deletedAt: doc.get('deletedAt'),\n deleted: doc.get('deletedAt') != null,\n }))\n}\n\nfunction subdomainOf(host: FirebaseFirestore.DocumentSnapshot): string | null {\n const subdomain = host.get('subdomain')\n return typeof subdomain === 'string' && subdomain ? subdomain : null\n}\n\n/**\n * The route's refusal when the site's plan does not include the kind, or\n * holds no more of it; `null` while it does. The feature is asked first, as\n * the route asks it before it counts.\n */\nexport function aiDraftBandRefusal(\n kind: AiDraftKind,\n rows: ReadonlyArray<Pick<SiblingRow, 'id' | 'kind' | 'sourceType' | 'deletedAt'>>,\n org: Partial<AglynOrgBilling> | null,\n routingMap?: RoutingMap,\n): string | null {\n const band = AI_DRAFT_BANDS[kind]\n if (band.entitlement && !checkEntitlement(org, band.entitlement)) {\n return AI_DRAFT_ENTITLEMENT_REFUSAL\n }\n if (!band.quotaKey) return null\n const used =\n kind === 'template'\n ? rows.filter((row) => row.sourceType !== undefined && row.sourceType !== 'starter').length\n : kind === 'screen'\n ? billableScreenIds(\n rows.map((row) => ({ id: row.id, kind: row.kind, deletedAt: row.deletedAt })),\n (routingMap ?? undefined) as never,\n ).size\n : rows.length\n const quota = checkQuota(org, band.quotaKey, used)\n return quota.allowed\n ? null\n : `Your plan includes ${quota.limit} ${band.label} — upgrade in Billing for more`\n}\n\n/** The draft kinds a plan's creations land as, whose band a plan is told about (AGL-3030). */\nconst AI_PLAN_DRAFT_KINDS = ['component', 'form', 'layout', 'template'] as const\n\ntype AiPlanDraftKind = (typeof AI_PLAN_DRAFT_KINDS)[number]\n\n/** Every draft kind's plural label made singular, for a band of one. */\nfunction bandLabel(label: string, count: number): string {\n return count === 1 ? label.replace(/s$/, '') : label\n}\n\n/**\n * Whether the site may take `used + 1` of a kind, in the create route's\n * arithmetic: the feature first, then the allowance. `null` for a kind whose\n * allowance is unlimited or counts nothing, which needs no rows read.\n */\nfunction planCreation(\n kind: AiPlanDraftKind,\n org: Partial<AglynOrgBilling> | null,\n rows: ReadonlyArray<Pick<SiblingRow, 'id' | 'kind' | 'sourceType' | 'deletedAt'>> | null,\n): AiPlanCreation {\n const band = AI_DRAFT_BANDS[kind]\n if (band.entitlement && !checkEntitlement(org, band.entitlement)) {\n return {\n allowed: false,\n left: 0,\n reason: `this workspace's plan does not include ${kind === 'form' ? 'saved forms' : band.label}`,\n }\n }\n const limit = band.quotaKey ? resolveOrgEntitlements(org)[band.quotaKey] : Number.POSITIVE_INFINITY\n if (!rows || !Number.isFinite(limit)) return { allowed: true, left: null, reason: null }\n // The route's own arithmetic decides; the count beside it is what is left.\n const refusal = aiDraftBandRefusal(kind, rows, org)\n const used =\n kind === 'template'\n ? rows.filter((row) => row.sourceType !== undefined && row.sourceType !== 'starter').length\n : rows.length\n return refusal\n ? {\n allowed: false,\n left: 0,\n reason: `this site already holds the ${limit} ${bandLabel(band.label, limit)} its plan includes`,\n }\n : { allowed: true, left: Math.max(0, limit - used), reason: null }\n}\n\n/** Whether a kind's rows are counted at all on this plan: included, and under a finite allowance. */\nfunction countsRows(kind: AiPlanDraftKind, org: Partial<AglynOrgBilling> | null): boolean {\n const band = AI_DRAFT_BANDS[kind]\n if (!band.quotaKey) return false\n if (band.entitlement && !checkEntitlement(org, band.entitlement)) return false\n return Number.isFinite(resolveOrgEntitlements(org)[band.quotaKey])\n}\n\n/**\n * What a plan may create on this site (AGL-3030), from the workspace's plan\n * and the rows each counted kind already holds — pure, so the specs and the\n * eval harness hold the arithmetic without a Firestore. A kind whose rows\n * are not given is counted as having room, which is what an unlimited\n * allowance means.\n *\n * A theme change is a proposal a member applies, never a draft, so it counts\n * against nothing. A dataset is org data that no job writes; a plan may name\n * one only where the workspace's plan includes datasets at all. An email\n * design is written by the email plugin, whose own gate decides.\n *\n * A workspace on the Free plan spends the Free taste, a wall of monthly\n * credits, so its plan is held to the sections that wall pays for (AGL-3070).\n * The effective plan decides, as it decides the taste's own allowance: a\n * workspace whose subscription died is Free again.\n */\nexport function aiPlanCapabilitiesFrom(\n org: Partial<AglynOrgBilling> | null,\n rows: Partial<Record<AiPlanDraftKind, ReadonlyArray<Pick<SiblingRow, 'id' | 'kind' | 'sourceType' | 'deletedAt'>>>> = {},\n): AiPlanCapabilities {\n const unrestricted = aiUnrestrictedPlanCapabilities()\n const datasets = checkDatasetQuota(org, 0).limit > 0\n return {\n reusableComponents: checkEntitlement(org, 'reusableComponents'),\n create: {\n ...unrestricted.create,\n component: planCreation('component', org, rows.component ?? null),\n form: planCreation('form', org, rows.form ?? null),\n layout: planCreation('layout', org, rows.layout ?? null),\n template: planCreation('template', org, rows.template ?? null),\n dataset: datasets\n ? unrestricted.create.dataset\n : { allowed: false, left: 0, reason: \"this workspace's plan does not include datasets\" },\n },\n ...(resolveEffectivePlan(org) === 'free' ? { freeTaste: true } : {}),\n }\n}\n\n/**\n * What a plan may create on this site, read: the rows of each kind whose\n * allowance the workspace's plan counts, as the draft writer reads them, and\n * nothing for a kind the plan leaves out or leaves unlimited.\n */\nexport async function readAiPlanCapabilities(\n firestore: Firestore,\n input: { hostId: string; org: Partial<AglynOrgBilling> | null },\n): Promise<AiPlanCapabilities> {\n const counted = AI_PLAN_DRAFT_KINDS.filter((kind) => countsRows(kind, input.org))\n const snapshots = await Promise.all(\n counted.map((kind) => siblingsOf(draftCollection(firestore, input.hostId, kind)).get()),\n )\n return aiPlanCapabilitiesFrom(\n input.org,\n Object.fromEntries(counted.map((kind, index) => [kind, siblingRows(snapshots[index])])),\n )\n}\n\n/**\n * Whether the site can take one more draft of the kind: a read, for a door\n * that asks before it spends. The write asks again inside its transaction.\n */\nexport async function aiDraftAllowanceRefusal(\n firestore: Firestore,\n input: { kind: AiDraftKind; hostId: string; org: Partial<AglynOrgBilling> | null },\n): Promise<string | null> {\n const [snapshot, host] = await Promise.all([\n siblingsOf(draftCollection(firestore, input.hostId, input.kind)).get(),\n input.kind === 'screen'\n ? firestore.collection('hosts').doc(input.hostId).get()\n : Promise.resolve(null),\n ])\n return aiDraftBandRefusal(\n input.kind,\n siblingRows(snapshot),\n input.org,\n host?.get('screens') as RoutingMap,\n )\n}\n\n/**\n * A draft screen's one-segment address: the one asked for, else one from its\n * name, never a reserved segment, and never an address a live screen keeps\n * or the routing map already serves — so the member's publish at it later\n * takes nothing from a live page. The root is asked for only while no screen\n * holds it.\n */\nexport function aiDraftScreenSlug(\n requested: string | null | undefined,\n name: string,\n rows: ReadonlyArray<Pick<SiblingRow, 'slug' | 'deleted'>>,\n routingMap: RoutingMap,\n): string {\n const taken = new Set<string>([\n ...rows.filter((row) => !row.deleted && row.slug).map((row) => row.slug),\n ...Object.values(routingMap ?? {}).filter((path): path is string => typeof path === 'string'),\n ])\n const usable = (value: string | undefined) =>\n value && !reservedScreenRouteSegment(value) && !(value === SCREEN_ROOT_PATH && taken.has(value))\n ? value\n : undefined\n const base = usable(normalizeScreenSlug(requested ?? undefined)) ?? usable(normalizeScreenSlug(name)) ?? 'page'\n if (base === SCREEN_ROOT_PATH) return base\n let slug = base\n let attempt = 2\n while (taken.has(slug)) slug = `${base}-${attempt++}`\n return slug\n}\n\nexport interface AiDraftRecord {\n id: string\n /** A layout's or a screen's first version; `null` for every other kind, which the writer gives none. */\n versionId: string | null\n name: string\n hostSubdomain: string | null\n /** A screen's search listing as the draft holds it now; absent on other kinds. */\n seo?: { title?: string; description?: string } | null\n /** Whether a member deleted the draft since it was written. */\n deleted?: boolean\n}\n\nfunction recordOf(\n id: string,\n draft: FirebaseFirestore.DocumentSnapshot,\n host: FirebaseFirestore.DocumentSnapshot,\n): AiDraftRecord {\n const versionId = draft.get('versionId')\n const seo = draft.get('seo')\n return {\n id,\n versionId: typeof versionId === 'string' && versionId ? versionId : null,\n name: String(draft.get('displayName') ?? ''),\n hostSubdomain: subdomainOf(host),\n ...(seo && typeof seo === 'object' ? { seo: seo as AiDraftRecord['seo'] } : {}),\n ...(draft.get('deletedAt') != null ? { deleted: true } : {}),\n }\n}\n\n/** The draft a job already wrote under `id`, when a run of its step got that far. */\nexport async function readAiDraft(\n firestore: Firestore,\n input: { kind: AiDraftKind; hostId: string; id: string },\n): Promise<AiDraftRecord | null> {\n const draft = await draftCollection(firestore, input.hostId, input.kind).doc(input.id).get()\n if (!draft.exists) return null\n return recordOf(input.id, draft, await firestore.collection('hosts').doc(input.hostId).get())\n}\n\n/**\n * The tree a template draft holds now, decoded; `null` when there is none.\n *\n * A template is not a versioned draft — its nodes live on the document\n * itself, which is why `readAiDraftNodes` refuses its kind.\n */\nexport async function readAiTemplateDraftNodes(\n firestore: Firestore,\n input: { hostId: string; id: string },\n): Promise<NodesMap | null> {\n const draft = await draftCollection(firestore, input.hostId, 'template').doc(input.id).get()\n if (!draft.exists) return null\n return decodeStoredNodes<NodesMap>(draft.get('nodes')) ?? null\n}\n\n/**\n * The property names a component draft declares now; `null` when there is no\n * such draft. Read from the draft document rather than its version, because a\n * component's props live beside its tree and a copy carries its source's.\n */\nexport async function readAiDraftComponentProps(\n firestore: Firestore,\n input: { hostId: string; id: string },\n): Promise<string[] | null> {\n const draft = await draftCollection(firestore, input.hostId, 'component').doc(input.id).get()\n if (!draft.exists) return null\n const props = draft.get('props')\n if (!Array.isArray(props)) return []\n return props\n .map((prop) => (prop && typeof prop === 'object' ? (prop as Record<string, unknown>)['name'] : null))\n .filter((name): name is string => typeof name === 'string' && name.length > 0)\n}\n\n/** The tree a versioned draft's first version holds now, decoded; `null` when there is none. */\nexport async function readAiDraftNodes(\n firestore: Firestore,\n input: { kind: AiVersionedDraftKind; hostId: string; id: string },\n): Promise<{ versionId: string; nodes: NodesMap } | null> {\n const draftRef = draftCollection(firestore, input.hostId, input.kind).doc(input.id)\n const draft = await draftRef.get()\n const versionId = draft.exists ? draft.get('versionId') : null\n if (typeof versionId !== 'string' || !versionId) return null\n const version = await draftRef.collection('versions').doc(versionId).get()\n const nodes = version.exists ? decodeStoredNodes<NodesMap>(version.get('nodes')) : null\n return nodes ? { versionId, nodes } : null\n}\n\nexport interface AiDraftInput {\n kind: AiDraftKind\n hostId: string\n /** The draft's document id: the id the job recorded for this draft. */\n id: string\n /** Whose work the draft is: the job's creator, whose brief it built. */\n uid: string\n org: Partial<AglynOrgBilling> | null\n /** The name asked for; a live sibling already called that gets a number. */\n name: string\n nodes: NodesMap\n /** A page template's suggested address, or a screen's one-segment address. */\n slug?: string | null\n /** A form's declaration and the ids its design hangs from; required for a form. */\n form?: AiFormDraftDeclaration\n /** A component's root, which the node map holds; required for a component. */\n rootId?: string\n /** The properties a component declares, which its tree binds. */\n props?: readonly ReusableComponentProp[]\n /** A screen's layout, for its first version to render inside; a layout the site has. */\n layoutId?: string | null\n now: Date\n}\n\n/** What a form draft declares beside its design, read off that design. */\nexport interface AiFormDraftDeclaration {\n /** The canvas root `nodes` is stored under, as the Forms page's Create stores it. */\n rootId: string\n /** The form node, which carries the form's id and is captioned with the draft's name. */\n formNodeId: string\n fields: FormFieldDecl[]\n consentFieldName: string | null\n routing: FormRouting | null\n}\n\nexport type AiDraftWrite =\n | (AiDraftRecord & { ok: true; replayed: boolean })\n | { ok: false; status: 403 | 404; error: string }\n\n/** Keep only the keys the create route admits for the kind. */\nfunction allowListed(kind: AiDraftKind, data: Record<string, unknown>): Record<string, unknown> {\n const allowed = new Set(AI_DRAFT_FIELDS[kind])\n return Object.fromEntries(\n Object.entries(data).filter(([key, value]) => allowed.has(key) && value !== undefined),\n )\n}\n\n/** The node map with its form node captioned with `name`, as Create captions a new form. */\nfunction withFormCaption(nodes: NodesMap, formNodeId: string, name: string): NodesMap {\n const node = nodes[formNodeId]\n if (!node || node.componentId !== FORM_COMPONENT_ID) return nodes\n return { ...nodes, [formNodeId]: { ...node, props: { ...(node.props ?? {}), formName: name } } }\n}\n\n/**\n * Write one draft: the band met, the name made unique among the live\n * siblings, the node map stored as msgpack, all in one transaction. A\n * refusal is returned, never thrown; a Firestore failure propagates, so the\n * machine's retry applies.\n */\nexport async function writeAiDraft(firestore: Firestore, input: AiDraftInput): Promise<AiDraftWrite> {\n const packed = encodeStoredNodes(input.nodes)\n if (!packed) throw new Error('an AI draft needs a node map')\n if (input.kind === 'form' && !input.form) throw new Error('an AI form draft needs its declaration')\n if (input.kind === 'component' && !(input.rootId && input.nodes[input.rootId])) {\n throw new Error('a component draft needs a root its node map holds')\n }\n const hostRef = firestore.collection('hosts').doc(input.hostId)\n const collection = draftCollection(firestore, input.hostId, input.kind)\n const draftRef = collection.doc(input.id)\n return firestore.runTransaction(async (tx): Promise<AiDraftWrite> => {\n // Every read before any write, which Firestore requires.\n const [host, existing, siblings] = await Promise.all([\n tx.get(hostRef),\n tx.get(draftRef),\n tx.get(siblingsOf(collection)),\n ])\n if (!host.exists) return { ok: false, status: 404, error: 'Unknown site' }\n if (existing.exists) {\n return { ok: true, replayed: true, ...recordOf(input.id, existing, host) }\n }\n const rows = siblingRows(siblings)\n // Read, never written: the routing map decides which screens count.\n const routingMap = host.get('screens') as RoutingMap\n const refusal = aiDraftBandRefusal(input.kind, rows, input.org, routingMap)\n if (refusal) return { ok: false, status: 403, error: refusal }\n const name = uniqueDuplicateName(\n input.name,\n rows.filter((row) => !row.deleted).map((row) => row.name),\n )\n const stamps = { createdAt: input.now, updatedAt: input.now, createdBy: input.uid }\n const nodes = Buffer.from(packed)\n let versionId: string | null = null\n if (input.kind === 'layout') {\n versionId = createResourceUid()\n tx.create(draftRef, { ...allowListed('layout', { displayName: name, versionId }), ...stamps })\n tx.create(draftRef.collection('versions').doc(versionId), {\n layoutId: input.id,\n hostId: input.hostId,\n displayName: AI_DRAFT_VERSION_NAME,\n nodes,\n ...stamps,\n })\n } else if (input.kind === 'screen') {\n versionId = createResourceUid()\n const slug = aiDraftScreenSlug(input.slug, name, rows, routingMap)\n tx.create(draftRef, {\n ...allowListed('screen', { displayName: name, slug, versionId }),\n nameLower: nameSearchKey(name),\n ...stamps,\n })\n tx.create(draftRef.collection('versions').doc(versionId), {\n screenId: input.id,\n hostId: input.hostId,\n displayName: AI_DRAFT_VERSION_NAME,\n nodes,\n ...(input.layoutId ? { layoutId: input.layoutId } : {}),\n ...stamps,\n })\n } else if (input.kind === 'template') {\n tx.create(draftRef, {\n ...allowListed('template', {\n kind: 'page',\n displayName: name,\n nodes,\n slug: input.slug || undefined,\n }),\n source: { type: 'authored' },\n ...stamps,\n })\n } else if (input.kind === 'component') {\n tx.create(draftRef, {\n ...allowListed('component', {\n displayName: name,\n rootId: input.rootId,\n nodes,\n props: input.props?.length ? [...input.props] : undefined,\n }),\n ...stamps,\n })\n } else {\n const form = input.form as AiFormDraftDeclaration\n const captioned = encodeStoredNodes(withFormCaption(input.nodes, form.formNodeId, name)) ?? packed\n tx.create(draftRef, {\n ...allowListed('form', {\n displayName: name,\n slug: normalizeFormSlug(name) || input.id,\n fields: form.fields,\n consentFieldName: form.consentFieldName || undefined,\n routing: form.routing ?? undefined,\n rootId: form.rootId,\n nodes: Buffer.from(captioned),\n }),\n ...stamps,\n })\n }\n return {\n ok: true,\n replayed: false,\n id: input.id,\n versionId,\n name,\n hostSubdomain: subdomainOf(host),\n }\n })\n}\n\nexport type AiDraftNodesUpdate =\n | { ok: true; changed: boolean }\n | { ok: false; status: 404; error: string }\n\n/**\n * Change the tree of a versioned draft the job wrote, in one transaction:\n * read the first version as it is stored NOW — a member may have opened and\n * saved it — hand it to `update`, and store what comes back with a fresh\n * `updatedAt`, which is what the besigner's save guard compares. `update`\n * answering `null` changes nothing: a pass run again after its write.\n */\nexport async function updateAiDraftNodes(\n firestore: Firestore,\n input: {\n kind: AiVersionedDraftKind\n hostId: string\n id: string\n now: Date\n update: (nodes: NodesMap) => NodesMap | null\n },\n): Promise<AiDraftNodesUpdate> {\n const draftRef = draftCollection(firestore, input.hostId, input.kind).doc(input.id)\n return firestore.runTransaction(async (tx): Promise<AiDraftNodesUpdate> => {\n const draft = await tx.get(draftRef)\n const versionId = draft.exists && draft.get('deletedAt') == null ? draft.get('versionId') : null\n if (typeof versionId !== 'string' || !versionId) {\n return { ok: false, status: 404, error: 'The draft is no longer on the site' }\n }\n const versionRef = draftRef.collection('versions').doc(versionId)\n const version = await tx.get(versionRef)\n const nodes = version.exists ? decodeStoredNodes<NodesMap>(version.get('nodes')) : null\n if (!nodes) return { ok: false, status: 404, error: 'The draft is no longer on the site' }\n const next = input.update(nodes)\n if (!next) return { ok: true, changed: false }\n const packed = encodeStoredNodes(next)\n if (!packed) throw new Error('an AI draft needs a node map')\n tx.update(versionRef, { nodes: Buffer.from(packed), updatedAt: input.now })\n return { ok: true, changed: true }\n })\n}\n\n/**\n * Put a search title and description on a draft screen the job wrote, each\n * only where the draft has none: a value a member typed since is theirs.\n */\nexport async function writeAiDraftScreenSeo(\n firestore: Firestore,\n input: {\n hostId: string\n id: string\n seo: { title?: string | null; description?: string | null }\n now: Date\n },\n): Promise<void> {\n const ref = draftCollection(firestore, input.hostId, 'screen').doc(input.id)\n await firestore.runTransaction(async (tx) => {\n const screen = await tx.get(ref)\n if (!screen.exists || screen.get('deletedAt') != null) return\n const patch: Record<string, unknown> = {}\n for (const key of ['title', 'description'] as const) {\n const value = String(input.seo[key] ?? '').trim()\n if (value && !String(screen.get(`seo.${key}`) ?? '').trim()) patch[`seo.${key}`] = value\n }\n if (Object.keys(patch).length) tx.update(ref, { ...patch, updatedAt: input.now })\n })\n}\n\n/** The site's subdomain, which a console link names the site by. */\nexport async function aiSiteSubdomain(firestore: Firestore, hostId: string): Promise<string | null> {\n return subdomainOf(await firestore.collection('hosts').doc(hostId).get())\n}\n\n/**\n * What a door says before a job that writes a draft of this kind exists: a\n * site is named, and it is the job's own org's; whatever the kind checks of\n * its own; then the allowance. `null` admits the job.\n */\nexport async function aiDraftAdmissionRefusal(\n firestore: Firestore,\n input: {\n orgId: string\n hostId: string | null\n kind: AiDraftKind\n /** What the refusal calls the job's output; the kind when absent. */\n noun?: string\n org: object | null\n /** The kind's own check, asked once the site is known to be the org's. */\n ownCheck?: (hostId: string) => Promise<AiJobAdmissionRefusal | null>\n },\n): Promise<AiJobAdmissionRefusal | null> {\n if (!input.hostId) {\n return {\n status: 400,\n error: `Open the site the ${input.noun ?? input.kind} is for before starting the job`,\n }\n }\n const owner = await resolveOrgIdForHost(input.hostId)\n if (!owner || owner !== input.orgId) return { status: 404, error: 'Unknown site' }\n const own = await input.ownCheck?.(input.hostId)\n if (own) return own\n const refusal = await aiDraftAllowanceRefusal(firestore, {\n kind: input.kind,\n hostId: input.hostId,\n org: input.org as Partial<AglynOrgBilling> | null,\n })\n return refusal ? { status: 403, error: refusal } : null\n}\n"],"names":["createResourceUid","uniqueDuplicateName","FORM_COMPONENT_ID","normalizeFormSlug","nameSearchKey","checkDatasetQuota","checkEntitlement","checkQuota","resolveEffectivePlan","resolveOrgEntitlements","billableScreenIds","normalizeScreenSlug","reservedScreenRouteSegment","SCREEN_ROOT_PATH","decodeStoredNodes","encodeStoredNodes","resolveOrgIdForHost","aiUnrestrictedPlanCapabilities","AI_DRAFT_FIELDS","layout","template","form","component","screen","AI_DRAFT_VERSION_FIELDS","AI_DRAFT_SCREEN_VERSION_FIELDS","AI_DRAFT_VERSION_NAME","AI_DRAFT_ENTITLEMENT_REFUSAL","AI_DRAFT_BANDS","collection","quotaKey","label","entitlement","draftCollection","firestore","hostId","kind","doc","siblingsOf","select","siblingRows","snapshot","docs","map","id","name","String","get","slug","sourceType","deletedAt","deleted","subdomainOf","host","subdomain","aiDraftBandRefusal","rows","org","routingMap","band","used","filter","row","undefined","length","size","quota","allowed","limit","AI_PLAN_DRAFT_KINDS","bandLabel","count","replace","planCreation","left","reason","Number","POSITIVE_INFINITY","isFinite","refusal","Math","max","countsRows","aiPlanCapabilitiesFrom","unrestricted","datasets","reusableComponents","create","dataset","freeTaste","readAiPlanCapabilities","input","counted","snapshots","Promise","all","Object","fromEntries","index","aiDraftAllowanceRefusal","resolve","aiDraftScreenSlug","requested","usable","taken","Set","values","path","value","has","base","attempt","recordOf","draft","versionId","seo","hostSubdomain","readAiDraft","exists","readAiTemplateDraftNodes","readAiDraftComponentProps","props","Array","isArray","prop","readAiDraftNodes","draftRef","version","nodes","allowListed","data","entries","key","withFormCaption","formNodeId","node","componentId","formName","writeAiDraft","packed","Error","rootId","hostRef","runTransaction","tx","existing","siblings","ok","status","error","replayed","stamps","createdAt","now","updatedAt","createdBy","uid","Buffer","from","displayName","layoutId","nameLower","screenId","source","type","captioned","fields","consentFieldName","routing","updateAiDraftNodes","versionRef","next","update","changed","writeAiDraftScreenSeo","ref","patch","trim","keys","aiSiteSubdomain","aiDraftAdmissionRefusal","noun","owner","orgId","own","ownCheck"],"mappings":"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"}
@@ -1126,7 +1126,9 @@
1126
1126
  padding: 200
1127
1127
  },
1128
1128
  presets: [
1129
- 'Email section'
1129
+ 'Email section',
1130
+ 'Header',
1131
+ 'Footer'
1130
1132
  ]
1131
1133
  },
1132
1134
  emailSpacer: {
@@ -5299,7 +5301,7 @@
5299
5301
  };
5300
5302
  /** The prompt catalog of each surface. */ export const AI_PALETTE_CATALOG = {
5301
5303
  screen: "Surface: screen. Root: the document wrapper (componentId \"div\", id \"_@_\"); every other node carries an allowed componentId.\nElements (id (name): purpose — children — props; * = required):\n- collectionCategories (Category Pills): A pill per collection category, each filtering the listing. — leaf — collectionSlug=text≤200, allLabel=text≤200\n- collectionEntries (Collection Entries): Repeats its children once per entry in a content collection. — holds any — collectionSlug=text≤200, entriesLimit=text≤200, filterCategory=text≤200, filterTag=text≤200, searchMode=filter|suggest\n- collectionEntryAuthor (Entry Author): The author card for an entry — portrait, byline and bio. — leaf — name=text≤200, bio=text≤2000, image=media, pageUrl=url, url=url\n- collectionEntryBody (Entry Body): The current entry's markdown body, on an entry template. — leaf — markdown=text≤200\n- collectionEntryMeta (Entry Meta): The byline row for an entry — author, date, category and tags. — leaf — date=text≤200, dateFormat=default|monthYear|mediumDate|longDate|iso, author=text≤200, category=text≤200, authorPageUrl=url\n- collectionRelated (Related Posts): Other entries sharing this one's category or tags. — leaf — heading=text≤200, layout=list|cards, headingVariant=displayXl|h1|h2|h3|h4|h5|…, titleVariant=displayXl|h1|h2|h3|h4|h5|…, dateFormat=default|monthYear|mediumDate|longDate|iso\n- collectionSearch (Collection Search): A search box for a content collection, with a suggestions dropdown. — leaf — collectionSlug=text≤200, searchPlaceholder=text≤200\n- collectionShare (Share Bar): Share buttons for the current page, plus a copy link. — leaf — heading=text≤200\n- contentAuthorProfile (Author Profile): Who an author page is about — portrait, name, role, bio and links. — leaf — name=text≤200, bio=text≤2000, image=media, jobTitle=text≤200, url=url\n- dataTable (Table): A grid of rows and columns — a feature matrix, a spec sheet, a comparison. — leaf — headerRow=boolean, emphasizeColumn=text≤200\n- form (Form): Collects its Form Fields and sends the answers to you, or into a dataset. — holds any — formName=text≤200, datasetName=text≤200, afterSubmit=message|redirect|reveal, redirectScreenId=screen, redirectUrl=url\n- formField (Form Field): One input inside a form — text, select, radio, checkbox or rating. — leaf — fieldName=text≤200, label=text≤200, placeholder=text≤200, fieldType=text|email|textarea|select|radio|checkbox|…, options=text≤2000\n- image (Image): A picture from your media library or any URL, with fit, size and an optional link. — leaf — src=media, objectFit=cover|contain|fill|none|scale-down, loading=lazy|eager, screenId=screen, href=url\n- muiAccordion (Accordion): Header that expands to reveal its details — FAQs, long forms, anything progressive. — holds only muiAccordionSummary|muiAccordionDetails — defaultExpanded=boolean, disableGutters=boolean, disabled=boolean\n- muiAccordionDetails (Accordion Details): The region an accordion reveals when expanded. — holds any — no props\n- muiAccordionSummary (Accordion Summary): The clickable header row of an accordion. — leaf — children=text≤2000, screenId=screen\n- muiAppBar (App Bar): The bar across the top of the site — put a Toolbar Content inside it. — holds any — component=div|section|article|aside|nav|header|…, ariaLabel=text≤200, color=default|inherit|transparent|primary|secondary|tertiary, position=absolute|fixed|relative|static|sticky\n- muiBox (Box): Generic container with no styling of its own — style it from the styles panel. — holds any — component=div|span|p|figure|figcaption|blockquote|…, colorScheme=site|light|dark\n- muiBreadcrumbs (Breadcrumbs): Trail showing where a page sits. — holds any — separator=text≤200, maxItems=text≤200, itemsBeforeCollapse=text≤200, itemsAfterCollapse=text≤200, expandText=text≤200\n- muiButton (Button): A button — or a link styled as one, if you point it at a screen or URL. — leaf — children=text≤40, color=inherit|primary|secondary|success|error|info|…, size=inherit|small|medium|large, variant=text|outlined|contained, target=_self|_blank|custom\n- muiCard (Card): Surface that groups a header, content and actions about one subject. — holds any — component=div|section|article|aside|nav|header|…, ariaLabel=text≤200, variant=elevation|outlined, elevation=text≤200\n- muiCardActions (Card Actions): The button row at the bottom of a card. — holds any — disableSpacing=boolean\n- muiCardContent (Card Content): The padded body region of a card. — holds any — no props\n- muiCardHeader (Card Header): The title and subheader at the top of a card. — leaf — title=text≤200, subheader=text≤200\n- muiContainer (Container): Centers your page content and caps how wide it gets. — holds any — component=div|section|article|aside|nav|header|…, ariaLabel=text≤200, fixed=boolean, maxWidth=xs|sm|md|lg|xl, colorScheme=site|light|dark\n- muiGrid (Grid): Responsive 12-column layout. — holds any — component=div|section|article|aside|nav|header|…, ariaLabel=text≤200, direction=row|row-reverse, wrap=wrap|nowrap|wrap-reverse, colorScheme=site|light|dark\n- muiImageList (Image List): Dense grid of images — standard, quilted, masonry or woven. — holds only muiImageListItem — variant=standard|quilted|masonry|woven, cols=text≤200, gap=text≤200, rowHeight=text≤200\n- muiImageListItem (Image List Item): One tile of an image list, with an optional caption bar. — holds any — title=text≤200, subtitle=text≤200, barPosition=bottom|top|below, cols=text≤200, rows=text≤200\n- muiList (List): Vertical list of items, optionally under a sticky heading. — holds only muiListItem — subheader=text≤200, dense=boolean, disablePadding=boolean\n- muiListItem (List Item): One row of a list. — holds only muiListItemText — divider=boolean, alignItems=center|flex-start, dense=boolean, disableGutters=boolean, disablePadding=boolean\n- muiListItemText (List Item Text): The primary and secondary text pair inside a list row. — leaf — primary=text≤2000, secondary=text≤2000, inset=boolean\n- muiScreenLink (Screen Link): Links to another screen by id, so it survives a slug change. — leaf — children=text≤40, target=_self|_blank|custom, renderAs=button|link|linkButton, color=inherit|primary|secondary|success|error|info|…, size=inherit|small|medium|large\n- muiStack (Stack): Lays children out in a row or column with even spacing — the workhorse for most layouts. — holds any — component=div|section|article|aside|nav|header|…, direction=column|column-reverse|row|row-reverse, justifyContent=flex-start|center|flex-end|space-between|space-around|space-evenly, alignItems=stretch|flex-start|center|flex-end|baseline, flexWrap=nowrap|wrap|wrap-reverse\n- muiToolbar (Toolbar Content): The content row inside an App Bar — brand, links, actions. — holds any — component=div|section|article|aside|nav|header|…, ariaLabel=text≤200, disableGutters=boolean, variant=dense|regular\n- muiTypography (Typography): A block of text in one of the theme's type styles. — leaf — children=text≤2000, variant=displayXl|h1|h2|h3|h4|h5|…, component=h1|h2|h3|h4|h5|h6|…, align=inherit|left|center|right|justify, noWrap=boolean\n- searchBox (Search Box): A search field that opens the site's search page. — leaf — placeholder=text≤200, defaultValue=text≤200\n- section (Section): Groups components inside a semantic HTML element (section, article, nav, …). — holds any — element=section|div|article|aside|nav|header|…, ariaLabel=text≤200, colorScheme=site|light|dark\n- socialLinks (Social Links): A row of icon links to whichever social profiles you fill in. — leaf — twitter=text≤200, instagram=text≤200, facebook=text≤200, linkedin=text≤200, youtube=text≤200\n- videoEmbed (Video): Embeds a YouTube or Vimeo player from the video's URL. — leaf — url=url, height=string\nNamed blocks to imitate: App Bar (muiAppBar); List (muiList); List Item (muiListItem); Image Gallery (muiStack); Testimonials (muiStack); Pricing Table (muiStack); FAQ (muiStack); Announcement Bar (muiStack); Nav Bar (muiAppBar); Hero (muiStack); Feature Grid (muiStack); Image + Text (muiStack); Call to Action (muiStack); Footer (section); Collection Entries (collectionEntries); Dropdown Panel (muiStack); Grid (muiGrid); Grid Cell (muiGrid); Card (muiCard); Card Outlined (muiCard); Accordion (muiAccordion); FAQ (muiStack); Image List (muiImageList); Image List Masonry (muiImageList); Image List Item (muiImageListItem); Breadcrumbs (muiBreadcrumbs); Contact Form (form); Contact Section (muiStack).",
5302
- email: 'Surface: email. Root: the document wrapper (componentId "div", id "_@_"); every other node carries an allowed componentId.\nElements (id (name): purpose — children — props; * = required):\n- emailButton (Email button): A call-to-action button that survives every mail client. — leaf — children=text≤40, href=url, backgroundColor=string, color=string, align=left|center|right\n- emailDivider (Email divider): A horizontal rule between email sections. — leaf — color=string\n- emailImage (Email image): An email-safe image, capped at 600px wide, optionally linked. — leaf — src=media, alt=text≤200, width=text≤200, href=url, align=left|center|right\n- emailProduct (Email product): A product card resolved at send time, so it never goes stale. — leaf — buttonLabel=text≤40\n- emailSection (Email section): An email-safe band with its own background color and padding. — holds any — backgroundColor=string, padding=text≤200, align=left|center|right\n- emailSpacer (Email spacer): Fixed vertical whitespace between email blocks. — leaf — height=text≤200\n- emailText (Email text): A paragraph of email copy in one of the preset styles. — leaf — children=text≤2000, variant=heading|subheading|body|caption, color=string, align=left|center|right',
5304
+ email: 'Surface: email. Root: the document wrapper (componentId "div", id "_@_"); every other node carries an allowed componentId.\nElements (id (name): purpose — children — props; * = required):\n- emailButton (Email button): A call-to-action button that survives every mail client. — leaf — children=text≤40, href=url, backgroundColor=string, color=string, align=left|center|right\n- emailDivider (Email divider): A horizontal rule between email sections. — leaf — color=string\n- emailImage (Email image): An email-safe image, capped at 600px wide, optionally linked. — leaf — src=media, alt=text≤200, width=text≤200, href=url, align=left|center|right\n- emailProduct (Email product): A product card resolved at send time, so it never goes stale. — leaf — buttonLabel=text≤40\n- emailSection (Email section): An email-safe band with its own background color and padding. — holds any — backgroundColor=string, padding=text≤200, align=left|center|right\n- emailSpacer (Email spacer): Fixed vertical whitespace between email blocks. — leaf — height=text≤200\n- emailText (Email text): A paragraph of email copy in one of the preset styles. — leaf — children=text≤2000, variant=heading|subheading|body|caption, color=string, align=left|center|right\nNamed blocks to imitate: Header (emailSection); Footer (emailSection).',
5303
5305
  form: 'Surface: form. Root: one "form" node.\nElements (id (name): purpose — children — props; * = required):\n- form (Form): Collects its Form Fields and sends the answers to you, or into a dataset. — holds any — formName=text≤200, datasetName=text≤200, afterSubmit=message|redirect|reveal, redirectScreenId=screen, redirectUrl=url\n- formField (Form Field): One input inside a form — text, select, radio, checkbox or rating. — leaf — fieldName=text≤200, label=text≤200, placeholder=text≤200, fieldType=text|email|textarea|select|radio|checkbox|…, options=text≤2000\nNamed blocks to imitate: Contact Form (form).',
5304
5306
  layout: "Surface: layout. Root: the document wrapper (componentId \"div\", id \"_@_\"); every other node carries an allowed componentId.\nElements (id (name): purpose — children — props; * = required):\n- collectionCategories (Category Pills): A pill per collection category, each filtering the listing. — leaf — collectionSlug=text≤200, allLabel=text≤200\n- collectionEntries (Collection Entries): Repeats its children once per entry in a content collection. — holds any — collectionSlug=text≤200, entriesLimit=text≤200, filterCategory=text≤200, filterTag=text≤200, searchMode=filter|suggest\n- collectionEntryAuthor (Entry Author): The author card for an entry — portrait, byline and bio. — leaf — name=text≤200, bio=text≤2000, image=media, pageUrl=url, url=url\n- collectionEntryBody (Entry Body): The current entry's markdown body, on an entry template. — leaf — markdown=text≤200\n- collectionEntryMeta (Entry Meta): The byline row for an entry — author, date, category and tags. — leaf — date=text≤200, dateFormat=default|monthYear|mediumDate|longDate|iso, author=text≤200, category=text≤200, authorPageUrl=url\n- collectionRelated (Related Posts): Other entries sharing this one's category or tags. — leaf — heading=text≤200, layout=list|cards, headingVariant=displayXl|h1|h2|h3|h4|h5|…, titleVariant=displayXl|h1|h2|h3|h4|h5|…, dateFormat=default|monthYear|mediumDate|longDate|iso\n- collectionSearch (Collection Search): A search box for a content collection, with a suggestions dropdown. — leaf — collectionSlug=text≤200, searchPlaceholder=text≤200\n- collectionShare (Share Bar): Share buttons for the current page, plus a copy link. — leaf — heading=text≤200\n- contentAuthorProfile (Author Profile): Who an author page is about — portrait, name, role, bio and links. — leaf — name=text≤200, bio=text≤2000, image=media, jobTitle=text≤200, url=url\n- dataTable (Table): A grid of rows and columns — a feature matrix, a spec sheet, a comparison. — leaf — headerRow=boolean, emphasizeColumn=text≤200\n- form (Form): Collects its Form Fields and sends the answers to you, or into a dataset. — holds any — formName=text≤200, datasetName=text≤200, afterSubmit=message|redirect|reveal, redirectScreenId=screen, redirectUrl=url\n- formField (Form Field): One input inside a form — text, select, radio, checkbox or rating. — leaf — fieldName=text≤200, label=text≤200, placeholder=text≤200, fieldType=text|email|textarea|select|radio|checkbox|…, options=text≤2000\n- image (Image): A picture from your media library or any URL, with fit, size and an optional link. — leaf — src=media, objectFit=cover|contain|fill|none|scale-down, loading=lazy|eager, screenId=screen, href=url\n- layoutSlot (Layout Slot): Marks where each bound screen's content renders inside this layout. — leaf — caption=text≤200, component=main|div|section|article\n- muiAccordion (Accordion): Header that expands to reveal its details — FAQs, long forms, anything progressive. — holds only muiAccordionSummary|muiAccordionDetails — defaultExpanded=boolean, disableGutters=boolean, disabled=boolean\n- muiAccordionDetails (Accordion Details): The region an accordion reveals when expanded. — holds any — no props\n- muiAccordionSummary (Accordion Summary): The clickable header row of an accordion. — leaf — children=text≤2000, screenId=screen\n- muiAppBar (App Bar): The bar across the top of the site — put a Toolbar Content inside it. — holds any — component=div|section|article|aside|nav|header|…, ariaLabel=text≤200, color=default|inherit|transparent|primary|secondary|tertiary, position=absolute|fixed|relative|static|sticky\n- muiBox (Box): Generic container with no styling of its own — style it from the styles panel. — holds any — component=div|span|p|figure|figcaption|blockquote|…, colorScheme=site|light|dark\n- muiBreadcrumbs (Breadcrumbs): Trail showing where a page sits. — holds any — separator=text≤200, maxItems=text≤200, itemsBeforeCollapse=text≤200, itemsAfterCollapse=text≤200, expandText=text≤200\n- muiButton (Button): A button — or a link styled as one, if you point it at a screen or URL. — leaf — children=text≤40, color=inherit|primary|secondary|success|error|info|…, size=inherit|small|medium|large, variant=text|outlined|contained, target=_self|_blank|custom\n- muiCard (Card): Surface that groups a header, content and actions about one subject. — holds any — component=div|section|article|aside|nav|header|…, ariaLabel=text≤200, variant=elevation|outlined, elevation=text≤200\n- muiCardActions (Card Actions): The button row at the bottom of a card. — holds any — disableSpacing=boolean\n- muiCardContent (Card Content): The padded body region of a card. — holds any — no props\n- muiCardHeader (Card Header): The title and subheader at the top of a card. — leaf — title=text≤200, subheader=text≤200\n- muiContainer (Container): Centers your page content and caps how wide it gets. — holds any — component=div|section|article|aside|nav|header|…, ariaLabel=text≤200, fixed=boolean, maxWidth=xs|sm|md|lg|xl, colorScheme=site|light|dark\n- muiGrid (Grid): Responsive 12-column layout. — holds any — component=div|section|article|aside|nav|header|…, ariaLabel=text≤200, direction=row|row-reverse, wrap=wrap|nowrap|wrap-reverse, colorScheme=site|light|dark\n- muiImageList (Image List): Dense grid of images — standard, quilted, masonry or woven. — holds only muiImageListItem — variant=standard|quilted|masonry|woven, cols=text≤200, gap=text≤200, rowHeight=text≤200\n- muiImageListItem (Image List Item): One tile of an image list, with an optional caption bar. — holds any — title=text≤200, subtitle=text≤200, barPosition=bottom|top|below, cols=text≤200, rows=text≤200\n- muiList (List): Vertical list of items, optionally under a sticky heading. — holds only muiListItem — subheader=text≤200, dense=boolean, disablePadding=boolean\n- muiListItem (List Item): One row of a list. — holds only muiListItemText — divider=boolean, alignItems=center|flex-start, dense=boolean, disableGutters=boolean, disablePadding=boolean\n- muiListItemText (List Item Text): The primary and secondary text pair inside a list row. — leaf — primary=text≤2000, secondary=text≤2000, inset=boolean\n- muiScreenLink (Screen Link): Links to another screen by id, so it survives a slug change. — leaf — children=text≤40, target=_self|_blank|custom, renderAs=button|link|linkButton, color=inherit|primary|secondary|success|error|info|…, size=inherit|small|medium|large\n- muiStack (Stack): Lays children out in a row or column with even spacing — the workhorse for most layouts. — holds any — component=div|section|article|aside|nav|header|…, direction=column|column-reverse|row|row-reverse, justifyContent=flex-start|center|flex-end|space-between|space-around|space-evenly, alignItems=stretch|flex-start|center|flex-end|baseline, flexWrap=nowrap|wrap|wrap-reverse\n- muiToolbar (Toolbar Content): The content row inside an App Bar — brand, links, actions. — holds any — component=div|section|article|aside|nav|header|…, ariaLabel=text≤200, disableGutters=boolean, variant=dense|regular\n- muiTypography (Typography): A block of text in one of the theme's type styles. — leaf — children=text≤2000, variant=displayXl|h1|h2|h3|h4|h5|…, component=h1|h2|h3|h4|h5|h6|…, align=inherit|left|center|right|justify, noWrap=boolean\n- searchBox (Search Box): A search field that opens the site's search page. — leaf — placeholder=text≤200, defaultValue=text≤200\n- section (Section): Groups components inside a semantic HTML element (section, article, nav, …). — holds any — element=section|div|article|aside|nav|header|…, ariaLabel=text≤200, colorScheme=site|light|dark\n- socialLinks (Social Links): A row of icon links to whichever social profiles you fill in. — leaf — twitter=text≤200, instagram=text≤200, facebook=text≤200, linkedin=text≤200, youtube=text≤200\n- videoEmbed (Video): Embeds a YouTube or Vimeo player from the video's URL. — leaf — url=url, height=string\nNamed blocks to imitate: App Bar (muiAppBar); List (muiList); List Item (muiListItem); Image Gallery (muiStack); Testimonials (muiStack); Pricing Table (muiStack); FAQ (muiStack); Announcement Bar (muiStack); Nav Bar (muiAppBar); Hero (muiStack); Feature Grid (muiStack); Image + Text (muiStack); Call to Action (muiStack); Footer (section); Collection Entries (collectionEntries); Dropdown Panel (muiStack); Grid (muiGrid); Grid Cell (muiGrid); Card (muiCard); Card Outlined (muiCard); Accordion (muiAccordion); FAQ (muiStack); Image List (muiImageList); Image List Masonry (muiImageList); Image List Item (muiImageListItem); Breadcrumbs (muiBreadcrumbs); Contact Form (form); Contact Section (muiStack).",
5305
5307
  component: "Surface: component. Root: the document wrapper (componentId \"div\", id \"_@_\"); every other node carries an allowed componentId.\nElements (id (name): purpose — children — props; * = required):\n- collectionCategories (Category Pills): A pill per collection category, each filtering the listing. — leaf — collectionSlug=text≤200, allLabel=text≤200\n- collectionEntries (Collection Entries): Repeats its children once per entry in a content collection. — holds any — collectionSlug=text≤200, entriesLimit=text≤200, filterCategory=text≤200, filterTag=text≤200, searchMode=filter|suggest\n- collectionEntryAuthor (Entry Author): The author card for an entry — portrait, byline and bio. — leaf — name=text≤200, bio=text≤2000, image=media, pageUrl=url, url=url\n- collectionEntryBody (Entry Body): The current entry's markdown body, on an entry template. — leaf — markdown=text≤200\n- collectionEntryMeta (Entry Meta): The byline row for an entry — author, date, category and tags. — leaf — date=text≤200, dateFormat=default|monthYear|mediumDate|longDate|iso, author=text≤200, category=text≤200, authorPageUrl=url\n- collectionRelated (Related Posts): Other entries sharing this one's category or tags. — leaf — heading=text≤200, layout=list|cards, headingVariant=displayXl|h1|h2|h3|h4|h5|…, titleVariant=displayXl|h1|h2|h3|h4|h5|…, dateFormat=default|monthYear|mediumDate|longDate|iso\n- collectionSearch (Collection Search): A search box for a content collection, with a suggestions dropdown. — leaf — collectionSlug=text≤200, searchPlaceholder=text≤200\n- collectionShare (Share Bar): Share buttons for the current page, plus a copy link. — leaf — heading=text≤200\n- contentAuthorProfile (Author Profile): Who an author page is about — portrait, name, role, bio and links. — leaf — name=text≤200, bio=text≤2000, image=media, jobTitle=text≤200, url=url\n- dataTable (Table): A grid of rows and columns — a feature matrix, a spec sheet, a comparison. — leaf — headerRow=boolean, emphasizeColumn=text≤200\n- form (Form): Collects its Form Fields and sends the answers to you, or into a dataset. — holds any — formName=text≤200, datasetName=text≤200, afterSubmit=message|redirect|reveal, redirectScreenId=screen, redirectUrl=url\n- formField (Form Field): One input inside a form — text, select, radio, checkbox or rating. — leaf — fieldName=text≤200, label=text≤200, placeholder=text≤200, fieldType=text|email|textarea|select|radio|checkbox|…, options=text≤2000\n- icon (Icon): A single icon from the library, at the size and color you pick. — leaf — iconId=string, size=text≤200, color=text≤200\n- image (Image): A picture from your media library or any URL, with fit, size and an optional link. — leaf — src=media, objectFit=cover|contain|fill|none|scale-down, loading=lazy|eager, screenId=screen, href=url\n- muiAccordion (Accordion): Header that expands to reveal its details — FAQs, long forms, anything progressive. — holds only muiAccordionSummary|muiAccordionDetails — defaultExpanded=boolean, disableGutters=boolean, disabled=boolean\n- muiAccordionDetails (Accordion Details): The region an accordion reveals when expanded. — holds any — no props\n- muiAccordionSummary (Accordion Summary): The clickable header row of an accordion. — leaf — children=text≤2000, screenId=screen\n- muiAppBar (App Bar): The bar across the top of the site — put a Toolbar Content inside it. — holds any — component=div|section|article|aside|nav|header|…, ariaLabel=text≤200, color=default|inherit|transparent|primary|secondary|tertiary, position=absolute|fixed|relative|static|sticky\n- muiBox (Box): Generic container with no styling of its own — style it from the styles panel. — holds any — component=div|span|p|figure|figcaption|blockquote|…, colorScheme=site|light|dark\n- muiBreadcrumbs (Breadcrumbs): Trail showing where a page sits. — holds any — separator=text≤200, maxItems=text≤200, itemsBeforeCollapse=text≤200, itemsAfterCollapse=text≤200, expandText=text≤200\n- muiButton (Button): A button — or a link styled as one, if you point it at a screen or URL. — leaf — children=text≤40, color=inherit|primary|secondary|success|error|info|…, size=inherit|small|medium|large, variant=text|outlined|contained, target=_self|_blank|custom\n- muiCard (Card): Surface that groups a header, content and actions about one subject. — holds any — component=div|section|article|aside|nav|header|…, ariaLabel=text≤200, variant=elevation|outlined, elevation=text≤200\n- muiCardActions (Card Actions): The button row at the bottom of a card. — holds any — disableSpacing=boolean\n- muiCardContent (Card Content): The padded body region of a card. — holds any — no props\n- muiCardHeader (Card Header): The title and subheader at the top of a card. — leaf — title=text≤200, subheader=text≤200\n- muiContainer (Container): Centers your page content and caps how wide it gets. — holds any — component=div|section|article|aside|nav|header|…, ariaLabel=text≤200, fixed=boolean, maxWidth=xs|sm|md|lg|xl, colorScheme=site|light|dark\n- muiGrid (Grid): Responsive 12-column layout. — holds any — component=div|section|article|aside|nav|header|…, ariaLabel=text≤200, direction=row|row-reverse, wrap=wrap|nowrap|wrap-reverse, colorScheme=site|light|dark\n- muiImageList (Image List): Dense grid of images — standard, quilted, masonry or woven. — holds only muiImageListItem — variant=standard|quilted|masonry|woven, cols=text≤200, gap=text≤200, rowHeight=text≤200\n- muiImageListItem (Image List Item): One tile of an image list, with an optional caption bar. — holds any — title=text≤200, subtitle=text≤200, barPosition=bottom|top|below, cols=text≤200, rows=text≤200\n- muiList (List): Vertical list of items, optionally under a sticky heading. — holds only muiListItem — subheader=text≤200, dense=boolean, disablePadding=boolean\n- muiListItem (List Item): One row of a list. — holds only muiListItemText — divider=boolean, alignItems=center|flex-start, dense=boolean, disableGutters=boolean, disablePadding=boolean\n- muiListItemText (List Item Text): The primary and secondary text pair inside a list row. — leaf — primary=text≤2000, secondary=text≤2000, inset=boolean\n- muiScreenLink (Screen Link): Links to another screen by id, so it survives a slug change. — leaf — children=text≤40, target=_self|_blank|custom, renderAs=button|link|linkButton, color=inherit|primary|secondary|success|error|info|…, size=inherit|small|medium|large\n- muiStack (Stack): Lays children out in a row or column with even spacing — the workhorse for most layouts. — holds any — component=div|section|article|aside|nav|header|…, direction=column|column-reverse|row|row-reverse, justifyContent=flex-start|center|flex-end|space-between|space-around|space-evenly, alignItems=stretch|flex-start|center|flex-end|baseline, flexWrap=nowrap|wrap|wrap-reverse\n- muiToolbar (Toolbar Content): The content row inside an App Bar — brand, links, actions. — holds any — component=div|section|article|aside|nav|header|…, ariaLabel=text≤200, disableGutters=boolean, variant=dense|regular\n- muiTypography (Typography): A block of text in one of the theme's type styles. — leaf — children=text≤2000, variant=displayXl|h1|h2|h3|h4|h5|…, component=h1|h2|h3|h4|h5|h6|…, align=inherit|left|center|right|justify, noWrap=boolean\n- searchBox (Search Box): A search field that opens the site's search page. — leaf — placeholder=text≤200, defaultValue=text≤200\n- section (Section): Groups components inside a semantic HTML element (section, article, nav, …). — holds any — element=section|div|article|aside|nav|header|…, ariaLabel=text≤200, colorScheme=site|light|dark\n- socialLinks (Social Links): A row of icon links to whichever social profiles you fill in. — leaf — twitter=text≤200, instagram=text≤200, facebook=text≤200, linkedin=text≤200, youtube=text≤200\n- videoEmbed (Video): Embeds a YouTube or Vimeo player from the video's URL. — leaf — url=url, height=string\nNamed blocks to imitate: App Bar (muiAppBar); List (muiList); List Item (muiListItem); Image Gallery (muiStack); Testimonials (muiStack); Pricing Table (muiStack); FAQ (muiStack); Announcement Bar (muiStack); Nav Bar (muiAppBar); Hero (muiStack); Feature Grid (muiStack); Image + Text (muiStack); Call to Action (muiStack); Footer (section); Collection Entries (collectionEntries); Dropdown Panel (muiStack); Grid (muiGrid); Grid Cell (muiGrid); Card (muiCard); Card Outlined (muiCard); Accordion (muiAccordion); FAQ (muiStack); Image List (muiImageList); Image List Masonry (muiImageList); Image List Item (muiImageListItem); Breadcrumbs (muiBreadcrumbs); Contact Form (form); Contact Section (muiStack)."