@aglyn/plugins-ai 1.0.0-beta.237 → 1.0.0-beta.239

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.
Files changed (172) hide show
  1. package/package.json +17 -17
  2. package/src/lib/components/ai-credits-prompt.component.d.ts +11 -4
  3. package/src/lib/components/ai-credits-prompt.component.js +84 -34
  4. package/src/lib/components/ai-credits-prompt.component.js.map +1 -1
  5. package/src/lib/components/ai-job-cancel.component.d.ts +38 -0
  6. package/src/lib/components/ai-job-cancel.component.js +156 -0
  7. package/src/lib/components/ai-job-cancel.component.js.map +1 -0
  8. package/src/lib/components/ai-job-links.js +8 -0
  9. package/src/lib/components/ai-job-links.js.map +1 -1
  10. package/src/lib/components/ai-job-plan.component.js +12 -1
  11. package/src/lib/components/ai-job-plan.component.js.map +1 -1
  12. package/src/lib/components/ai-jobs-list-page.component.js +141 -72
  13. package/src/lib/components/ai-jobs-list-page.component.js.map +1 -1
  14. package/src/lib/components/ai-jobs-store.d.ts +12 -0
  15. package/src/lib/components/ai-jobs-store.js +15 -0
  16. package/src/lib/components/ai-jobs-store.js.map +1 -1
  17. package/src/lib/components/ai-site-build-page.component.js +22 -2
  18. package/src/lib/components/ai-site-build-page.component.js.map +1 -1
  19. package/src/lib/components/ai-usage-meter-refresh.d.ts +35 -0
  20. package/src/lib/components/ai-usage-meter-refresh.js +63 -0
  21. package/src/lib/components/ai-usage-meter-refresh.js.map +1 -0
  22. package/src/lib/components/ai-usage-strip.component.d.ts +19 -2
  23. package/src/lib/components/ai-usage-strip.component.js +40 -9
  24. package/src/lib/components/ai-usage-strip.component.js.map +1 -1
  25. package/src/lib/components/assist-jobs-drawer.component.js +12 -16
  26. package/src/lib/components/assist-jobs-drawer.component.js.map +1 -1
  27. package/src/lib/components/assist-panel.component.js +91 -51
  28. package/src/lib/components/assist-panel.component.js.map +1 -1
  29. package/src/lib/components/use-ai-usage-meter.d.ts +2 -0
  30. package/src/lib/components/use-ai-usage-meter.js +8 -3
  31. package/src/lib/components/use-ai-usage-meter.js.map +1 -1
  32. package/src/lib/jobs/ai-build-unit-outcome.d.ts +3 -1
  33. package/src/lib/jobs/ai-build-unit-outcome.js +19 -3
  34. package/src/lib/jobs/ai-build-unit-outcome.js.map +1 -1
  35. package/src/lib/jobs/ai-job-build-step.js +33 -3
  36. package/src/lib/jobs/ai-job-build-step.js.map +1 -1
  37. package/src/lib/jobs/ai-job-cancel.d.ts +71 -0
  38. package/src/lib/jobs/ai-job-cancel.js +102 -0
  39. package/src/lib/jobs/ai-job-cancel.js.map +1 -0
  40. package/src/lib/jobs/ai-job-component-step.js +4 -1
  41. package/src/lib/jobs/ai-job-component-step.js.map +1 -1
  42. package/src/lib/jobs/ai-job-draft-ids.js +7 -2
  43. package/src/lib/jobs/ai-job-draft-ids.js.map +1 -1
  44. package/src/lib/jobs/ai-job-drafts.js +2 -0
  45. package/src/lib/jobs/ai-job-drafts.js.map +1 -1
  46. package/src/lib/jobs/ai-job-form-dataset.d.ts +100 -0
  47. package/src/lib/jobs/ai-job-form-dataset.js +161 -0
  48. package/src/lib/jobs/ai-job-form-dataset.js.map +1 -0
  49. package/src/lib/jobs/ai-job-form-step.d.ts +15 -3
  50. package/src/lib/jobs/ai-job-form-step.js +41 -10
  51. package/src/lib/jobs/ai-job-form-step.js.map +1 -1
  52. package/src/lib/jobs/ai-job-layout-language.d.ts +2 -0
  53. package/src/lib/jobs/ai-job-layout-language.js +13 -1
  54. package/src/lib/jobs/ai-job-layout-language.js.map +1 -1
  55. package/src/lib/jobs/ai-job-layout-step.js +6 -1
  56. package/src/lib/jobs/ai-job-layout-step.js.map +1 -1
  57. package/src/lib/jobs/ai-job-page-language.d.ts +9 -0
  58. package/src/lib/jobs/ai-job-page-language.js +45 -2
  59. package/src/lib/jobs/ai-job-page-language.js.map +1 -1
  60. package/src/lib/jobs/ai-job-page-step.js +12 -1
  61. package/src/lib/jobs/ai-job-page-step.js.map +1 -1
  62. package/src/lib/jobs/ai-job-plan-step.d.ts +38 -1
  63. package/src/lib/jobs/ai-job-plan-step.js +90 -13
  64. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  65. package/src/lib/jobs/ai-job-site-content.d.ts +15 -0
  66. package/src/lib/jobs/ai-job-site-content.js +66 -7
  67. package/src/lib/jobs/ai-job-site-content.js.map +1 -1
  68. package/src/lib/jobs/ai-job-site-datasets.d.ts +170 -0
  69. package/src/lib/jobs/ai-job-site-datasets.js +499 -0
  70. package/src/lib/jobs/ai-job-site-datasets.js.map +1 -0
  71. package/src/lib/jobs/ai-job-site-step.d.ts +15 -8
  72. package/src/lib/jobs/ai-job-site-step.js +152 -30
  73. package/src/lib/jobs/ai-job-site-step.js.map +1 -1
  74. package/src/lib/jobs/ai-job-template-step.js +4 -1
  75. package/src/lib/jobs/ai-job-template-step.js.map +1 -1
  76. package/src/lib/jobs/ai-jobs.d.ts +37 -12
  77. package/src/lib/jobs/ai-jobs.js +184 -28
  78. package/src/lib/jobs/ai-jobs.js.map +1 -1
  79. package/src/lib/jobs/ai-layout-stock-photos.d.ts +143 -70
  80. package/src/lib/jobs/ai-layout-stock-photos.js +649 -176
  81. package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
  82. package/src/lib/jobs/fixtures/ai-ceramics-portfolio-site-plan.d.ts +30 -0
  83. package/src/lib/jobs/fixtures/ai-ceramics-portfolio-site-plan.js +220 -0
  84. package/src/lib/jobs/fixtures/ai-ceramics-portfolio-site-plan.js.map +1 -0
  85. package/src/lib/jobs/fixtures/ai-dataset-site-plan-recordings.d.ts +30 -0
  86. package/src/lib/jobs/fixtures/ai-dataset-site-plan-recordings.js +637 -0
  87. package/src/lib/jobs/fixtures/ai-dataset-site-plan-recordings.js.map +1 -0
  88. package/src/lib/jobs/fixtures/ai-stock-photo-pixabay-recordings.d.ts +27 -0
  89. package/src/lib/jobs/fixtures/ai-stock-photo-pixabay-recordings.js +151 -0
  90. package/src/lib/jobs/fixtures/ai-stock-photo-pixabay-recordings.js.map +1 -0
  91. package/src/lib/layout-language/ai-layout-compiler.d.ts +16 -0
  92. package/src/lib/layout-language/ai-layout-compiler.js +397 -9
  93. package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
  94. package/src/lib/layout-language/ai-layout-design.d.ts +6 -3
  95. package/src/lib/layout-language/ai-layout-design.js +5 -3
  96. package/src/lib/layout-language/ai-layout-design.js.map +1 -1
  97. package/src/lib/layout-language/ai-layout-frame.d.ts +12 -0
  98. package/src/lib/layout-language/ai-layout-frame.js +58 -3
  99. package/src/lib/layout-language/ai-layout-frame.js.map +1 -1
  100. package/src/lib/layout-language/ai-layout-gaps.js +17 -1
  101. package/src/lib/layout-language/ai-layout-gaps.js.map +1 -1
  102. package/src/lib/layout-language/ai-layout-listings.d.ts +44 -3
  103. package/src/lib/layout-language/ai-layout-listings.js +118 -50
  104. package/src/lib/layout-language/ai-layout-listings.js.map +1 -1
  105. package/src/lib/layout-language/ai-layout-pictures.d.ts +8 -0
  106. package/src/lib/layout-language/ai-layout-pictures.js.map +1 -1
  107. package/src/lib/layout-language/ai-layout-storefront.d.ts +82 -0
  108. package/src/lib/layout-language/ai-layout-storefront.js +223 -0
  109. package/src/lib/layout-language/ai-layout-storefront.js.map +1 -0
  110. package/src/lib/model/ai-build-job.d.ts +11 -1
  111. package/src/lib/model/ai-build-job.js +35 -6
  112. package/src/lib/model/ai-build-job.js.map +1 -1
  113. package/src/lib/model/ai-build-plan.d.ts +23 -3
  114. package/src/lib/model/ai-build-plan.js +47 -6
  115. package/src/lib/model/ai-build-plan.js.map +1 -1
  116. package/src/lib/model/ai-build-progress.d.ts +2 -2
  117. package/src/lib/model/ai-build-progress.js +6 -4
  118. package/src/lib/model/ai-build-progress.js.map +1 -1
  119. package/src/lib/model/ai-credit-estimate.d.ts +69 -2
  120. package/src/lib/model/ai-credit-estimate.js +91 -3
  121. package/src/lib/model/ai-credit-estimate.js.map +1 -1
  122. package/src/lib/model/ai-job-cancel-copy.d.ts +44 -0
  123. package/src/lib/model/ai-job-cancel-copy.js +53 -0
  124. package/src/lib/model/ai-job-cancel-copy.js.map +1 -0
  125. package/src/lib/model/ai-job-failure-copy.d.ts +4 -1
  126. package/src/lib/model/ai-job-failure-copy.js +8 -3
  127. package/src/lib/model/ai-job-failure-copy.js.map +1 -1
  128. package/src/lib/model/ai-jobs.types.d.ts +20 -0
  129. package/src/lib/model/ai-jobs.types.js.map +1 -1
  130. package/src/lib/model/ai-plan-capabilities.d.ts +7 -0
  131. package/src/lib/model/ai-plan-capabilities.js.map +1 -1
  132. package/src/lib/model/ai-site-build-progress.js +7 -4
  133. package/src/lib/model/ai-site-build-progress.js.map +1 -1
  134. package/src/lib/model/ai-site-job.d.ts +38 -4
  135. package/src/lib/model/ai-site-job.js +50 -14
  136. package/src/lib/model/ai-site-job.js.map +1 -1
  137. package/src/lib/model/ai-site-kinds.d.ts +4 -1
  138. package/src/lib/model/ai-site-kinds.js +34 -6
  139. package/src/lib/model/ai-site-kinds.js.map +1 -1
  140. package/src/lib/runtime/ai-dataset-generation.d.ts +77 -0
  141. package/src/lib/runtime/ai-dataset-generation.js +127 -0
  142. package/src/lib/runtime/ai-dataset-generation.js.map +1 -0
  143. package/src/lib/runtime/ai-doctrine-validators.d.ts +77 -1
  144. package/src/lib/runtime/ai-doctrine-validators.js +280 -6
  145. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  146. package/src/lib/runtime/ai-doctrine.js +10 -4
  147. package/src/lib/runtime/ai-doctrine.js.map +1 -1
  148. package/src/lib/runtime/ai-node-tree.d.ts +2 -0
  149. package/src/lib/runtime/ai-node-tree.js +16 -0
  150. package/src/lib/runtime/ai-node-tree.js.map +1 -1
  151. package/src/lib/runtime/ai-palette.generated.js +53 -7
  152. package/src/lib/runtime/ai-palette.generated.js.map +1 -1
  153. package/src/lib/server/ai-jobs-cancel.d.ts +13 -5
  154. package/src/lib/server/ai-jobs-cancel.js +27 -9
  155. package/src/lib/server/ai-jobs-cancel.js.map +1 -1
  156. package/src/lib/server/ai-jobs-gate.d.ts +12 -2
  157. package/src/lib/server/ai-jobs-gate.js +20 -9
  158. package/src/lib/server/ai-jobs-gate.js.map +1 -1
  159. package/src/lib/tools/ai-dataset-tool.d.ts +68 -0
  160. package/src/lib/tools/ai-dataset-tool.js +231 -0
  161. package/src/lib/tools/ai-dataset-tool.js.map +1 -0
  162. package/src/lib/usage/ai-usage-meter.d.ts +2 -1
  163. package/src/lib/usage/ai-usage-meter.js +17 -1
  164. package/src/lib/usage/ai-usage-meter.js.map +1 -1
  165. package/src/lib/usage/ai-usage-wire.d.ts +6 -0
  166. package/src/lib/usage/ai-usage-wire.js.map +1 -1
  167. package/src/lib/usage/assist-usage.d.ts +7 -0
  168. package/src/lib/usage/assist-usage.js +14 -4
  169. package/src/lib/usage/assist-usage.js.map +1 -1
  170. package/src/lib/usage/free-ai-credits-left.d.ts +7 -0
  171. package/src/lib/usage/free-ai-credits-left.js +30 -4
  172. package/src/lib/usage/free-ai-credits-left.js.map +1 -1
@@ -0,0 +1,127 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ import { _ as _extends } from "@swc/helpers/_/_extends";
17
+ import { AI_ROUTING_TABLE } from "../providers/routing.js";
18
+ import { AI_DATASET_LIMITS, AI_DATASET_LIST_SEPARATOR, AI_DATASET_TOOL, AI_DATASET_TOOL_NAME, checkAiDataset } from "../tools/ai-dataset-tool.js";
19
+ import { runValidatedGeneration } from "./ai-doctrine.js";
20
+ /**
21
+ * A SITE'S DATASETS (AGL-3616): the typed fields and first records of one
22
+ * dataset a site plan created, through the doctrine's generation call and the
23
+ * strict `submit_dataset` tool. The data plugin's writer stores what it
24
+ * answers; nothing here writes.
25
+ *
26
+ * Routed as `job.products`, the row a store's first products are proposed on
27
+ * (no thinking, a structured list from a brief, held to the storefront claim
28
+ * rules): one dataset of a dozen records is the same work. Its answer is held
29
+ * to a ceiling of its own under that row's. The rules are cached and
30
+ * byte-identical; the site's words, the dataset and the pages that show it
31
+ * ride in the user turn.
32
+ */ /** The step a dataset is routed as. */ export const AI_DATASET_STEP = 'job.products';
33
+ /**
34
+ * One dataset's answer ceiling: twelve fields and twelve records of a few
35
+ * sentences each, written as JSON, about 9,000 characters at three a token
36
+ * with room; under the routing row's.
37
+ */ export const AI_DATASET_MAX_TOKENS = Math.min(4000, AI_ROUTING_TABLE[AI_DATASET_STEP].maxTokens);
38
+ /** The generation kind a dataset is asked under, which the doctrine scopes. */ export const AI_DATASET_KIND = 'dataset';
39
+ /** The rules for one dataset. Byte-identical on every request. */ export const AI_DATASET_INSTRUCTIONS = [
40
+ {
41
+ text: 'You design one dataset of a new website: a list of like things — a menu, a team, services, portfolio pieces, events, questions and answers — kept as records the owner edits in one place, which the site’s pages list.\n\n' + `Answer by calling ${AI_DATASET_TOOL_NAME} exactly once. A reply in prose cannot be used.\n\n` + 'Rules:\n' + [
42
+ 'Keep the planned fields, in order and named exactly as planned; the pages around the dataset already name them. Add a field only where the records clearly need it.',
43
+ 'Type each field: text for words, including a date or a time written the way a visitor reads it; number or integer only for a figure a visitor compares; boolean for yes or no; list for a few short tags. The first field names each record and is text.',
44
+ `Write ${AI_DATASET_LIMITS.recordsMin} to ${AI_DATASET_LIMITS.recordsMax} records, as many as the brief supports: what the business actually offers, in its own words where the brief gives them, otherwise plain and general enough to be true of it.`,
45
+ 'Never invent a fact: no price, date, address, phone number, award, credential, client or number the brief does not give, and no review, testimonial, rating or quote from anyone. Leave a value "" rather than make one up.',
46
+ 'Never give a person a name the brief does not give: a team member the brief does not name is their role, such as "Head chef".',
47
+ 'Never say anything cures, treats, heals or prevents anything, and never promise a health, financial or legal result.',
48
+ `Values are plain text with no links, markup, emoji or square brackets, at most ${AI_DATASET_LIMITS.valueMax} characters each; a description is one to three sentences.`,
49
+ 'Write in the language of the brief.'
50
+ ].map((rule)=>`- ${rule}`).join('\n'),
51
+ cacheBreakpoint: true
52
+ }
53
+ ];
54
+ /** The user turn: the site's words, the dataset, and where it is shown. */ export function aiDatasetPrompt(input) {
55
+ return [
56
+ 'Brief:',
57
+ input.brief.trim(),
58
+ `Dataset: “${input.name}” — ${input.why.trim() || 'the site’s structured content'}`,
59
+ `Planned fields: ${input.fields.length ? input.fields.map((field)=>`“${field}”`).join(', ') : '(none: choose them)'}`,
60
+ input.shownIn.length ? `Listed on: ${input.shownIn.join('; ')}.` : 'Listed on the site’s pages.',
61
+ input.recordPages ? 'Each record also gets a page of its own, so give each one enough to read: a sentence or two describing it.' : 'Each record is shown as a card, so keep its values short.'
62
+ ].join('\n');
63
+ }
64
+ /** Design one dataset. It writes nothing to the site. */ export function generateAiDataset(input) {
65
+ var _input_maxTokens;
66
+ const row = AI_ROUTING_TABLE[AI_DATASET_STEP];
67
+ return runValidatedGeneration(AI_DATASET_KIND, _extends({
68
+ step: AI_DATASET_STEP
69
+ }, input.settings ? {
70
+ settings: input.settings
71
+ } : {}, input.model ? {
72
+ model: input.model
73
+ } : {}, {
74
+ instructions: AI_DATASET_INSTRUCTIONS,
75
+ messages: [
76
+ {
77
+ role: 'user',
78
+ content: aiDatasetPrompt(input)
79
+ }
80
+ ],
81
+ tool: AI_DATASET_TOOL,
82
+ maxTokens: (_input_maxTokens = input.maxTokens) != null ? _input_maxTokens : AI_DATASET_MAX_TOKENS
83
+ }, row.thinking ? {
84
+ thinking: row.thinking
85
+ } : {}, row.effort ? {
86
+ effort: row.effort
87
+ } : {}, input.signal ? {
88
+ signal: input.signal
89
+ } : {}, input.provider ? {
90
+ provider: input.provider
91
+ } : {}, {
92
+ check: (answer)=>checkAiDataset(answer, {
93
+ planned: input.fields,
94
+ merchantWords: input.merchantWords
95
+ })
96
+ }));
97
+ }
98
+ /** A designed dataset as the data plugin's `dataset` writer takes it: values by field name. */ export function aiDatasetDraftContent(name, dataset, options) {
99
+ const records = dataset.records.map((values)=>{
100
+ const record = {};
101
+ dataset.fields.forEach((field, column)=>{
102
+ var _values_column;
103
+ const value = ((_values_column = values[column]) != null ? _values_column : '').trim();
104
+ if (!value) return;
105
+ if (field.type === 'list') {
106
+ record[field.name] = value.split(AI_DATASET_LIST_SEPARATOR.trim()).map((part)=>part.trim()).filter(Boolean);
107
+ } else if (field.type === 'number' || field.type === 'integer') {
108
+ record[field.name] = value.replace(/,/g, '');
109
+ } else {
110
+ record[field.name] = value;
111
+ }
112
+ });
113
+ return record;
114
+ });
115
+ return _extends({
116
+ name,
117
+ fields: dataset.fields.map((field)=>({
118
+ name: field.name,
119
+ type: field.type
120
+ })),
121
+ records
122
+ }, options.recordPages && dataset.fields[0] ? {
123
+ pageAddressFrom: dataset.fields[0].name
124
+ } : {});
125
+ }
126
+
127
+ //# sourceMappingURL=ai-dataset-generation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/runtime/ai-dataset-generation.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { AiStepKind } from '../providers/catalog'\nimport type { AiProvider } from '../providers/contract'\nimport { AI_ROUTING_TABLE, type AiPluginSettings } from '../providers/routing'\nimport {\n AI_DATASET_LIMITS,\n AI_DATASET_LIST_SEPARATOR,\n AI_DATASET_TOOL,\n AI_DATASET_TOOL_NAME,\n checkAiDataset,\n type AiDataset,\n} from '../tools/ai-dataset-tool'\nimport { runValidatedGeneration, type AiValidatedGeneration } from './ai-doctrine'\nimport type { AiSystemBlock } from './ai-runtime'\n\n/**\n * A SITE'S DATASETS (AGL-3616): the typed fields and first records of one\n * dataset a site plan created, through the doctrine's generation call and the\n * strict `submit_dataset` tool. The data plugin's writer stores what it\n * answers; nothing here writes.\n *\n * Routed as `job.products`, the row a store's first products are proposed on\n * (no thinking, a structured list from a brief, held to the storefront claim\n * rules): one dataset of a dozen records is the same work. Its answer is held\n * to a ceiling of its own under that row's. The rules are cached and\n * byte-identical; the site's words, the dataset and the pages that show it\n * ride in the user turn.\n */\n\n/** The step a dataset is routed as. */\nexport const AI_DATASET_STEP: AiStepKind = 'job.products'\n\n/**\n * One dataset's answer ceiling: twelve fields and twelve records of a few\n * sentences each, written as JSON, about 9,000 characters at three a token\n * with room; under the routing row's.\n */\nexport const AI_DATASET_MAX_TOKENS = Math.min(4_000, AI_ROUTING_TABLE[AI_DATASET_STEP].maxTokens)\n\n/** The generation kind a dataset is asked under, which the doctrine scopes. */\nexport const AI_DATASET_KIND = 'dataset'\n\n/** The rules for one dataset. Byte-identical on every request. */\nexport const AI_DATASET_INSTRUCTIONS: AiSystemBlock[] = [\n {\n text:\n 'You design one dataset of a new website: a list of like things — a menu, a team, services, portfolio pieces, events, questions and answers — kept as records the owner edits in one place, which the site’s pages list.\\n\\n' +\n `Answer by calling ${AI_DATASET_TOOL_NAME} exactly once. A reply in prose cannot be used.\\n\\n` +\n 'Rules:\\n' +\n [\n 'Keep the planned fields, in order and named exactly as planned; the pages around the dataset already name them. Add a field only where the records clearly need it.',\n 'Type each field: text for words, including a date or a time written the way a visitor reads it; number or integer only for a figure a visitor compares; boolean for yes or no; list for a few short tags. The first field names each record and is text.',\n `Write ${AI_DATASET_LIMITS.recordsMin} to ${AI_DATASET_LIMITS.recordsMax} records, as many as the brief supports: what the business actually offers, in its own words where the brief gives them, otherwise plain and general enough to be true of it.`,\n 'Never invent a fact: no price, date, address, phone number, award, credential, client or number the brief does not give, and no review, testimonial, rating or quote from anyone. Leave a value \"\" rather than make one up.',\n 'Never give a person a name the brief does not give: a team member the brief does not name is their role, such as \"Head chef\".',\n 'Never say anything cures, treats, heals or prevents anything, and never promise a health, financial or legal result.',\n `Values are plain text with no links, markup, emoji or square brackets, at most ${AI_DATASET_LIMITS.valueMax} characters each; a description is one to three sentences.`,\n 'Write in the language of the brief.',\n ]\n .map((rule) => `- ${rule}`)\n .join('\\n'),\n cacheBreakpoint: true,\n },\n]\n\nexport interface AiDatasetPromptInput {\n /** The site's words: the brief and its answers, as a unit's brief carries them. */\n brief: string\n /** The dataset as the plan named it, and why it has one. */\n name: string\n why: string\n /** The field names the plan gave it. */\n fields: readonly string[]\n /** The sections that list it, by page: \"Menu › Our dishes (12 items)\". */\n shownIn: readonly string[]\n /** Whether each record also gets a page of its own. */\n recordPages: boolean\n}\n\n/** The user turn: the site's words, the dataset, and where it is shown. */\nexport function aiDatasetPrompt(input: AiDatasetPromptInput): string {\n return [\n 'Brief:',\n input.brief.trim(),\n `Dataset: “${input.name}” — ${input.why.trim() || 'the site’s structured content'}`,\n `Planned fields: ${input.fields.length ? input.fields.map((field) => `“${field}”`).join(', ') : '(none: choose them)'}`,\n input.shownIn.length ? `Listed on: ${input.shownIn.join('; ')}.` : 'Listed on the site’s pages.',\n input.recordPages\n ? 'Each record also gets a page of its own, so give each one enough to read: a sentence or two describing it.'\n : 'Each record is shown as a card, so keep its values short.',\n ].join('\\n')\n}\n\nexport interface GenerateAiDatasetInput extends AiDatasetPromptInput {\n /** What a claim or a price may be quoted from: the brief and the site's answers. */\n merchantWords: string\n settings?: AiPluginSettings\n model?: string\n maxTokens?: number\n signal?: AbortSignal\n provider?: AiProvider\n}\n\n/** Design one dataset. It writes nothing to the site. */\nexport function generateAiDataset(input: GenerateAiDatasetInput): Promise<AiValidatedGeneration<AiDataset>> {\n const row = AI_ROUTING_TABLE[AI_DATASET_STEP]\n return runValidatedGeneration(AI_DATASET_KIND, {\n step: AI_DATASET_STEP,\n ...(input.settings ? { settings: input.settings } : {}),\n ...(input.model ? { model: input.model } : {}),\n instructions: AI_DATASET_INSTRUCTIONS,\n messages: [{ role: 'user', content: aiDatasetPrompt(input) }],\n tool: AI_DATASET_TOOL,\n maxTokens: input.maxTokens ?? AI_DATASET_MAX_TOKENS,\n ...(row.thinking ? { thinking: row.thinking } : {}),\n ...(row.effort ? { effort: row.effort } : {}),\n ...(input.signal ? { signal: input.signal } : {}),\n ...(input.provider ? { provider: input.provider } : {}),\n check: (answer) => checkAiDataset(answer, { planned: input.fields, merchantWords: input.merchantWords }),\n }) as Promise<AiValidatedGeneration<AiDataset>>\n}\n\n/** A designed dataset as the data plugin's `dataset` writer takes it: values by field name. */\nexport function aiDatasetDraftContent(\n name: string,\n dataset: AiDataset,\n options: { recordPages: boolean },\n): Record<string, unknown> {\n const records = dataset.records.map((values) => {\n const record: Record<string, unknown> = {}\n dataset.fields.forEach((field, column) => {\n const value = (values[column] ?? '').trim()\n if (!value) return\n if (field.type === 'list') {\n record[field.name] = value.split(AI_DATASET_LIST_SEPARATOR.trim()).map((part) => part.trim()).filter(Boolean)\n } else if (field.type === 'number' || field.type === 'integer') {\n record[field.name] = value.replace(/,/g, '')\n } else {\n record[field.name] = value\n }\n })\n return record\n })\n return {\n name,\n fields: dataset.fields.map((field) => ({ name: field.name, type: field.type })),\n records,\n // A record page shows each record at an address made from its name.\n ...(options.recordPages && dataset.fields[0] ? { pageAddressFrom: dataset.fields[0].name } : {}),\n }\n}\n"],"names":["AI_ROUTING_TABLE","AI_DATASET_LIMITS","AI_DATASET_LIST_SEPARATOR","AI_DATASET_TOOL","AI_DATASET_TOOL_NAME","checkAiDataset","runValidatedGeneration","AI_DATASET_STEP","AI_DATASET_MAX_TOKENS","Math","min","maxTokens","AI_DATASET_KIND","AI_DATASET_INSTRUCTIONS","text","recordsMin","recordsMax","valueMax","map","rule","join","cacheBreakpoint","aiDatasetPrompt","input","brief","trim","name","why","fields","length","field","shownIn","recordPages","generateAiDataset","row","step","settings","model","instructions","messages","role","content","tool","thinking","effort","signal","provider","check","answer","planned","merchantWords","aiDatasetDraftContent","dataset","options","records","values","record","forEach","column","value","type","split","part","filter","Boolean","replace","pageAddressFrom"],"mappings":"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"}
@@ -747,8 +747,53 @@ export declare function detectPlanSplitLists(plan: AiBuildPlan, inventory: AiSit
747
747
  * creation before the rules read the plan, when exactly one creation matches
748
748
  * it with case, spaces and punctuation ignored. Two that match, or none, are
749
749
  * left for the rules to ask about.
750
+ *
751
+ * So is a creation named without its prefix — `Contact form` in a section's
752
+ * uses for the "Contact form" the plan creates — where the name is no record
753
+ * the site has (AGL-3660). A re-asked plan told to place its form "by id"
754
+ * can answer with the form's bare name, which is no reference at all.
755
+ */
756
+ export declare function aiSettlePlanRefs(plan: AiBuildPlan, inventory?: AiSiteInventory | null): AiBuildPlan;
757
+ /**
758
+ * What customers say, which no dataset of the site's holds: a review, a
759
+ * testimonial, a rating, a quote (AGL-3616). A job never writes them for the
760
+ * people who did not, so a dataset of them is one nothing can seed.
761
+ */
762
+ export declare const AI_DATASET_SAID_NAME: RegExp;
763
+ /**
764
+ * A plan that creates more datasets than this job may make is settled in
765
+ * code, never re-asked (AGL-3616). The live eval of 2026-10-10 refused a
766
+ * dental practice's plan four times over for a fourth dataset, and a food
767
+ * bank's re-ask for one broke a layout rule its first answer had kept.
768
+ *
769
+ * A dataset of what customers say (`AI_DATASET_SAID_NAME`, in its name or a
770
+ * field) is let go first, whatever the cap: both of those plans made one of
771
+ * testimonials, which nothing may seed.
772
+ *
773
+ * The datasets kept are the most useful: the ones the most sections list,
774
+ * a record template counting double, plan order breaking ties. Each one let
775
+ * go leaves the plan whole — its `new:` reference comes off every section,
776
+ * a record template of it becomes a page of its own, and a section that
777
+ * listed it shows fewer items than rule 8 binds, so its items are written
778
+ * out on the page. Where the job may create no dataset at all, rule 7 still
779
+ * asks, as it does for any creation.
780
+ */
781
+ export declare function aiSettlePlanDatasets(plan: AiBuildPlan, capabilities?: AiPlanCapabilities | null): AiBuildPlan;
782
+ /**
783
+ * A form's dataset, settled in code before the rules read the plan
784
+ * (AGL-3616, Zach 2026-10-10: Aglyn AI's forms write to its datasets).
785
+ *
786
+ * - A `writesTo` that names nothing the form can write to comes off: a
787
+ * `new:` of no dataset this plan creates, an id the inventory does not
788
+ * hold, or a dataset this job may not create. The form is then the
789
+ * form it always was, its submissions in the Inbox. A dataset the
790
+ * inventory holds, named by its name rather than its id, is given its id.
791
+ * - A dataset this plan creates for a form holds the people who send it:
792
+ * no page lists it and none is its record template, so its `new:` comes
793
+ * off every section and record that named it, and a section that listed
794
+ * it shows fewer items than rule 8 binds.
750
795
  */
751
- export declare function aiSettlePlanRefs(plan: AiBuildPlan): AiBuildPlan;
796
+ export declare function aiSettlePlanFormDatasets(plan: AiBuildPlan, inventory: AiSiteInventory | null, capabilities?: AiPlanCapabilities | null): AiBuildPlan;
752
797
  /**
753
798
  * A page that names no layout, on a site where only one layout can frame it,
754
799
  * is put in that layout before the rules read the plan (2026-10-07). The
@@ -760,6 +805,37 @@ export declare function aiSettlePlanRefs(plan: AiBuildPlan): AiBuildPlan;
760
805
  * so the rule still asks.
761
806
  */
762
807
  export declare function aiSettlePlanLayouts(plan: AiBuildPlan, inventory: AiSiteInventory | null, capabilities?: AiPlanCapabilities | null): AiBuildPlan;
808
+ /** The contact form a plan is given when a section collects answers and the plan makes no form. */
809
+ export declare const AI_PLAN_SETTLED_FORM: {
810
+ readonly name: "Contact form";
811
+ readonly why: "Visitors need a way to reach the owner, and the site has no saved form for it.";
812
+ readonly fields: readonly ["name", "email", "message"];
813
+ };
814
+ /**
815
+ * A section that collects answers and places no saved form is given one
816
+ * before the rules read the plan (AGL-3660), rather than failing the build.
817
+ * A production guided start for a ceramic artist's portfolio (2026-10-09,
818
+ * five pages, 94 credits refunded) planned a section that asks for
819
+ * commissions on its work page without placing the contact form the plan
820
+ * made for its Contact page; the answer and its re-ask both did, and rule 3
821
+ * failed the whole site for one section.
822
+ *
823
+ * The section places, in order of preference:
824
+ * 1. the form whose name best matches it, among those the plan creates and
825
+ * the site has, preferring one the plan already places — so a guided
826
+ * start, which makes one form, places that same form again rather than a
827
+ * second one;
828
+ * 2. where there is no form at all and this job may create one, a form the
829
+ * plan now creates: named for the `new:` reference the section already
830
+ * holds when it names a form it never declared, the contact form
831
+ * otherwise.
832
+ *
833
+ * Nothing is taken out of the plan: the section keeps its name, its other
834
+ * uses and its items. Where the site has no form and the job may create none,
835
+ * rule 3 asks for nothing (AGL-3596), and neither does this. A section whose
836
+ * uses are already full is left for the rule to ask about.
837
+ */
838
+ export declare function aiSettlePlanForms(plan: AiBuildPlan, inventory: AiSiteInventory | null, capabilities?: AiPlanCapabilities | null): AiBuildPlan;
763
839
  /**
764
840
  * Rule 2 (plan): every screen declares a layout, no section is a copy of a
765
841
  * layout region, and no section places a layout, which frames a whole screen
@@ -23,7 +23,7 @@ import { FREE_AI_TASTE_CREDITS_PER_MONTH } from "../plan-entitlements.js";
23
23
  import { REUSABLE_INSTANCE_COMPONENT_ID, REUSABLE_INSTANCE_PROP_VALUES_KEY } from "@aglyn/aglyn/app-utils/reusable-component-keys";
24
24
  import { parseBreakpointSpan } from "@aglyn/shared-data-enums/breakpoint-span";
25
25
  import { renderEmailHtml } from "@aglyn/shared-util-email/email-render";
26
- import { AI_BUILD_PLAN_CREATION_NOUNS, AI_BUILD_PLAN_EMBED_HOST_NAMES, AI_PLAN_ASKS_FOR_VIDEO, aiEmbedHostOf, aiEmbedVideoKey, aiPlanCreateFor, aiPlanSlugKey, aiPlanUndeclaredRefs, isAiPlanNewRef } from "../model/ai-build-plan.js";
26
+ import { AI_BUILD_PLAN_CREATION_NOUNS, AI_BUILD_PLAN_EMBED_HOST_NAMES, AI_BUILD_PLAN_LIMITS, AI_PLAN_ASKS_FOR_VIDEO, aiEmbedHostOf, aiEmbedVideoKey, AI_PLAN_DATASET_ITEM_OP, aiPlanCreateFor, aiPlanFormDatasetNames, aiPlanItemFor, aiPlanSlugKey, aiPlanUndeclaredRefs, isAiPlanNewRef } from "../model/ai-build-plan.js";
27
27
  import { aiCreationNoun, aiPlanCanPlaceForm, aiPlanUncreatable, aiPlanUncreatableKind } from "../model/ai-plan-capabilities.js";
28
28
  import { aiFreeSiteSectionsWithin, aiSiteHomeMinSections, aiSitePlanIsHome } from "../model/ai-site-job.js";
29
29
  import { AI_INSTANCE_REF_PROP, validateAiNodeTree } from "./ai-node-tree.js";
@@ -279,12 +279,15 @@ function listNames(ids, tree) {
279
279
  const index = indexShapes(tree);
280
280
  /**
281
281
  * A compiled picture card (AGL-3660): a Stack that opens with its picture,
282
- * in a cell of a grid — or that cell, holding only the card.
282
+ * in a cell of a grid — or that cell, holding only the card. A storefront's
283
+ * icon column (AGL-3676), a Stack that opens with its library icon, is the
284
+ * compiler's the same way: no model writes an Icon on a page.
283
285
  */ const card = (id)=>{
284
286
  var _node_nodes, _tree_nodes_first;
285
287
  const node = id === undefined ? undefined : tree.nodes[id];
286
288
  const first = node == null ? void 0 : (_node_nodes = node.nodes) == null ? void 0 : _node_nodes[0];
287
- return (node == null ? void 0 : node.componentId) === 'muiStack' && first !== undefined && ((_tree_nodes_first = tree.nodes[first]) == null ? void 0 : _tree_nodes_first.componentId) === 'image';
289
+ const opens = first === undefined ? undefined : (_tree_nodes_first = tree.nodes[first]) == null ? void 0 : _tree_nodes_first.componentId;
290
+ return (node == null ? void 0 : node.componentId) === 'muiStack' && (opens === 'image' || opens === 'icon');
288
291
  };
289
292
  const pictureCard = (id)=>{
290
293
  var _node_props, _node_nodes, _index_visits_find, _tree_nodes_parentId;
@@ -3286,11 +3289,18 @@ const FORM_SECTION_NAME = /\bform\b|\bsign[- ]?up\b|\bsubscribe\b|\bnewsletter\b
3286
3289
  * creation before the rules read the plan, when exactly one creation matches
3287
3290
  * it with case, spaces and punctuation ignored. Two that match, or none, are
3288
3291
  * left for the rules to ask about.
3289
- */ export function aiSettlePlanRefs(plan) {
3292
+ *
3293
+ * So is a creation named without its prefix — `Contact form` in a section's
3294
+ * uses for the "Contact form" the plan creates — where the name is no record
3295
+ * the site has (AGL-3660). A re-asked plan told to place its form "by id"
3296
+ * can answer with the form's bare name, which is no reference at all.
3297
+ */ export function aiSettlePlanRefs(plan, inventory = null) {
3290
3298
  if (!plan.create.length) return plan;
3299
+ const kinds = inventoryKinds(inventory);
3291
3300
  const resolve = (ref)=>{
3292
- if (!isAiPlanNewRef(ref) || aiPlanCreateFor(plan, ref)) return ref;
3293
- const key = creationKey(ref.slice('new:'.length));
3301
+ if (!ref || kinds.has(ref) || aiPlanCreateFor(plan, ref)) return ref;
3302
+ const key = creationKey(isAiPlanNewRef(ref) ? ref.slice('new:'.length) : ref);
3303
+ if (!key) return ref;
3294
3304
  const matches = plan.create.filter((entry)=>creationKey(entry.name) === key);
3295
3305
  return matches.length === 1 ? `new:${matches[0].name}` : ref;
3296
3306
  };
@@ -3314,6 +3324,135 @@ const FORM_SECTION_NAME = /\bform\b|\bsign[- ]?up\b|\bsubscribe\b|\bnewsletter\b
3314
3324
  screens
3315
3325
  }) : plan;
3316
3326
  }
3327
+ /**
3328
+ * What customers say, which no dataset of the site's holds: a review, a
3329
+ * testimonial, a rating, a quote (AGL-3616). A job never writes them for the
3330
+ * people who did not, so a dataset of them is one nothing can seed.
3331
+ */ export const AI_DATASET_SAID_NAME = /\b(reviews?|testimonials?|ratings?|quotes?|stars?|endorsements?)\b/i;
3332
+ /**
3333
+ * A plan that creates more datasets than this job may make is settled in
3334
+ * code, never re-asked (AGL-3616). The live eval of 2026-10-10 refused a
3335
+ * dental practice's plan four times over for a fourth dataset, and a food
3336
+ * bank's re-ask for one broke a layout rule its first answer had kept.
3337
+ *
3338
+ * A dataset of what customers say (`AI_DATASET_SAID_NAME`, in its name or a
3339
+ * field) is let go first, whatever the cap: both of those plans made one of
3340
+ * testimonials, which nothing may seed.
3341
+ *
3342
+ * The datasets kept are the most useful: the ones the most sections list,
3343
+ * a record template counting double, plan order breaking ties. Each one let
3344
+ * go leaves the plan whole — its `new:` reference comes off every section,
3345
+ * a record template of it becomes a page of its own, and a section that
3346
+ * listed it shows fewer items than rule 8 binds, so its items are written
3347
+ * out on the page. Where the job may create no dataset at all, rule 7 still
3348
+ * asks, as it does for any creation.
3349
+ */ export function aiSettlePlanDatasets(plan, capabilities = null) {
3350
+ var _ref, _entry_left;
3351
+ const entry = capabilities == null ? void 0 : capabilities.create.dataset;
3352
+ if (!(entry == null ? void 0 : entry.allowed)) return plan;
3353
+ const cap = Math.min((_ref = capabilities == null ? void 0 : capabilities.datasetsMax) != null ? _ref : Infinity, (_entry_left = entry.left) != null ? _entry_left : Infinity);
3354
+ const datasets = plan.create.filter((one)=>one.kind === 'dataset');
3355
+ // A dataset a form writes to holds what visitors send (AGL-3616): its
3356
+ // fields may well be a rating or a quote request, and nothing seeds it.
3357
+ const forForms = aiPlanFormDatasetNames(plan);
3358
+ const said = (one)=>!forForms.has(one.name.trim().toLowerCase()) && (AI_DATASET_SAID_NAME.test(one.name) || one.fields.some((field)=>AI_DATASET_SAID_NAME.test(field)));
3359
+ if (!datasets.some(said) && (!Number.isFinite(cap) || datasets.length <= cap)) return plan;
3360
+ const named = (ref, name)=>!!ref && isAiPlanNewRef(ref) && ref.slice('new:'.length).trim().toLowerCase() === name.trim().toLowerCase();
3361
+ // A form's dataset counts as much as a record template: the form the
3362
+ // person asked for is built around it.
3363
+ const score = (name)=>(forForms.has(name.trim().toLowerCase()) ? 2 : 0) + plan.screens.reduce((total, screen)=>{
3364
+ var _screen_record;
3365
+ return total + (named((_screen_record = screen.record) == null ? void 0 : _screen_record.dataset, name) ? 2 : 0) + screen.sections.filter((section)=>section.uses.some((ref)=>named(ref, name))).length;
3366
+ }, 0);
3367
+ const ranked = datasets.filter((one)=>!said(one)).map((one, index)=>({
3368
+ name: one.name,
3369
+ index,
3370
+ score: score(one.name)
3371
+ })).sort((a, b)=>b.score - a.score || a.index - b.index);
3372
+ const kept = new Set(ranked.slice(0, Math.max(0, cap)).map((one)=>one.name.trim().toLowerCase()));
3373
+ const dropped = new Set(datasets.map((one)=>one.name.trim().toLowerCase()).filter((name)=>!kept.has(name)));
3374
+ const isDropped = (ref)=>[
3375
+ ...dropped
3376
+ ].some((name)=>named(ref, name));
3377
+ return _extends({}, plan, {
3378
+ create: plan.create.filter((one)=>one.kind !== 'dataset' || !dropped.has(one.name.trim().toLowerCase()))// A form whose dataset was let go sends its submissions to the Inbox only.
3379
+ .map((one)=>one.kind === 'form' && isDropped(one.writesTo) ? withoutWritesTo(one) : one),
3380
+ screens: plan.screens.map((screen)=>_extends({}, screen, {
3381
+ record: screen.record && isDropped(screen.record.dataset) ? null : screen.record,
3382
+ sections: screen.sections.map((section)=>section.uses.some(isDropped) ? _extends({}, section, {
3383
+ uses: section.uses.filter((ref)=>!isDropped(ref)),
3384
+ items: Math.min(section.items, AI_TYPED_LIST_MIN_ITEMS - 1)
3385
+ }) : section)
3386
+ }))
3387
+ });
3388
+ }
3389
+ /** A creation with no `writesTo`, as a plan that never said one reads. */ function withoutWritesTo(creation) {
3390
+ const rest = _extends({}, creation);
3391
+ delete rest.writesTo;
3392
+ return rest;
3393
+ }
3394
+ /**
3395
+ * A form's dataset, settled in code before the rules read the plan
3396
+ * (AGL-3616, Zach 2026-10-10: Aglyn AI's forms write to its datasets).
3397
+ *
3398
+ * - A `writesTo` that names nothing the form can write to comes off: a
3399
+ * `new:` of no dataset this plan creates, an id the inventory does not
3400
+ * hold, or a dataset this job may not create. The form is then the
3401
+ * form it always was, its submissions in the Inbox. A dataset the
3402
+ * inventory holds, named by its name rather than its id, is given its id.
3403
+ * - A dataset this plan creates for a form holds the people who send it:
3404
+ * no page lists it and none is its record template, so its `new:` comes
3405
+ * off every section and record that named it, and a section that listed
3406
+ * it shows fewer items than rule 8 binds.
3407
+ */ export function aiSettlePlanFormDatasets(plan, inventory, capabilities = null) {
3408
+ var _ref;
3409
+ if (!plan.create.some((one)=>one.kind === 'form' && one.writesTo)) return plan;
3410
+ const held = (_ref = inventory == null ? void 0 : inventory.datasets) != null ? _ref : [];
3411
+ const creatable = !capabilities || capabilities.create.dataset.allowed;
3412
+ let changed = false;
3413
+ const create = plan.create.map((one)=>{
3414
+ if (one.kind !== 'form' || !one.writesTo) return one;
3415
+ const ref = one.writesTo.trim();
3416
+ if (isAiPlanNewRef(ref)) {
3417
+ var _aiPlanCreateFor, _aiPlanItemFor;
3418
+ if (creatable && ((_aiPlanCreateFor = aiPlanCreateFor(plan, ref)) == null ? void 0 : _aiPlanCreateFor.kind) === 'dataset') return one;
3419
+ // A build's dataset item, which the data plugin's operation makes.
3420
+ if (((_aiPlanItemFor = aiPlanItemFor(plan, ref)) == null ? void 0 : _aiPlanItemFor.op) === AI_PLAN_DATASET_ITEM_OP) return one;
3421
+ } else {
3422
+ if (held.some((row)=>row.id === ref)) return one;
3423
+ const byName = held.find((row)=>aiNamesMatch(row.name, ref));
3424
+ if (byName) {
3425
+ changed = true;
3426
+ return _extends({}, one, {
3427
+ writesTo: byName.id
3428
+ });
3429
+ }
3430
+ }
3431
+ changed = true;
3432
+ return withoutWritesTo(one);
3433
+ });
3434
+ const settled = _extends({}, plan, {
3435
+ create
3436
+ });
3437
+ const forForms = aiPlanFormDatasetNames(settled);
3438
+ const listsOne = (ref)=>isAiPlanNewRef(ref) && forForms.has(ref.slice('new:'.length).trim().toLowerCase());
3439
+ const screens = plan.screens.map((screen)=>{
3440
+ const record = screen.record && listsOne(screen.record.dataset) ? null : screen.record;
3441
+ const sections = screen.sections.map((section)=>section.uses.some(listsOne) ? _extends({}, section, {
3442
+ uses: section.uses.filter((ref)=>!listsOne(ref)),
3443
+ items: Math.min(section.items, AI_TYPED_LIST_MIN_ITEMS - 1)
3444
+ }) : section);
3445
+ if (record === screen.record && sections.every((section, index)=>section === screen.sections[index])) return screen;
3446
+ changed = true;
3447
+ return _extends({}, screen, {
3448
+ record,
3449
+ sections
3450
+ });
3451
+ });
3452
+ return changed ? _extends({}, settled, {
3453
+ screens
3454
+ }) : plan;
3455
+ }
3317
3456
  /**
3318
3457
  * A page that names no layout, on a site where only one layout can frame it,
3319
3458
  * is put in that layout before the rules read the plan (2026-10-07). The
@@ -3366,6 +3505,140 @@ const FORM_SECTION_NAME = /\bform\b|\bsign[- ]?up\b|\bsubscribe\b|\bnewsletter\b
3366
3505
  }))
3367
3506
  });
3368
3507
  }
3508
+ /** Every saved form a plan could place, the plan's own creations first. */ function planFormChoices(plan, inventory) {
3509
+ var _ref;
3510
+ return [
3511
+ ...plan.create.filter((entry)=>entry.kind === 'form').map((entry)=>({
3512
+ ref: `new:${entry.name}`,
3513
+ name: entry.name,
3514
+ created: true
3515
+ })),
3516
+ ...((_ref = inventory == null ? void 0 : inventory.forms) != null ? _ref : []).map((form)=>({
3517
+ ref: form.id,
3518
+ name: form.name,
3519
+ created: false
3520
+ }))
3521
+ ];
3522
+ }
3523
+ /** The form a plan's own words would name for a section's reference, so placements count alike. */ function formChoiceKey(ref, plan) {
3524
+ const created = isAiPlanNewRef(ref) ? aiPlanCreateFor(plan, ref) : undefined;
3525
+ return created ? `new:${created.name}` : ref;
3526
+ }
3527
+ /**
3528
+ * The form a section that collects answers places: the one whose name shares
3529
+ * the most words with the section's ("Commission inquiry" → "Commission
3530
+ * inquiry form"), then the one the plan already places most, then the plan's
3531
+ * own creation over a form the site has. `null` when there is none.
3532
+ */ function bestPlanForm(section, choices, placed) {
3533
+ var _ranked_;
3534
+ const words = new Set(nameTokens(section.name).filter((word)=>word !== 'form'));
3535
+ const score = (choice)=>nameTokens(choice.name).filter((word)=>word !== 'form' && words.has(word)).length;
3536
+ const ranked = [
3537
+ ...choices
3538
+ ].sort((a, b)=>{
3539
+ var _placed_get, _placed_get1;
3540
+ return score(b) - score(a) || ((_placed_get = placed.get(b.ref)) != null ? _placed_get : 0) - ((_placed_get1 = placed.get(a.ref)) != null ? _placed_get1 : 0) || Number(b.created) - Number(a.created);
3541
+ });
3542
+ return (_ranked_ = ranked[0]) != null ? _ranked_ : null;
3543
+ }
3544
+ /** The contact form a plan is given when a section collects answers and the plan makes no form. */ export const AI_PLAN_SETTLED_FORM = {
3545
+ name: 'Contact form',
3546
+ why: 'Visitors need a way to reach the owner, and the site has no saved form for it.',
3547
+ fields: [
3548
+ 'name',
3549
+ 'email',
3550
+ 'message'
3551
+ ]
3552
+ };
3553
+ /**
3554
+ * A section that collects answers and places no saved form is given one
3555
+ * before the rules read the plan (AGL-3660), rather than failing the build.
3556
+ * A production guided start for a ceramic artist's portfolio (2026-10-09,
3557
+ * five pages, 94 credits refunded) planned a section that asks for
3558
+ * commissions on its work page without placing the contact form the plan
3559
+ * made for its Contact page; the answer and its re-ask both did, and rule 3
3560
+ * failed the whole site for one section.
3561
+ *
3562
+ * The section places, in order of preference:
3563
+ * 1. the form whose name best matches it, among those the plan creates and
3564
+ * the site has, preferring one the plan already places — so a guided
3565
+ * start, which makes one form, places that same form again rather than a
3566
+ * second one;
3567
+ * 2. where there is no form at all and this job may create one, a form the
3568
+ * plan now creates: named for the `new:` reference the section already
3569
+ * holds when it names a form it never declared, the contact form
3570
+ * otherwise.
3571
+ *
3572
+ * Nothing is taken out of the plan: the section keeps its name, its other
3573
+ * uses and its items. Where the site has no form and the job may create none,
3574
+ * rule 3 asks for nothing (AGL-3596), and neither does this. A section whose
3575
+ * uses are already full is left for the rule to ask about.
3576
+ */ export function aiSettlePlanForms(plan, inventory, capabilities = null) {
3577
+ if (!aiPlanCanPlaceForm(inventory, capabilities)) return plan;
3578
+ const kinds = inventoryKinds(inventory);
3579
+ const unbound = (current)=>sectionPaths(current).filter(({ section })=>FORM_SECTION_NAME.test(section.name) && section.uses.length < AI_BUILD_PLAN_LIMITS.uses && !section.uses.some((ref)=>refKind(ref, current, kinds) === 'form'));
3580
+ const first = unbound(plan);
3581
+ if (!first.length) return plan;
3582
+ let next = plan;
3583
+ if (!planFormChoices(next, inventory).length) {
3584
+ if (capabilities && !capabilities.create.form.allowed) return plan;
3585
+ // A form the section already names and the plan never declared is the form it meant.
3586
+ const named = first.flatMap(({ section })=>section.uses).find((ref)=>isAiPlanNewRef(ref) && !aiPlanCreateFor(next, ref) && !aiPlanItemFor(next, ref));
3587
+ const taken = new Set(next.create.map((entry)=>entry.name.toLowerCase()));
3588
+ const name = named ? named.slice('new:'.length).trim() : taken.has(AI_PLAN_SETTLED_FORM.name.toLowerCase()) ? 'Get in touch form' : AI_PLAN_SETTLED_FORM.name;
3589
+ next = _extends({}, next, {
3590
+ create: [
3591
+ ...next.create,
3592
+ {
3593
+ kind: 'form',
3594
+ name,
3595
+ why: AI_PLAN_SETTLED_FORM.why,
3596
+ duplicateOf: null,
3597
+ fields: [
3598
+ ...AI_PLAN_SETTLED_FORM.fields
3599
+ ]
3600
+ }
3601
+ ]
3602
+ });
3603
+ }
3604
+ const choices = planFormChoices(next, inventory);
3605
+ const placed = new Map();
3606
+ for (const { section } of sectionPaths(next)){
3607
+ for (const ref of section.uses){
3608
+ var _placed_get;
3609
+ if (refKind(ref, next, kinds) !== 'form') continue;
3610
+ const key = formChoiceKey(ref, next);
3611
+ placed.set(key, ((_placed_get = placed.get(key)) != null ? _placed_get : 0) + 1);
3612
+ }
3613
+ }
3614
+ const settle = new Map();
3615
+ for (const { section, path } of unbound(next)){
3616
+ const form = bestPlanForm(section, choices, placed);
3617
+ if (form) settle.set(path, form.ref);
3618
+ }
3619
+ if (!settle.size) return next;
3620
+ return _extends({}, next, {
3621
+ screens: next.screens.map((screen, screenIndex)=>_extends({}, screen, {
3622
+ sections: screen.sections.map((section, sectionIndex)=>{
3623
+ const ref = settle.get(`screens[${screenIndex}].sections[${sectionIndex}]`);
3624
+ if (!ref) return section;
3625
+ // The undeclared reference the form was named for now names it; any other stays.
3626
+ const uses = section.uses.some((use)=>formChoiceKey(use, next) === ref) ? section.uses : [
3627
+ ...section.uses,
3628
+ ref
3629
+ ];
3630
+ return _extends({}, section, {
3631
+ uses
3632
+ });
3633
+ })
3634
+ }))
3635
+ });
3636
+ }
3637
+ /** The forms a section that collects answers can place, as the re-ask names them. */ function planFormChoicesText(plan, inventory) {
3638
+ const choices = planFormChoices(plan, inventory);
3639
+ if (!choices.length) return 'Plan one in create as a form, and put new:<its name> in the section\'s uses.';
3640
+ return `Put one of these in the section's uses: ${choices.map((choice)=>choice.created ? choice.ref : `${choice.ref} (${choice.name})`).join(', ')}.`;
3641
+ }
3369
3642
  /**
3370
3643
  * Rule 2 (plan): every screen declares a layout, no section is a copy of a
3371
3644
  * layout region, and no section places a layout, which frames a whole screen
@@ -3426,6 +3699,7 @@ const FORM_SECTION_NAME = /\bform\b|\bsign[- ]?up\b|\bsubscribe\b|\bnewsletter\b
3426
3699
  rule: 3,
3427
3700
  code: 'plan-form-not-placed',
3428
3701
  message: 'A section collects answers without a form. Reuse a form from the Forms page, or plan one there, and place it by id.',
3702
+ detail: planFormChoicesText(plan, inventory),
3429
3703
  paths: unbound.map((entry)=>entry.path)
3430
3704
  }
3431
3705
  ] : [];