@aglyn/plugins-ai 1.0.0-beta.236 → 1.0.0-beta.238

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 (164) hide show
  1. package/package.json +17 -17
  2. package/src/lib/components/ai-credits-prompt.component.d.ts +44 -0
  3. package/src/lib/components/ai-credits-prompt.component.js +117 -0
  4. package/src/lib/components/ai-credits-prompt.component.js.map +1 -0
  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-plan.component.d.ts +5 -4
  9. package/src/lib/components/ai-job-plan.component.js +48 -12
  10. package/src/lib/components/ai-job-plan.component.js.map +1 -1
  11. package/src/lib/components/ai-job-requests.d.ts +10 -0
  12. package/src/lib/components/ai-job-requests.js +12 -4
  13. package/src/lib/components/ai-job-requests.js.map +1 -1
  14. package/src/lib/components/ai-jobs-list-page.component.js +141 -72
  15. package/src/lib/components/ai-jobs-list-page.component.js.map +1 -1
  16. package/src/lib/components/ai-site-build-page.component.js +48 -9
  17. package/src/lib/components/ai-site-build-page.component.js.map +1 -1
  18. package/src/lib/components/ai-site-start-card.component.js +68 -22
  19. package/src/lib/components/ai-site-start-card.component.js.map +1 -1
  20. package/src/lib/components/ai-usage-strip.component.d.ts +19 -2
  21. package/src/lib/components/ai-usage-strip.component.js +40 -9
  22. package/src/lib/components/ai-usage-strip.component.js.map +1 -1
  23. package/src/lib/components/assist-build-card.component.d.ts +3 -1
  24. package/src/lib/components/assist-build-card.component.js +58 -7
  25. package/src/lib/components/assist-build-card.component.js.map +1 -1
  26. package/src/lib/components/assist-jobs-drawer.component.js +66 -22
  27. package/src/lib/components/assist-jobs-drawer.component.js.map +1 -1
  28. package/src/lib/components/assist-panel.component.js +89 -52
  29. package/src/lib/components/assist-panel.component.js.map +1 -1
  30. package/src/lib/jobs/ai-job-admission.d.ts +22 -2
  31. package/src/lib/jobs/ai-job-admission.js.map +1 -1
  32. package/src/lib/jobs/ai-job-build-step.d.ts +24 -11
  33. package/src/lib/jobs/ai-job-build-step.js +54 -35
  34. package/src/lib/jobs/ai-job-build-step.js.map +1 -1
  35. package/src/lib/jobs/ai-job-cancel.d.ts +71 -0
  36. package/src/lib/jobs/ai-job-cancel.js +102 -0
  37. package/src/lib/jobs/ai-job-cancel.js.map +1 -0
  38. package/src/lib/jobs/ai-job-component-step.js +4 -1
  39. package/src/lib/jobs/ai-job-component-step.js.map +1 -1
  40. package/src/lib/jobs/ai-job-drafts.js +2 -0
  41. package/src/lib/jobs/ai-job-drafts.js.map +1 -1
  42. package/src/lib/jobs/ai-job-form-step.js +4 -1
  43. package/src/lib/jobs/ai-job-form-step.js.map +1 -1
  44. package/src/lib/jobs/ai-job-layout-language.d.ts +2 -0
  45. package/src/lib/jobs/ai-job-layout-language.js +13 -1
  46. package/src/lib/jobs/ai-job-layout-language.js.map +1 -1
  47. package/src/lib/jobs/ai-job-layout-step.js +6 -1
  48. package/src/lib/jobs/ai-job-layout-step.js.map +1 -1
  49. package/src/lib/jobs/ai-job-page-language.js +12 -0
  50. package/src/lib/jobs/ai-job-page-language.js.map +1 -1
  51. package/src/lib/jobs/ai-job-page-step.d.ts +13 -0
  52. package/src/lib/jobs/ai-job-page-step.js +46 -4
  53. package/src/lib/jobs/ai-job-page-step.js.map +1 -1
  54. package/src/lib/jobs/ai-job-plan-step.d.ts +42 -0
  55. package/src/lib/jobs/ai-job-plan-step.js +118 -18
  56. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  57. package/src/lib/jobs/ai-job-site-content.d.ts +49 -3
  58. package/src/lib/jobs/ai-job-site-content.js +196 -37
  59. package/src/lib/jobs/ai-job-site-content.js.map +1 -1
  60. package/src/lib/jobs/ai-job-site-step.js +49 -16
  61. package/src/lib/jobs/ai-job-site-step.js.map +1 -1
  62. package/src/lib/jobs/ai-job-template-step.js +4 -1
  63. package/src/lib/jobs/ai-job-template-step.js.map +1 -1
  64. package/src/lib/jobs/ai-job-text-step.d.ts +8 -0
  65. package/src/lib/jobs/ai-job-text-step.js.map +1 -1
  66. package/src/lib/jobs/ai-jobs.d.ts +55 -13
  67. package/src/lib/jobs/ai-jobs.js +213 -41
  68. package/src/lib/jobs/ai-jobs.js.map +1 -1
  69. package/src/lib/jobs/ai-layout-stock-photos.d.ts +194 -34
  70. package/src/lib/jobs/ai-layout-stock-photos.js +861 -146
  71. package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
  72. package/src/lib/jobs/fixtures/ai-ceramics-portfolio-site-plan.d.ts +30 -0
  73. package/src/lib/jobs/fixtures/ai-ceramics-portfolio-site-plan.js +220 -0
  74. package/src/lib/jobs/fixtures/ai-ceramics-portfolio-site-plan.js.map +1 -0
  75. package/src/lib/jobs/fixtures/ai-stock-photo-pixabay-recordings.d.ts +27 -0
  76. package/src/lib/jobs/fixtures/ai-stock-photo-pixabay-recordings.js +151 -0
  77. package/src/lib/jobs/fixtures/ai-stock-photo-pixabay-recordings.js.map +1 -0
  78. package/src/lib/layout-language/ai-layout-compiler.d.ts +33 -0
  79. package/src/lib/layout-language/ai-layout-compiler.js +381 -18
  80. package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
  81. package/src/lib/layout-language/ai-layout-design.d.ts +6 -3
  82. package/src/lib/layout-language/ai-layout-design.js +5 -3
  83. package/src/lib/layout-language/ai-layout-design.js.map +1 -1
  84. package/src/lib/layout-language/ai-layout-frame.d.ts +12 -0
  85. package/src/lib/layout-language/ai-layout-frame.js +58 -3
  86. package/src/lib/layout-language/ai-layout-frame.js.map +1 -1
  87. package/src/lib/layout-language/ai-layout-gaps.js +3 -1
  88. package/src/lib/layout-language/ai-layout-gaps.js.map +1 -1
  89. package/src/lib/layout-language/ai-layout-language.js +1 -1
  90. package/src/lib/layout-language/ai-layout-language.js.map +1 -1
  91. package/src/lib/layout-language/ai-layout-listings.d.ts +49 -1
  92. package/src/lib/layout-language/ai-layout-listings.js +117 -47
  93. package/src/lib/layout-language/ai-layout-listings.js.map +1 -1
  94. package/src/lib/layout-language/ai-layout-pictures.d.ts +10 -1
  95. package/src/lib/layout-language/ai-layout-pictures.js +5 -2
  96. package/src/lib/layout-language/ai-layout-pictures.js.map +1 -1
  97. package/src/lib/layout-language/ai-layout-storefront.d.ts +82 -0
  98. package/src/lib/layout-language/ai-layout-storefront.js +223 -0
  99. package/src/lib/layout-language/ai-layout-storefront.js.map +1 -0
  100. package/src/lib/model/ai-build-job.d.ts +39 -0
  101. package/src/lib/model/ai-build-job.js +100 -1
  102. package/src/lib/model/ai-build-job.js.map +1 -1
  103. package/src/lib/model/ai-build-progress.d.ts +8 -0
  104. package/src/lib/model/ai-build-progress.js +35 -0
  105. package/src/lib/model/ai-build-progress.js.map +1 -1
  106. package/src/lib/model/ai-credit-estimate.d.ts +249 -0
  107. package/src/lib/model/ai-credit-estimate.js +278 -0
  108. package/src/lib/model/ai-credit-estimate.js.map +1 -0
  109. package/src/lib/model/ai-job-cancel-copy.d.ts +44 -0
  110. package/src/lib/model/ai-job-cancel-copy.js +53 -0
  111. package/src/lib/model/ai-job-cancel-copy.js.map +1 -0
  112. package/src/lib/model/ai-job-failure-copy.d.ts +4 -1
  113. package/src/lib/model/ai-job-failure-copy.js +8 -3
  114. package/src/lib/model/ai-job-failure-copy.js.map +1 -1
  115. package/src/lib/model/ai-jobs.types.d.ts +35 -0
  116. package/src/lib/model/ai-jobs.types.js.map +1 -1
  117. package/src/lib/model/ai-site-build-progress.js +7 -4
  118. package/src/lib/model/ai-site-build-progress.js.map +1 -1
  119. package/src/lib/model/ai-site-job.d.ts +76 -22
  120. package/src/lib/model/ai-site-job.js +125 -23
  121. package/src/lib/model/ai-site-job.js.map +1 -1
  122. package/src/lib/model/ai-site-kinds.d.ts +4 -1
  123. package/src/lib/model/ai-site-kinds.js +35 -7
  124. package/src/lib/model/ai-site-kinds.js.map +1 -1
  125. package/src/lib/runtime/ai-doctrine-validators.d.ts +37 -1
  126. package/src/lib/runtime/ai-doctrine-validators.js +161 -7
  127. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  128. package/src/lib/runtime/ai-doctrine.js +4 -3
  129. package/src/lib/runtime/ai-doctrine.js.map +1 -1
  130. package/src/lib/runtime/ai-node-tree.d.ts +11 -0
  131. package/src/lib/runtime/ai-node-tree.js +26 -0
  132. package/src/lib/runtime/ai-node-tree.js.map +1 -1
  133. package/src/lib/runtime/ai-palette.generated.js +592 -14
  134. package/src/lib/runtime/ai-palette.generated.js.map +1 -1
  135. package/src/lib/server/ai-jobs-cancel.d.ts +13 -5
  136. package/src/lib/server/ai-jobs-cancel.js +27 -9
  137. package/src/lib/server/ai-jobs-cancel.js.map +1 -1
  138. package/src/lib/server/ai-jobs-gate.d.ts +12 -2
  139. package/src/lib/server/ai-jobs-gate.js +20 -9
  140. package/src/lib/server/ai-jobs-gate.js.map +1 -1
  141. package/src/lib/server/ai-jobs-resume.js +93 -35
  142. package/src/lib/server/ai-jobs-resume.js.map +1 -1
  143. package/src/lib/server/ai-jobs-route.js +45 -4
  144. package/src/lib/server/ai-jobs-route.js.map +1 -1
  145. package/src/lib/server/assist-chat.js +4 -1
  146. package/src/lib/server/assist-chat.js.map +1 -1
  147. package/src/lib/server/assist-edit.js +3 -1
  148. package/src/lib/server/assist-edit.js.map +1 -1
  149. package/src/lib/server/assist-music.d.ts +36 -0
  150. package/src/lib/server/assist-music.js +36 -0
  151. package/src/lib/server/assist-music.js.map +1 -0
  152. package/src/lib/server.js +6 -0
  153. package/src/lib/server.js.map +1 -1
  154. package/src/lib/usage/ai-usage-meter.d.ts +2 -1
  155. package/src/lib/usage/ai-usage-meter.js +17 -1
  156. package/src/lib/usage/ai-usage-meter.js.map +1 -1
  157. package/src/lib/usage/ai-usage-wire.d.ts +6 -0
  158. package/src/lib/usage/ai-usage-wire.js.map +1 -1
  159. package/src/lib/usage/assist-usage.d.ts +7 -0
  160. package/src/lib/usage/assist-usage.js +14 -4
  161. package/src/lib/usage/assist-usage.js.map +1 -1
  162. package/src/lib/usage/free-ai-credits-left.d.ts +7 -0
  163. package/src/lib/usage/free-ai-credits-left.js +4 -2
  164. package/src/lib/usage/free-ai-credits-left.js.map +1 -1
@@ -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, aiPlanCreateFor, 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;
@@ -2351,6 +2354,13 @@ const DUPLICATE_SX_MIN_NODES = 4;
2351
2354
  const key = aiEmbedVideoKey(url);
2352
2355
  return key && planned.some((embed)=>embed.url && aiEmbedVideoKey(embed.url) === key) ? 'planned' : 'unplanned-link';
2353
2356
  }
2357
+ /**
2358
+ * Containers whose empty state is something they draw themselves, so an empty
2359
+ * one is a placement and not a gap (AGL-3716): the Music player, placed with
2360
+ * no tracks, asks its owner to add their own; a model never fills it.
2361
+ */ const OWNS_ITS_EMPTY_STATE = new Set([
2362
+ 'musicPlayer'
2363
+ ]);
2354
2364
  /**
2355
2365
  * Rule 16. The flattest tree that renders the design: no container that
2356
2366
  * wraps one other container and adds nothing, no empty containers, no inline
@@ -2379,7 +2389,9 @@ const DUPLICATE_SX_MIN_NODES = 4;
2379
2389
  wrappers.push(id);
2380
2390
  }
2381
2391
  const boundForm = node.componentId === 'form' && typeof ((_node_props1 = node.props) == null ? void 0 : _node_props1['formId']) === 'string';
2382
- if ((entry == null ? void 0 : entry.acceptsChildren) && !children.length && !boundForm) empty.push(id);
2392
+ if ((entry == null ? void 0 : entry.acceptsChildren) && !children.length && !boundForm && !OWNS_ITS_EMPTY_STATE.has(node.componentId)) {
2393
+ empty.push(id);
2394
+ }
2383
2395
  if (outputKind !== 'email' && ((_node_sx2 = node.sx) == null ? void 0 : _node_sx2['fontFamily']) !== undefined) fonts.push(id);
2384
2396
  if (node.componentId === 'image') {
2385
2397
  var _node_props6;
@@ -3277,11 +3289,18 @@ const FORM_SECTION_NAME = /\bform\b|\bsign[- ]?up\b|\bsubscribe\b|\bnewsletter\b
3277
3289
  * creation before the rules read the plan, when exactly one creation matches
3278
3290
  * it with case, spaces and punctuation ignored. Two that match, or none, are
3279
3291
  * left for the rules to ask about.
3280
- */ 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) {
3281
3298
  if (!plan.create.length) return plan;
3299
+ const kinds = inventoryKinds(inventory);
3282
3300
  const resolve = (ref)=>{
3283
- if (!isAiPlanNewRef(ref) || aiPlanCreateFor(plan, ref)) return ref;
3284
- 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;
3285
3304
  const matches = plan.create.filter((entry)=>creationKey(entry.name) === key);
3286
3305
  return matches.length === 1 ? `new:${matches[0].name}` : ref;
3287
3306
  };
@@ -3357,6 +3376,140 @@ const FORM_SECTION_NAME = /\bform\b|\bsign[- ]?up\b|\bsubscribe\b|\bnewsletter\b
3357
3376
  }))
3358
3377
  });
3359
3378
  }
3379
+ /** Every saved form a plan could place, the plan's own creations first. */ function planFormChoices(plan, inventory) {
3380
+ var _ref;
3381
+ return [
3382
+ ...plan.create.filter((entry)=>entry.kind === 'form').map((entry)=>({
3383
+ ref: `new:${entry.name}`,
3384
+ name: entry.name,
3385
+ created: true
3386
+ })),
3387
+ ...((_ref = inventory == null ? void 0 : inventory.forms) != null ? _ref : []).map((form)=>({
3388
+ ref: form.id,
3389
+ name: form.name,
3390
+ created: false
3391
+ }))
3392
+ ];
3393
+ }
3394
+ /** The form a plan's own words would name for a section's reference, so placements count alike. */ function formChoiceKey(ref, plan) {
3395
+ const created = isAiPlanNewRef(ref) ? aiPlanCreateFor(plan, ref) : undefined;
3396
+ return created ? `new:${created.name}` : ref;
3397
+ }
3398
+ /**
3399
+ * The form a section that collects answers places: the one whose name shares
3400
+ * the most words with the section's ("Commission inquiry" → "Commission
3401
+ * inquiry form"), then the one the plan already places most, then the plan's
3402
+ * own creation over a form the site has. `null` when there is none.
3403
+ */ function bestPlanForm(section, choices, placed) {
3404
+ var _ranked_;
3405
+ const words = new Set(nameTokens(section.name).filter((word)=>word !== 'form'));
3406
+ const score = (choice)=>nameTokens(choice.name).filter((word)=>word !== 'form' && words.has(word)).length;
3407
+ const ranked = [
3408
+ ...choices
3409
+ ].sort((a, b)=>{
3410
+ var _placed_get, _placed_get1;
3411
+ 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);
3412
+ });
3413
+ return (_ranked_ = ranked[0]) != null ? _ranked_ : null;
3414
+ }
3415
+ /** The contact form a plan is given when a section collects answers and the plan makes no form. */ export const AI_PLAN_SETTLED_FORM = {
3416
+ name: 'Contact form',
3417
+ why: 'Visitors need a way to reach the owner, and the site has no saved form for it.',
3418
+ fields: [
3419
+ 'name',
3420
+ 'email',
3421
+ 'message'
3422
+ ]
3423
+ };
3424
+ /**
3425
+ * A section that collects answers and places no saved form is given one
3426
+ * before the rules read the plan (AGL-3660), rather than failing the build.
3427
+ * A production guided start for a ceramic artist's portfolio (2026-10-09,
3428
+ * five pages, 94 credits refunded) planned a section that asks for
3429
+ * commissions on its work page without placing the contact form the plan
3430
+ * made for its Contact page; the answer and its re-ask both did, and rule 3
3431
+ * failed the whole site for one section.
3432
+ *
3433
+ * The section places, in order of preference:
3434
+ * 1. the form whose name best matches it, among those the plan creates and
3435
+ * the site has, preferring one the plan already places — so a guided
3436
+ * start, which makes one form, places that same form again rather than a
3437
+ * second one;
3438
+ * 2. where there is no form at all and this job may create one, a form the
3439
+ * plan now creates: named for the `new:` reference the section already
3440
+ * holds when it names a form it never declared, the contact form
3441
+ * otherwise.
3442
+ *
3443
+ * Nothing is taken out of the plan: the section keeps its name, its other
3444
+ * uses and its items. Where the site has no form and the job may create none,
3445
+ * rule 3 asks for nothing (AGL-3596), and neither does this. A section whose
3446
+ * uses are already full is left for the rule to ask about.
3447
+ */ export function aiSettlePlanForms(plan, inventory, capabilities = null) {
3448
+ if (!aiPlanCanPlaceForm(inventory, capabilities)) return plan;
3449
+ const kinds = inventoryKinds(inventory);
3450
+ 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'));
3451
+ const first = unbound(plan);
3452
+ if (!first.length) return plan;
3453
+ let next = plan;
3454
+ if (!planFormChoices(next, inventory).length) {
3455
+ if (capabilities && !capabilities.create.form.allowed) return plan;
3456
+ // A form the section already names and the plan never declared is the form it meant.
3457
+ const named = first.flatMap(({ section })=>section.uses).find((ref)=>isAiPlanNewRef(ref) && !aiPlanCreateFor(next, ref) && !aiPlanItemFor(next, ref));
3458
+ const taken = new Set(next.create.map((entry)=>entry.name.toLowerCase()));
3459
+ 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;
3460
+ next = _extends({}, next, {
3461
+ create: [
3462
+ ...next.create,
3463
+ {
3464
+ kind: 'form',
3465
+ name,
3466
+ why: AI_PLAN_SETTLED_FORM.why,
3467
+ duplicateOf: null,
3468
+ fields: [
3469
+ ...AI_PLAN_SETTLED_FORM.fields
3470
+ ]
3471
+ }
3472
+ ]
3473
+ });
3474
+ }
3475
+ const choices = planFormChoices(next, inventory);
3476
+ const placed = new Map();
3477
+ for (const { section } of sectionPaths(next)){
3478
+ for (const ref of section.uses){
3479
+ var _placed_get;
3480
+ if (refKind(ref, next, kinds) !== 'form') continue;
3481
+ const key = formChoiceKey(ref, next);
3482
+ placed.set(key, ((_placed_get = placed.get(key)) != null ? _placed_get : 0) + 1);
3483
+ }
3484
+ }
3485
+ const settle = new Map();
3486
+ for (const { section, path } of unbound(next)){
3487
+ const form = bestPlanForm(section, choices, placed);
3488
+ if (form) settle.set(path, form.ref);
3489
+ }
3490
+ if (!settle.size) return next;
3491
+ return _extends({}, next, {
3492
+ screens: next.screens.map((screen, screenIndex)=>_extends({}, screen, {
3493
+ sections: screen.sections.map((section, sectionIndex)=>{
3494
+ const ref = settle.get(`screens[${screenIndex}].sections[${sectionIndex}]`);
3495
+ if (!ref) return section;
3496
+ // The undeclared reference the form was named for now names it; any other stays.
3497
+ const uses = section.uses.some((use)=>formChoiceKey(use, next) === ref) ? section.uses : [
3498
+ ...section.uses,
3499
+ ref
3500
+ ];
3501
+ return _extends({}, section, {
3502
+ uses
3503
+ });
3504
+ })
3505
+ }))
3506
+ });
3507
+ }
3508
+ /** The forms a section that collects answers can place, as the re-ask names them. */ function planFormChoicesText(plan, inventory) {
3509
+ const choices = planFormChoices(plan, inventory);
3510
+ if (!choices.length) return 'Plan one in create as a form, and put new:<its name> in the section\'s uses.';
3511
+ return `Put one of these in the section's uses: ${choices.map((choice)=>choice.created ? choice.ref : `${choice.ref} (${choice.name})`).join(', ')}.`;
3512
+ }
3360
3513
  /**
3361
3514
  * Rule 2 (plan): every screen declares a layout, no section is a copy of a
3362
3515
  * layout region, and no section places a layout, which frames a whole screen
@@ -3417,6 +3570,7 @@ const FORM_SECTION_NAME = /\bform\b|\bsign[- ]?up\b|\bsubscribe\b|\bnewsletter\b
3417
3570
  rule: 3,
3418
3571
  code: 'plan-form-not-placed',
3419
3572
  message: 'A section collects answers without a form. Reuse a form from the Forms page, or plan one there, and place it by id.',
3573
+ detail: planFormChoicesText(plan, inventory),
3420
3574
  paths: unbound.map((entry)=>entry.path)
3421
3575
  }
3422
3576
  ] : [];