@aglyn/plugins-ai 1.0.0-beta.230 → 1.0.0-beta.231

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 (139) hide show
  1. package/package.json +17 -17
  2. package/src/lib/components/ai-assist-provider-on-host.component.d.ts +66 -0
  3. package/src/lib/components/ai-assist-provider-on-host.component.js +78 -0
  4. package/src/lib/components/ai-assist-provider-on-host.component.js.map +1 -0
  5. package/src/lib/components/ai-assist-provider.component.js +9 -10
  6. package/src/lib/components/ai-assist-provider.component.js.map +1 -1
  7. package/src/lib/components/ai-job-plan.component.js +3 -1
  8. package/src/lib/components/ai-job-plan.component.js.map +1 -1
  9. package/src/lib/components/ai-permissions-on-host.component.d.ts +3 -46
  10. package/src/lib/components/ai-permissions-on-host.component.js +5 -53
  11. package/src/lib/components/ai-permissions-on-host.component.js.map +1 -1
  12. package/src/lib/components/ai-site-build-page.component.js +20 -8
  13. package/src/lib/components/ai-site-build-page.component.js.map +1 -1
  14. package/src/lib/jobs/ai-build-unit-outcome.js +4 -2
  15. package/src/lib/jobs/ai-build-unit-outcome.js.map +1 -1
  16. package/src/lib/jobs/ai-job-auto-confirm.d.ts +8 -0
  17. package/src/lib/jobs/ai-job-auto-confirm.js +30 -6
  18. package/src/lib/jobs/ai-job-auto-confirm.js.map +1 -1
  19. package/src/lib/jobs/ai-job-build-step.js +11 -0
  20. package/src/lib/jobs/ai-job-build-step.js.map +1 -1
  21. package/src/lib/jobs/ai-job-generation.d.ts +8 -0
  22. package/src/lib/jobs/ai-job-generation.js +13 -2
  23. package/src/lib/jobs/ai-job-generation.js.map +1 -1
  24. package/src/lib/jobs/ai-job-insight-step.js +8 -1
  25. package/src/lib/jobs/ai-job-insight-step.js.map +1 -1
  26. package/src/lib/jobs/ai-job-layout-language.d.ts +73 -0
  27. package/src/lib/jobs/ai-job-layout-language.js +194 -0
  28. package/src/lib/jobs/ai-job-layout-language.js.map +1 -0
  29. package/src/lib/jobs/ai-job-layout-site-pages.d.ts +13 -2
  30. package/src/lib/jobs/ai-job-layout-site-pages.js +18 -2
  31. package/src/lib/jobs/ai-job-layout-site-pages.js.map +1 -1
  32. package/src/lib/jobs/ai-job-layout-step.d.ts +5 -0
  33. package/src/lib/jobs/ai-job-layout-step.js +105 -5
  34. package/src/lib/jobs/ai-job-layout-step.js.map +1 -1
  35. package/src/lib/jobs/ai-job-page-language.d.ts +149 -0
  36. package/src/lib/jobs/ai-job-page-language.js +355 -0
  37. package/src/lib/jobs/ai-job-page-language.js.map +1 -0
  38. package/src/lib/jobs/ai-job-page-sections.d.ts +37 -1
  39. package/src/lib/jobs/ai-job-page-sections.js +93 -11
  40. package/src/lib/jobs/ai-job-page-sections.js.map +1 -1
  41. package/src/lib/jobs/ai-job-page-step.d.ts +4 -4
  42. package/src/lib/jobs/ai-job-page-step.js +105 -14
  43. package/src/lib/jobs/ai-job-page-step.js.map +1 -1
  44. package/src/lib/jobs/ai-job-plan-step.d.ts +1 -1
  45. package/src/lib/jobs/ai-job-plan-step.js +8 -2
  46. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  47. package/src/lib/jobs/ai-job-site-step.d.ts +4 -0
  48. package/src/lib/jobs/ai-job-site-step.js +20 -3
  49. package/src/lib/jobs/ai-job-site-step.js.map +1 -1
  50. package/src/lib/jobs/ai-job-workflow-step.js +2 -2
  51. package/src/lib/jobs/ai-job-workflow-step.js.map +1 -1
  52. package/src/lib/jobs/ai-jobs.d.ts +1 -1
  53. package/src/lib/jobs/ai-jobs.js +17 -8
  54. package/src/lib/jobs/ai-jobs.js.map +1 -1
  55. package/src/lib/jobs/ai-layout-full-height.d.ts +40 -0
  56. package/src/lib/jobs/ai-layout-full-height.js +82 -0
  57. package/src/lib/jobs/ai-layout-full-height.js.map +1 -0
  58. package/src/lib/jobs/ai-layout-site-facts.d.ts +54 -0
  59. package/src/lib/jobs/ai-layout-site-facts.js +153 -0
  60. package/src/lib/jobs/ai-layout-site-facts.js.map +1 -0
  61. package/src/lib/layout-language/ai-layout-compiler.d.ts +145 -0
  62. package/src/lib/layout-language/ai-layout-compiler.js +1148 -0
  63. package/src/lib/layout-language/ai-layout-compiler.js.map +1 -0
  64. package/src/lib/layout-language/ai-layout-copy.d.ts +73 -0
  65. package/src/lib/layout-language/ai-layout-copy.js +137 -0
  66. package/src/lib/layout-language/ai-layout-copy.js.map +1 -0
  67. package/src/lib/layout-language/ai-layout-frame.d.ts +52 -0
  68. package/src/lib/layout-language/ai-layout-frame.js +457 -0
  69. package/src/lib/layout-language/ai-layout-frame.js.map +1 -0
  70. package/src/lib/layout-language/ai-layout-gaps.d.ts +63 -0
  71. package/src/lib/layout-language/ai-layout-gaps.js +345 -0
  72. package/src/lib/layout-language/ai-layout-gaps.js.map +1 -0
  73. package/src/lib/layout-language/ai-layout-language.d.ts +161 -0
  74. package/src/lib/layout-language/ai-layout-language.js +446 -0
  75. package/src/lib/layout-language/ai-layout-language.js.map +1 -0
  76. package/src/lib/layout-language/ai-layout-links.d.ts +80 -0
  77. package/src/lib/layout-language/ai-layout-links.js +109 -0
  78. package/src/lib/layout-language/ai-layout-links.js.map +1 -0
  79. package/src/lib/layout-language/ai-layout-store.d.ts +39 -0
  80. package/src/lib/layout-language/ai-layout-store.js +69 -0
  81. package/src/lib/layout-language/ai-layout-store.js.map +1 -0
  82. package/src/lib/layout-language/ai-layout-tree.d.ts +59 -0
  83. package/src/lib/layout-language/ai-layout-tree.js +84 -0
  84. package/src/lib/layout-language/ai-layout-tree.js.map +1 -0
  85. package/src/lib/lazy-widget.d.ts +45 -0
  86. package/src/lib/lazy-widget.js +53 -0
  87. package/src/lib/lazy-widget.js.map +1 -0
  88. package/src/lib/model/ai-build-job.js +2 -1
  89. package/src/lib/model/ai-build-job.js.map +1 -1
  90. package/src/lib/model/ai-build-progress.d.ts +20 -3
  91. package/src/lib/model/ai-build-progress.js +48 -6
  92. package/src/lib/model/ai-build-progress.js.map +1 -1
  93. package/src/lib/model/ai-host-publish-context.d.ts +51 -0
  94. package/src/lib/model/ai-host-publish-context.js +104 -0
  95. package/src/lib/model/ai-host-publish-context.js.map +1 -0
  96. package/src/lib/model/ai-job-activity.d.ts +22 -0
  97. package/src/lib/model/ai-job-activity.js +22 -1
  98. package/src/lib/model/ai-job-activity.js.map +1 -1
  99. package/src/lib/model/ai-job-failure-copy.d.ts +38 -1
  100. package/src/lib/model/ai-job-failure-copy.js +56 -1
  101. package/src/lib/model/ai-job-failure-copy.js.map +1 -1
  102. package/src/lib/model/ai-job-notice.d.ts +3 -1
  103. package/src/lib/model/ai-job-notice.js +31 -9
  104. package/src/lib/model/ai-job-notice.js.map +1 -1
  105. package/src/lib/model/ai-jobs.types.d.ts +19 -0
  106. package/src/lib/model/ai-jobs.types.js.map +1 -1
  107. package/src/lib/model/ai-site-build-progress.d.ts +5 -1
  108. package/src/lib/model/ai-site-build-progress.js +27 -4
  109. package/src/lib/model/ai-site-build-progress.js.map +1 -1
  110. package/src/lib/model/ai-site-job.d.ts +8 -0
  111. package/src/lib/model/ai-site-job.js +9 -0
  112. package/src/lib/model/ai-site-job.js.map +1 -1
  113. package/src/lib/model/ai-site-start-seo.d.ts +1 -1
  114. package/src/lib/model/ai-site-start.d.ts +5 -2
  115. package/src/lib/model/ai-site-start.js +5 -2
  116. package/src/lib/model/ai-site-start.js.map +1 -1
  117. package/src/lib/plugin.js +79 -47
  118. package/src/lib/plugin.js.map +1 -1
  119. package/src/lib/providers/catalog.d.ts +1 -1
  120. package/src/lib/providers/catalog.js +3 -3
  121. package/src/lib/providers/catalog.js.map +1 -1
  122. package/src/lib/runtime/ai-doctrine-validators.d.ts +82 -0
  123. package/src/lib/runtime/ai-doctrine-validators.js +436 -4
  124. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  125. package/src/lib/runtime/ai-doctrine.js +12 -3
  126. package/src/lib/runtime/ai-doctrine.js.map +1 -1
  127. package/src/lib/runtime/ai-icon-library.d.ts +257 -0
  128. package/src/lib/runtime/ai-icon-library.js +254 -0
  129. package/src/lib/runtime/ai-icon-library.js.map +1 -0
  130. package/src/lib/runtime/ai-node-tree.d.ts +9 -0
  131. package/src/lib/runtime/ai-node-tree.js +30 -4
  132. package/src/lib/runtime/ai-node-tree.js.map +1 -1
  133. package/src/lib/runtime/ai-palette.d.ts +6 -0
  134. package/src/lib/runtime/ai-palette.generated.js +70 -0
  135. package/src/lib/runtime/ai-palette.generated.js.map +1 -1
  136. package/src/lib/runtime/ai-palette.js.map +1 -1
  137. package/src/lib/tools/ai-workflow-tool.d.ts +6 -2
  138. package/src/lib/tools/ai-workflow-tool.js +16 -14
  139. package/src/lib/tools/ai-workflow-tool.js.map +1 -1
@@ -18,6 +18,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
18
18
  import { formatBytes, measureNodeMap, nodeMapBytes } from "@aglyn/aglyn/app-utils/measure-node-map";
19
19
  import { MEDIA_CDN_VARIANT_WIDTHS, parseMediaRef } from "@aglyn/aglyn/app-utils/media-ref";
20
20
  import { ESTIMATED_PAGE_TRANSFER_BYTES } from "@aglyn/aglyn/app-utils/plan-entitlements";
21
+ import { createIdUrlSafe } from "@aglyn/aglyn/foundation/constants/app";
21
22
  import { FREE_AI_TASTE_CREDITS_PER_MONTH } from "../plan-entitlements.js";
22
23
  import { REUSABLE_INSTANCE_COMPONENT_ID, REUSABLE_INSTANCE_PROP_VALUES_KEY } from "@aglyn/aglyn/app-utils/reusable-component-keys";
23
24
  import { parseBreakpointSpan } from "@aglyn/shared-data-enums/breakpoint-span";
@@ -27,7 +28,7 @@ import { aiCreationNoun, aiPlanCanPlaceForm, aiPlanUncreatable, aiPlanUncreatabl
27
28
  import { aiFreeSiteSectionsWithin } from "../model/ai-site-job.js";
28
29
  import { AI_INSTANCE_REF_PROP, validateAiNodeTree } from "./ai-node-tree.js";
29
30
  import { AI_PAGE_SCROLL_TO_PROP, aiPageLinkTarget, aiStoredScrollTargets } from "./ai-page-links.js";
30
- import { AI_EMAIL_CLIP_BYTES, AI_OUTPUT_BUDGETS, AI_OUTPUT_SURFACE, isHeadlineVariant } from "./ai-palette.js";
31
+ import { AI_EMAIL_CLIP_BYTES, AI_OUTPUT_BUDGETS, AI_OUTPUT_SURFACE, AI_TEXT_LIMITS, isHeadlineVariant } from "./ai-palette.js";
31
32
  import { AI_PALETTE, AI_SX_TOKENS } from "./ai-palette.generated.js";
32
33
  /** Each rule's title, in the words the proposal and the docs page use. */ export const AI_DOCTRINE_RULES = {
33
34
  1: 'Repeats become one reusable component',
@@ -170,13 +171,13 @@ function listNames(ids, tree) {
170
171
  * the outermost repeat: the header inside each of twelve cards repeats
171
172
  * because the card does, and the fix is the card. `groupOf` counts each
172
173
  * group's repeats apart; absent, the whole tree is one group.
173
- */ function repeatedShapes(tree, index, minNodes, minCount, groupOf = ()=>tree.rootId) {
174
+ */ function repeatedShapes(tree, index, minNodes, minCount, groupOf = ()=>tree.rootId, skip = ()=>false) {
174
175
  const byShape = new Map();
175
176
  for (const visit of index.visits){
176
177
  var _index_sizeOf_get, _byShape_get;
177
178
  if (visit.id === tree.rootId) continue;
178
179
  if (((_index_sizeOf_get = index.sizeOf.get(visit.id)) != null ? _index_sizeOf_get : 0) < minNodes) continue;
179
- if (repeatedByData(tree, visit)) continue;
180
+ if (repeatedByData(tree, visit) || skip(visit)) continue;
180
181
  const shape = JSON.stringify([
181
182
  groupOf(visit),
182
183
  index.shapeOf.get(visit.id)
@@ -202,6 +203,71 @@ function listNames(ids, tree) {
202
203
  ids
203
204
  }));
204
205
  }
206
+ /** Elements that frame and space what they hold, as rule 1 reads a frame. */ const REPEAT_FRAMES = new Set([
207
+ 'muiBox',
208
+ 'muiStack',
209
+ 'muiContainer',
210
+ 'muiGrid',
211
+ 'section',
212
+ 'muiList'
213
+ ]);
214
+ /**
215
+ * What rule 1 does not call a block (AGL-3660), however like another it is:
216
+ *
217
+ * - a part of the page's OUTLINE — anything holding a heading at h1 or h2.
218
+ * Three sections that each open with an h2 over a row of items share a
219
+ * shape because a page has sections, and a component cannot carry the
220
+ * page's outline: a heading inside one is no heading of the page.
221
+ * - a FRAME OF ITEMS — a Grid container, a List, or a Stack of like
222
+ * elements — and any frame that holds only such frames. What repeats in
223
+ * it is its items, which are judged on their own: two rows of three
224
+ * instances of two different components are not one block.
225
+ *
226
+ * So rule 1 still refuses three like cards, and the same card on two pages;
227
+ * it no longer refuses a page for having more than two sections, or more than
228
+ * two groups.
229
+ */ function aiRepeatFrameOrSection(tree) {
230
+ var _ref;
231
+ const memo = new Map();
232
+ const childrenOf = (id)=>{
233
+ var _ref;
234
+ var _tree_nodes_id;
235
+ return ((_ref = (_tree_nodes_id = tree.nodes[id]) == null ? void 0 : _tree_nodes_id.nodes) != null ? _ref : []).filter((child)=>tree.nodes[child]);
236
+ };
237
+ const holdsOutline = (id)=>{
238
+ const node = tree.nodes[id];
239
+ const level = node ? aiHeadingLevel(node) : null;
240
+ return level !== null && level <= 2 || childrenOf(id).some(holdsOutline);
241
+ };
242
+ const itemFrame = (id)=>{
243
+ var _node_props;
244
+ const node = tree.nodes[id];
245
+ if (!node) return false;
246
+ if (node.componentId === 'muiGrid' && ((_node_props = node.props) == null ? void 0 : _node_props['container']) === true) return true;
247
+ if (node.componentId === 'muiList') return true;
248
+ const children = childrenOf(id);
249
+ // A Stack of like blocks — cards, rows, panels — and never of lines of text.
250
+ const kinds = new Set(children.map((child)=>tree.nodes[child].componentId));
251
+ return node.componentId === 'muiStack' && children.length >= 2 && kinds.size === 1 && children.every((child)=>{
252
+ var _tree_nodes_child_nodes;
253
+ return ((_tree_nodes_child_nodes = tree.nodes[child].nodes) != null ? _tree_nodes_child_nodes : []).length > 0;
254
+ });
255
+ };
256
+ const parentOf = new Map();
257
+ for (const [id, node] of Object.entries(tree.nodes))for (const child of (_ref = node == null ? void 0 : node.nodes) != null ? _ref : [])parentOf.set(child, id);
258
+ const exempt = (id)=>{
259
+ const known = memo.get(id);
260
+ if (known !== undefined) return known;
261
+ const node = tree.nodes[id];
262
+ const children = childrenOf(id);
263
+ const parent = parentOf.get(id);
264
+ const result = // An item of a frame is an item, whatever heading it carries.
265
+ holdsOutline(id) && !(parent !== undefined && itemFrame(parent)) || itemFrame(id) || !!node && REPEAT_FRAMES.has(node.componentId) && children.length > 0 && children.every(exempt);
266
+ memo.set(id, result);
267
+ return result;
268
+ };
269
+ return (visit)=>exempt(visit.id);
270
+ }
205
271
  /**
206
272
  * Rule 1. A block repeated on one page, or a block this page shares with
207
273
  * another page generated beside it, is one component placed as instances.
@@ -211,7 +277,18 @@ function listNames(ids, tree) {
211
277
  */ export function detectRepeatedSubtrees(tree, otherPages = [], context = {}) {
212
278
  if (context.reusableComponents === false) return [];
213
279
  const index = indexShapes(tree);
214
- const violations = repeatedShapes(tree, index, AI_REPEAT_MIN_NODES, AI_REPEAT_MIN_COUNT).map(({ ids })=>{
280
+ const violations = repeatedShapes(tree, index, AI_REPEAT_MIN_NODES, AI_REPEAT_MIN_COUNT, // Counted within the band of the page it sits in (AGL-3660), as rule 8
281
+ // counts a typed list: a heading over its words in the About band and in
282
+ // the Services band are two parts of two stories, not one block placed twice.
283
+ (visit)=>{
284
+ var _reverse_find;
285
+ return (_reverse_find = [
286
+ ...visit.ancestors
287
+ ].reverse().find((id)=>{
288
+ var _tree_nodes_id;
289
+ return ((_tree_nodes_id = tree.nodes[id]) == null ? void 0 : _tree_nodes_id.componentId) === 'section';
290
+ })) != null ? _reverse_find : tree.rootId;
291
+ }, aiRepeatFrameOrSection(tree)).map(({ ids })=>{
215
292
  const name = displayName(tree.nodes[ids[0]].componentId);
216
293
  return {
217
294
  rule: 1,
@@ -1258,6 +1335,192 @@ const GRID = 'muiGrid';
1258
1335
  }
1259
1336
  return violations;
1260
1337
  }
1338
+ /** The breakpoints a size names, in the order the renderer's string writes them. */ const GRID_SIZE_BREAKPOINTS = [
1339
+ 'xs',
1340
+ 'sm',
1341
+ 'md',
1342
+ 'lg',
1343
+ 'xl'
1344
+ ];
1345
+ /**
1346
+ * A size the model wrote as an object of breakpoints (`{ "xs": 12, "md": 4 }`),
1347
+ * which the palette validator drops because a size is stored as one string,
1348
+ * written as that string (`xs:12 md:4`). Anything else is not read.
1349
+ */ function writtenGridSizeText(size) {
1350
+ if (!isRecord(size)) return null;
1351
+ const pairs = [];
1352
+ for (const [breakpoint, value] of Object.entries(size)){
1353
+ const span = typeof value === 'string' && value.trim() !== '' ? Number(value) : value;
1354
+ if (!GRID_SIZE_BREAKPOINTS.includes(breakpoint) || typeof span !== 'number' || !Number.isInteger(span)) return null;
1355
+ pairs.push(`${breakpoint}:${span}`);
1356
+ }
1357
+ pairs.sort((a, b)=>GRID_SIZE_BREAKPOINTS.indexOf(a.split(':')[0]) - GRID_SIZE_BREAKPOINTS.indexOf(b.split(':')[0]));
1358
+ return pairs.length ? pairs.join(' ') : null;
1359
+ }
1360
+ /**
1361
+ * Rule 12's `grid-item-size`, settled before the check reads it where it has
1362
+ * one answer (AGL-3596), as `aiSettlePlanRefs` settles a plan. The re-ask for
1363
+ * it states the very size to write, and a production Home page was refused and
1364
+ * refunded after its re-ask still left a container's items unsized: the model
1365
+ * breaks a different rule each run, and a rule whose answer the validator
1366
+ * already knows is applied rather than asked for.
1367
+ *
1368
+ * For every Grid container, its items are read exactly as
1369
+ * `detectUnresponsiveGrids` counts them, a Box or a Stack holding nothing but
1370
+ * sized Grid items lending its items to the container:
1371
+ *
1372
+ * - an item whose size is missing, or is not full width on a phone, takes the
1373
+ * size the model wrote as an object of breakpoints where that one is full
1374
+ * width on a phone, and otherwise the size the re-ask would ask for
1375
+ * (`gridItemSizeFor`);
1376
+ * - an element that is no Grid, directly in the container, is wrapped in a new
1377
+ * Grid item of that size, minted the way the palette validator mints ids;
1378
+ * - a container of several items none of which steps down to columns at a
1379
+ * larger width takes that size on every item, which is what the re-ask asks;
1380
+ * - a `container` written as the text "true" in any case or spacing
1381
+ * (`grid-container-text`) is true, since it can mean nothing else.
1382
+ *
1383
+ * A size that is already full width on a phone is kept. Nothing else rule 12
1384
+ * names is touched: a Grid that is not a container may be meant as a row or as
1385
+ * a stack (`grid-not-container`, `grid-as-stack`), and a sized item inside a
1386
+ * Box (`grid-item-outside-container`) is moved only by changing the Box.
1387
+ *
1388
+ * `written` is the node as the model wrote it, by an id of `tree`, as
1389
+ * `detectUnresponsiveGrids` reads it, for a tree the palette validator has
1390
+ * read; a tree as the model wrote it is read as it stands, which is how the
1391
+ * tree check settles a layout, a template or a component (`aiSettleWrittenGridItems`).
1392
+ * A tree with nothing to settle comes back
1393
+ * as the same object, so settling is idempotent. A node that is changed is
1394
+ * copied, never edited in place.
1395
+ */ export function aiSettleGridItems(tree, options = {}) {
1396
+ var _options_written, _options_mintId;
1397
+ const written = (_options_written = options.written) != null ? _options_written : ()=>undefined;
1398
+ const mintId = (_options_mintId = options.mintId) != null ? _options_mintId : createIdUrlSafe;
1399
+ const nodes = _extends({}, tree.nodes);
1400
+ const copied = new Set();
1401
+ const edit = (id)=>{
1402
+ if (!copied.has(id)) {
1403
+ const node = nodes[id];
1404
+ nodes[id] = _extends({}, node, {
1405
+ props: _extends({}, node.props)
1406
+ }, node.nodes ? {
1407
+ nodes: [
1408
+ ...node.nodes
1409
+ ]
1410
+ } : {});
1411
+ copied.add(id);
1412
+ }
1413
+ return nodes[id];
1414
+ };
1415
+ const setSize = (id, size)=>{
1416
+ var _nodes_id_props;
1417
+ if (((_nodes_id_props = nodes[id].props) == null ? void 0 : _nodes_id_props['size']) === size) return;
1418
+ edit(id).props['size'] = size;
1419
+ };
1420
+ const writtenProp = (id, name)=>{
1421
+ const node = written(id);
1422
+ return isRecord(node) && isRecord(node['props']) ? node['props'][name] : undefined;
1423
+ };
1424
+ const childrenOf = (id)=>{
1425
+ var _ref;
1426
+ var _nodes_id;
1427
+ return ((_ref = (_nodes_id = nodes[id]) == null ? void 0 : _nodes_id.nodes) != null ? _ref : []).filter((child)=>nodes[child]);
1428
+ };
1429
+ const isSized = (id)=>{
1430
+ var _nodes_id, _nodes_id_props;
1431
+ return ((_nodes_id = nodes[id]) == null ? void 0 : _nodes_id.componentId) === GRID && ((_nodes_id_props = nodes[id].props) == null ? void 0 : _nodes_id_props['size']) !== undefined;
1432
+ };
1433
+ const wrapsItems = (id)=>{
1434
+ const children = childrenOf(id);
1435
+ return GRID_ITEM_WRAPPERS.has(nodes[id].componentId) && children.length > 0 && children.every(isSized);
1436
+ };
1437
+ for (const { id } of walkTree(tree)){
1438
+ var _nodes_id_props;
1439
+ if (nodes[id].componentId !== GRID) continue;
1440
+ if (((_nodes_id_props = nodes[id].props) == null ? void 0 : _nodes_id_props['container']) !== true) {
1441
+ var _nodes_id_props1;
1442
+ // Text the palette validator dropped, or, in a tree as the model wrote it, the prop itself.
1443
+ const own = (_nodes_id_props1 = nodes[id].props) == null ? void 0 : _nodes_id_props1['container'];
1444
+ const text = own === undefined ? writtenProp(id, 'container') : own;
1445
+ if (typeof text !== 'string' || text.trim().toLowerCase() !== 'true') continue;
1446
+ edit(id).props['container'] = true;
1447
+ }
1448
+ const container = nodes[id];
1449
+ const columns = gridColumns(container);
1450
+ const children = childrenOf(id);
1451
+ const items = children.flatMap((child)=>wrapsItems(child) ? childrenOf(child) : [
1452
+ child
1453
+ ]);
1454
+ if (!items.length) continue;
1455
+ const size = gridItemSizeFor(items.length, columns);
1456
+ const settledItems = [];
1457
+ for (const item of items){
1458
+ var _nodes_item_props;
1459
+ if (nodes[item].componentId !== GRID) {
1460
+ var _host_nodes;
1461
+ // Only a direct child can be no Grid: a wrapper lends only sized Grid items.
1462
+ const wrapperId = mintId();
1463
+ const host = edit(id);
1464
+ host.nodes = ((_host_nodes = host.nodes) != null ? _host_nodes : []).map((child)=>child === item ? wrapperId : child);
1465
+ const shape = container;
1466
+ nodes[wrapperId] = _extends({}, '$id' in shape ? {
1467
+ $id: wrapperId
1468
+ } : {}, 'type' in shape ? {
1469
+ type: shape['type']
1470
+ } : {}, {
1471
+ componentId: GRID
1472
+ }, 'pluginId' in shape ? {
1473
+ pluginId: shape['pluginId']
1474
+ } : {}, 'parentId' in shape ? {
1475
+ parentId: id
1476
+ } : {}, {
1477
+ nodes: [
1478
+ item
1479
+ ],
1480
+ props: {
1481
+ size
1482
+ }
1483
+ });
1484
+ copied.add(wrapperId);
1485
+ if ('parentId' in nodes[item]) edit(item)['parentId'] = wrapperId;
1486
+ settledItems.push(wrapperId);
1487
+ continue;
1488
+ }
1489
+ if (!gridItemSpan((_nodes_item_props = nodes[item].props) == null ? void 0 : _nodes_item_props['size'], columns).phoneFull) {
1490
+ var _writtenProp;
1491
+ var _nodes_item_props1;
1492
+ const own = writtenGridSizeText((_writtenProp = writtenProp(item, 'size')) != null ? _writtenProp : (_nodes_item_props1 = nodes[item].props) == null ? void 0 : _nodes_item_props1['size']);
1493
+ setSize(item, own !== null && gridItemSpan(own, columns).phoneFull ? own : size);
1494
+ }
1495
+ settledItems.push(item);
1496
+ }
1497
+ const steps = settledItems.some((item)=>{
1498
+ var _nodes_item_props;
1499
+ return gridItemSpan((_nodes_item_props = nodes[item].props) == null ? void 0 : _nodes_item_props['size'], columns).steps;
1500
+ });
1501
+ if (settledItems.length >= 2 && !steps) for (const item of settledItems)setSize(item, size);
1502
+ }
1503
+ return copied.size ? _extends({}, tree, {
1504
+ nodes
1505
+ }) : tree;
1506
+ }
1507
+ /**
1508
+ * `aiSettleGridItems` on a tree as the model wrote it, before the palette
1509
+ * validator reads it (AGL-3596): the tree check every kind but a page section
1510
+ * goes through stores what the validator makes of its answer, so the answer
1511
+ * is settled first. Anything that is no flat node map comes back as it was.
1512
+ */ export function aiSettleWrittenGridItems(input) {
1513
+ if (!isRecord(input) || typeof input['rootId'] !== 'string' || !isRecord(input['nodes'])) return input;
1514
+ const nodes = Object.fromEntries(Object.entries(input['nodes']).filter(([, node])=>isRecord(node)));
1515
+ const tree = {
1516
+ rootId: input['rootId'],
1517
+ nodes: nodes
1518
+ };
1519
+ const settled = aiSettleGridItems(tree);
1520
+ return settled === tree ? input : _extends({}, input, {
1521
+ nodes: _extends({}, input['nodes'], settled.nodes)
1522
+ });
1523
+ }
1261
1524
  /** The palette colors a link or a button can take, and a band can be painted in. */ const LINK_COLOR_FAMILIES = new Set([
1262
1525
  'primary',
1263
1526
  'secondary',
@@ -2305,6 +2568,175 @@ const BUDGET_METRICS = [
2305
2568
  }
2306
2569
  return violations;
2307
2570
  }
2571
+ /** The key a repeated item is written under (`AI_REPEAT_KEY` in `ai-repeated-items.ts`, which imports this file). */ const REPEAT_KEY = 'repeat';
2572
+ /**
2573
+ * A node map that does not agree with itself, settled where it has one
2574
+ * reading (AGL-3596), before `detectDisagreeingNodes` reads it. A guided
2575
+ * start's Home page stopped on each of these after its re-ask, and each is
2576
+ * one line the model left out, not a choice it made.
2577
+ *
2578
+ * - A name listed under `nodes` that the map never holds is taken out of the
2579
+ * list: there is nothing written to place there.
2580
+ * - An element written and listed by nobody goes where the model said it
2581
+ * goes, when its own `parentId` names an element the tree reaches that can
2582
+ * hold it. Failing that, a single such element goes in the single element
2583
+ * the tree reaches that can hold elements and holds none, which is the shape
2584
+ * of a row whose item was written beside it. Failing both, it is dropped,
2585
+ * with everything under it: nothing on the page holds it, and none of it
2586
+ * would ever be shown.
2587
+ * - A repeated item (`repeat`) is the exception: dropping it would lose every
2588
+ * card it draws, so one that cannot be placed is left for the re-ask.
2589
+ *
2590
+ * Reads and returns the tree as the model wrote it; an input with nothing to
2591
+ * settle comes back as the same object.
2592
+ */ export function aiSettleDisagreeingNodes(input) {
2593
+ if (!isRecord(input) || typeof input['rootId'] !== 'string' || !isRecord(input['nodes'])) return input;
2594
+ const rootId = input['rootId'];
2595
+ const source = input['nodes'];
2596
+ if (!isRecord(source[rootId])) return input;
2597
+ const nodes = _extends({}, source);
2598
+ let changed = false;
2599
+ const listed = (id)=>{
2600
+ const node = nodes[id];
2601
+ return isRecord(node) && Array.isArray(node['nodes']) ? node['nodes'] : [];
2602
+ };
2603
+ const setListed = (id, children)=>{
2604
+ nodes[id] = _extends({}, nodes[id], {
2605
+ nodes: children
2606
+ });
2607
+ changed = true;
2608
+ };
2609
+ const holds = (id)=>{
2610
+ var _AI_PALETTE_node_componentId;
2611
+ const node = nodes[id];
2612
+ return isRecord(node) && typeof node['componentId'] === 'string' && ((_AI_PALETTE_node_componentId = AI_PALETTE[node['componentId']]) == null ? void 0 : _AI_PALETTE_node_componentId.acceptsChildren) === true;
2613
+ };
2614
+ const reach = ()=>{
2615
+ const reached = new Set([
2616
+ rootId
2617
+ ]);
2618
+ const queue = [
2619
+ rootId
2620
+ ];
2621
+ while(queue.length){
2622
+ for (const child of listed(queue.shift())){
2623
+ if (typeof child !== 'string' || reached.has(child) || !isRecord(nodes[child])) continue;
2624
+ reached.add(child);
2625
+ queue.push(child);
2626
+ }
2627
+ }
2628
+ return reached;
2629
+ };
2630
+ // Names that were never written come out of the lists that name them.
2631
+ for (const id of Object.keys(nodes)){
2632
+ const children = listed(id);
2633
+ const written = children.filter((child)=>typeof child !== 'string' || isRecord(nodes[child]));
2634
+ if (written.length !== children.length) setListed(id, written);
2635
+ }
2636
+ let reached = reach();
2637
+ const orphans = Object.keys(nodes).filter((id)=>!reached.has(id) && isRecord(nodes[id]));
2638
+ if (orphans.length) {
2639
+ const inOrphan = new Set(orphans.flatMap((id)=>listed(id)));
2640
+ const roots = orphans.filter((id)=>!inOrphan.has(id));
2641
+ const empty = [
2642
+ ...reached
2643
+ ].filter((id)=>id !== rootId && holds(id) && listed(id).length === 0);
2644
+ for (const id of roots){
2645
+ const named = nodes[id]['parentId'];
2646
+ const parent = typeof named === 'string' && reached.has(named) && holds(named) ? named : roots.length === 1 && empty.length === 1 ? empty[0] : null;
2647
+ if (parent) setListed(parent, [
2648
+ ...listed(parent),
2649
+ id
2650
+ ]);
2651
+ }
2652
+ reached = reach();
2653
+ const unplaced = Object.keys(nodes).filter((id)=>!reached.has(id) && isRecord(nodes[id]));
2654
+ // A repeated item, and what it holds, is kept for the re-ask to place.
2655
+ const kept = new Set();
2656
+ const queue = unplaced.filter((id)=>nodes[id][REPEAT_KEY] !== undefined);
2657
+ while(queue.length){
2658
+ const id = queue.shift();
2659
+ if (kept.has(id) || !isRecord(nodes[id])) continue;
2660
+ kept.add(id);
2661
+ for (const child of listed(id))if (typeof child === 'string') queue.push(child);
2662
+ }
2663
+ for (const id of unplaced){
2664
+ if (kept.has(id)) continue;
2665
+ delete nodes[id];
2666
+ changed = true;
2667
+ }
2668
+ }
2669
+ return changed ? _extends({}, input, {
2670
+ nodes
2671
+ }) : input;
2672
+ }
2673
+ /** The fewest words a heading cut at a boundary keeps, so a cut never leaves a stub. */ const HEADING_CUT_MIN_WORDS = 3;
2674
+ /** The least share of the ceiling a heading cut at a boundary keeps. */ const HEADING_CUT_MIN_SHARE = 0.3;
2675
+ /** Where a sentence ends: its stop is kept. */ const SENTENCE_END = /[.!?](?=\s)/g;
2676
+ /** Where a clause ends: the mark is dropped. */ const CLAUSE_END = /\s*(?:[,;:]|\s[—–-])\s/g;
2677
+ /**
2678
+ * The longest whole part of a heading that fits `limit`: up to the last
2679
+ * sentence that ends within it, else the last clause, or `null` where neither
2680
+ * does, or where what is left is a stub.
2681
+ */ function headingWithin(line, limit) {
2682
+ const keeps = (head)=>head.split(/\s+/).length >= HEADING_CUT_MIN_WORDS && head.length >= limit * HEADING_CUT_MIN_SHARE;
2683
+ for (const [pattern, keepMark] of [
2684
+ [
2685
+ SENTENCE_END,
2686
+ true
2687
+ ],
2688
+ [
2689
+ CLAUSE_END,
2690
+ false
2691
+ ]
2692
+ ]){
2693
+ let best = null;
2694
+ for (const match of line.matchAll(pattern)){
2695
+ var _match_index;
2696
+ const head = line.slice(0, ((_match_index = match.index) != null ? _match_index : 0) + (keepMark ? match[0].length : 0)).trim();
2697
+ if (head.length > limit) break;
2698
+ best = head;
2699
+ }
2700
+ if (best !== null && keeps(best)) return best;
2701
+ }
2702
+ return null;
2703
+ }
2704
+ /**
2705
+ * A heading written past the headline ceiling, settled where it has one
2706
+ * reading (AGL-3596). The palette validator cuts such a line where the
2707
+ * ceiling falls, mid-word, and the check refuses the cut (rule 14,
2708
+ * `copy-cut-at-ceiling`); live guided starts' heroes stopped on it after
2709
+ * their re-ask. A heading whose first sentence or clause ends within the
2710
+ * ceiling is a whole line already, so it is kept to that, its later words
2711
+ * dropped: "Free inspections after a storm, with photos of every shingle we
2712
+ * replace and a written…" becomes "Free inspections after a storm". A heading
2713
+ * with no such boundary, or whose part within is a stub, is left for the
2714
+ * re-ask. Reads and returns the tree as the model wrote it.
2715
+ */ export function aiSettleCutHeadings(input) {
2716
+ var _ref;
2717
+ var _AI_PALETTE_muiTypography_textLimits, _AI_PALETTE_muiTypography;
2718
+ if (!isRecord(input) || !isRecord(input['nodes'])) return input;
2719
+ const limit = Math.min((_ref = (_AI_PALETTE_muiTypography = AI_PALETTE['muiTypography']) == null ? void 0 : (_AI_PALETTE_muiTypography_textLimits = _AI_PALETTE_muiTypography.textLimits) == null ? void 0 : _AI_PALETTE_muiTypography_textLimits['children']) != null ? _ref : Infinity, AI_TEXT_LIMITS.headline);
2720
+ let nodes = null;
2721
+ for (const [id, node] of Object.entries(input['nodes'])){
2722
+ if (!isRecord(node) || node['componentId'] !== 'muiTypography' || !isRecord(node['props'])) continue;
2723
+ const line = node['props']['children'];
2724
+ if (!isHeadlineVariant(node['props']['variant']) || typeof line !== 'string') continue;
2725
+ const trimmed = line.trim();
2726
+ if (trimmed.length <= limit || trimmed.includes('{{')) continue;
2727
+ const head = headingWithin(trimmed, limit);
2728
+ if (head === null) continue;
2729
+ nodes != null ? nodes : nodes = _extends({}, input['nodes']);
2730
+ nodes[id] = _extends({}, node, {
2731
+ props: _extends({}, node['props'], {
2732
+ children: head
2733
+ })
2734
+ });
2735
+ }
2736
+ return nodes ? _extends({}, input, {
2737
+ nodes
2738
+ }) : input;
2739
+ }
2308
2740
  /**
2309
2741
  * The whole check on one emitted tree: the palette validator first, then
2310
2742
  * every tree rule, then the budget. `otherPages` are the trees generated