@aglyn/plugins-ai 1.0.0-beta.217 → 1.0.0-beta.219

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 (187) hide show
  1. package/README.md +2 -2
  2. package/package.json +17 -17
  3. package/src/index.d.ts +1 -0
  4. package/src/index.js.map +1 -1
  5. package/src/lib/activity/ai-activity-actions.js +10 -2
  6. package/src/lib/activity/ai-activity-actions.js.map +1 -1
  7. package/src/lib/components/ai-assist-provider.component.js +2 -2
  8. package/src/lib/components/ai-assist-provider.component.js.map +1 -1
  9. package/src/lib/components/ai-brief-dialog.component.js +3 -3
  10. package/src/lib/components/ai-brief-dialog.component.js.map +1 -1
  11. package/src/lib/components/ai-experiment-cards.component.js +1 -1
  12. package/src/lib/components/ai-experiment-cards.component.js.map +1 -1
  13. package/src/lib/components/ai-job-plan.component.d.ts +8 -0
  14. package/src/lib/components/ai-job-plan.component.js +21 -4
  15. package/src/lib/components/ai-job-plan.component.js.map +1 -1
  16. package/src/lib/components/ai-permissions-on-host.component.d.ts +12 -2
  17. package/src/lib/components/ai-permissions-on-host.component.js +12 -3
  18. package/src/lib/components/ai-permissions-on-host.component.js.map +1 -1
  19. package/src/lib/components/ai-product-copy-card.component.d.ts +1 -1
  20. package/src/lib/components/ai-product-copy-card.component.js.map +1 -1
  21. package/src/lib/components/ai-product-import-option.component.d.ts +1 -1
  22. package/src/lib/components/ai-product-import-option.component.js.map +1 -1
  23. package/src/lib/components/ai-product-zones.d.ts +124 -0
  24. package/src/lib/components/ai-product-zones.js +32 -0
  25. package/src/lib/components/ai-product-zones.js.map +1 -0
  26. package/src/lib/components/ai-products-hub-card.component.d.ts +1 -1
  27. package/src/lib/components/ai-products-hub-card.component.js.map +1 -1
  28. package/src/lib/components/ai-products-proposals.component.d.ts +1 -1
  29. package/src/lib/components/ai-products-proposals.component.js.map +1 -1
  30. package/src/lib/components/ai-seo-audit-card.component.js +35 -92
  31. package/src/lib/components/ai-seo-audit-card.component.js.map +1 -1
  32. package/src/lib/components/ai-seo-fields-card.component.js.map +1 -1
  33. package/src/lib/components/ai-theme-proposal-card.component.js.map +1 -1
  34. package/src/lib/components/assist-jobs-drawer.component.js +21 -15
  35. package/src/lib/components/assist-jobs-drawer.component.js.map +1 -1
  36. package/src/lib/components/assist-panel.component.js +2 -2
  37. package/src/lib/components/assist-panel.component.js.map +1 -1
  38. package/src/lib/components/assist-signals-page.component.js +2 -2
  39. package/src/lib/components/assist-signals-page.component.js.map +1 -1
  40. package/src/lib/jobs/ai-email-bindings.d.ts +28 -21
  41. package/src/lib/jobs/ai-email-bindings.js +75 -42
  42. package/src/lib/jobs/ai-email-bindings.js.map +1 -1
  43. package/src/lib/jobs/ai-job-campaign-step.d.ts +0 -2
  44. package/src/lib/jobs/ai-job-campaign-step.js +5 -10
  45. package/src/lib/jobs/ai-job-campaign-step.js.map +1 -1
  46. package/src/lib/jobs/ai-job-component-step.js +9 -3
  47. package/src/lib/jobs/ai-job-component-step.js.map +1 -1
  48. package/src/lib/jobs/ai-job-drafts.d.ts +9 -1
  49. package/src/lib/jobs/ai-job-drafts.js +12 -5
  50. package/src/lib/jobs/ai-job-drafts.js.map +1 -1
  51. package/src/lib/jobs/ai-job-email-step.d.ts +0 -2
  52. package/src/lib/jobs/ai-job-email-step.js +1 -3
  53. package/src/lib/jobs/ai-job-email-step.js.map +1 -1
  54. package/src/lib/jobs/ai-job-experiment-step.d.ts +1 -1
  55. package/src/lib/jobs/ai-job-experiment-step.js +1 -1
  56. package/src/lib/jobs/ai-job-experiment-step.js.map +1 -1
  57. package/src/lib/jobs/ai-job-generation.d.ts +1 -1
  58. package/src/lib/jobs/ai-job-generation.js +10 -2
  59. package/src/lib/jobs/ai-job-generation.js.map +1 -1
  60. package/src/lib/jobs/ai-job-layout-step.js +1 -1
  61. package/src/lib/jobs/ai-job-layout-step.js.map +1 -1
  62. package/src/lib/jobs/ai-job-page-sections.d.ts +4 -2
  63. package/src/lib/jobs/ai-job-page-sections.js +7 -4
  64. package/src/lib/jobs/ai-job-page-sections.js.map +1 -1
  65. package/src/lib/jobs/ai-job-page-step.js +7 -2
  66. package/src/lib/jobs/ai-job-page-step.js.map +1 -1
  67. package/src/lib/jobs/ai-job-plan-conformance.js +33 -0
  68. package/src/lib/jobs/ai-job-plan-conformance.js.map +1 -1
  69. package/src/lib/jobs/ai-job-plan-step.d.ts +3 -0
  70. package/src/lib/jobs/ai-job-plan-step.js +29 -17
  71. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  72. package/src/lib/jobs/ai-job-plugin-drafts.d.ts +14 -6
  73. package/src/lib/jobs/ai-job-plugin-drafts.js +12 -4
  74. package/src/lib/jobs/ai-job-plugin-drafts.js.map +1 -1
  75. package/src/lib/jobs/ai-job-products-step.d.ts +7 -0
  76. package/src/lib/jobs/ai-job-products-step.js +54 -32
  77. package/src/lib/jobs/ai-job-products-step.js.map +1 -1
  78. package/src/lib/jobs/ai-job-seo-step.d.ts +3 -24
  79. package/src/lib/jobs/ai-job-seo-step.js +35 -108
  80. package/src/lib/jobs/ai-job-seo-step.js.map +1 -1
  81. package/src/lib/jobs/ai-job-template-step.js +1 -0
  82. package/src/lib/jobs/ai-job-template-step.js.map +1 -1
  83. package/src/lib/jobs/ai-job-theme-step.d.ts +1 -1
  84. package/src/lib/jobs/ai-job-theme-step.js +1 -1
  85. package/src/lib/jobs/ai-job-theme-step.js.map +1 -1
  86. package/src/lib/jobs/ai-job-workflow-step.js +17 -9
  87. package/src/lib/jobs/ai-job-workflow-step.js.map +1 -1
  88. package/src/lib/jobs/ai-workflow-records.d.ts +19 -11
  89. package/src/lib/jobs/ai-workflow-records.js +60 -27
  90. package/src/lib/jobs/ai-workflow-records.js.map +1 -1
  91. package/src/lib/model/ai-allotments.d.ts +11 -3
  92. package/src/lib/model/ai-allotments.js +21 -4
  93. package/src/lib/model/ai-allotments.js.map +1 -1
  94. package/src/lib/model/ai-automation-outline.d.ts +21 -2
  95. package/src/lib/model/ai-automation-outline.js.map +1 -1
  96. package/src/lib/model/ai-build-plan.d.ts +57 -0
  97. package/src/lib/model/ai-build-plan.js +116 -7
  98. package/src/lib/model/ai-build-plan.js.map +1 -1
  99. package/src/lib/model/ai-seo.d.ts +11 -51
  100. package/src/lib/model/ai-seo.js +0 -17
  101. package/src/lib/model/ai-seo.js.map +1 -1
  102. package/src/lib/model/ai-site-inventory.js +1 -1
  103. package/src/lib/model/ai-site-inventory.js.map +1 -1
  104. package/src/lib/model/ai-template-subjects.js +2 -2
  105. package/src/lib/model/ai-template-subjects.js.map +1 -1
  106. package/src/lib/model/ai-theme-proposal.d.ts +1 -1
  107. package/src/lib/model/ai-theme-proposal.js +1 -1
  108. package/src/lib/model/ai-theme-proposal.js.map +1 -1
  109. package/src/lib/model/ai-workflow-job.d.ts +1 -3
  110. package/src/lib/model/ai-workflow-job.js +1 -2
  111. package/src/lib/model/ai-workflow-job.js.map +1 -1
  112. package/src/lib/plan-entitlements.d.ts +149 -0
  113. package/src/lib/plan-entitlements.js +168 -0
  114. package/src/lib/plan-entitlements.js.map +1 -0
  115. package/src/lib/plugin.js +1 -1
  116. package/src/lib/plugin.js.map +1 -1
  117. package/src/lib/runtime/ai-doctrine-validators.d.ts +25 -4
  118. package/src/lib/runtime/ai-doctrine-validators.js +179 -23
  119. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  120. package/src/lib/runtime/ai-doctrine.js +5 -5
  121. package/src/lib/runtime/ai-doctrine.js.map +1 -1
  122. package/src/lib/runtime/ai-eval-live.js +29 -14
  123. package/src/lib/runtime/ai-eval-live.js.map +1 -1
  124. package/src/lib/runtime/ai-eval.d.ts +8 -1
  125. package/src/lib/runtime/ai-eval.js +29 -2
  126. package/src/lib/runtime/ai-eval.js.map +1 -1
  127. package/src/lib/runtime/ai-node-tree.js +3 -3
  128. package/src/lib/runtime/ai-node-tree.js.map +1 -1
  129. package/src/lib/runtime/ai-palette.d.ts +6 -0
  130. package/src/lib/runtime/ai-palette.generated.js +21 -255
  131. package/src/lib/runtime/ai-palette.generated.js.map +1 -1
  132. package/src/lib/runtime/ai-palette.js +7 -1
  133. package/src/lib/runtime/ai-palette.js.map +1 -1
  134. package/src/lib/runtime/seo-audit.d.ts +13 -63
  135. package/src/lib/runtime/seo-audit.js +22 -207
  136. package/src/lib/runtime/seo-audit.js.map +1 -1
  137. package/src/lib/runtime/site-inventory.js +1 -1
  138. package/src/lib/runtime/site-inventory.js.map +1 -1
  139. package/src/lib/server/ai-admin-org.js +4 -25
  140. package/src/lib/server/ai-admin-org.js.map +1 -1
  141. package/src/lib/server/ai-admin-user.js +2 -19
  142. package/src/lib/server/ai-admin-user.js.map +1 -1
  143. package/src/lib/server/ai-assist-prompts.js +3 -3
  144. package/src/lib/server/ai-assist-prompts.js.map +1 -1
  145. package/src/lib/server/ai-insight-digest-route.js +8 -1
  146. package/src/lib/server/ai-insight-digest-route.js.map +1 -1
  147. package/src/lib/server/assist-chat.js +11 -10
  148. package/src/lib/server/assist-chat.js.map +1 -1
  149. package/src/lib/server/assist-view-context.js +23 -23
  150. package/src/lib/server/assist-view-context.js.map +1 -1
  151. package/src/lib/server.d.ts +1 -0
  152. package/src/lib/server.js.map +1 -1
  153. package/src/lib/testing/stand-in-automation-index.d.ts +20 -0
  154. package/src/lib/testing/stand-in-automation-index.js +102 -0
  155. package/src/lib/testing/stand-in-automation-index.js.map +1 -0
  156. package/src/lib/testing/stand-in-list-send-time.d.ts +4 -0
  157. package/src/lib/testing/stand-in-list-send-time.js +77 -0
  158. package/src/lib/testing/stand-in-list-send-time.js.map +1 -0
  159. package/src/lib/testing/stand-in-pipeline-index.d.ts +32 -0
  160. package/src/lib/testing/stand-in-pipeline-index.js +59 -0
  161. package/src/lib/testing/stand-in-pipeline-index.js.map +1 -0
  162. package/src/lib/testing/stand-in-product-index.d.ts +20 -0
  163. package/src/lib/testing/stand-in-product-index.js +105 -0
  164. package/src/lib/testing/stand-in-product-index.js.map +1 -0
  165. package/src/lib/tools/ai-component-tool.js +2 -2
  166. package/src/lib/tools/ai-component-tool.js.map +1 -1
  167. package/src/lib/tools/ai-duplicate-resource-tool.js +1 -1
  168. package/src/lib/tools/ai-duplicate-resource-tool.js.map +1 -1
  169. package/src/lib/tools/ai-seo-tool.d.ts +6 -18
  170. package/src/lib/tools/ai-seo-tool.js +7 -38
  171. package/src/lib/tools/ai-seo-tool.js.map +1 -1
  172. package/src/lib/tools/ai-workflow-tool.d.ts +1 -1
  173. package/src/lib/tools/ai-workflow-tool.js +1 -1
  174. package/src/lib/tools/ai-workflow-tool.js.map +1 -1
  175. package/src/lib/usage/ai-allotment-alerts.d.ts +3 -0
  176. package/src/lib/usage/ai-allotment-alerts.js +20 -8
  177. package/src/lib/usage/ai-allotment-alerts.js.map +1 -1
  178. package/src/lib/usage/assist-free-taste.js +2 -1
  179. package/src/lib/usage/assist-free-taste.js.map +1 -1
  180. package/src/lib/usage/assist-signal-mining.js +1 -1
  181. package/src/lib/usage/assist-signal-mining.js.map +1 -1
  182. package/src/lib/usage-axes.d.ts +39 -0
  183. package/src/lib/usage-axes.js +70 -0
  184. package/src/lib/usage-axes.js.map +1 -0
  185. package/src/lib/runtime/seo-page-facts.d.ts +0 -61
  186. package/src/lib/runtime/seo-page-facts.js +0 -150
  187. package/src/lib/runtime/seo-page-facts.js.map +0 -1
@@ -17,11 +17,12 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
17
17
  */ import { sanitizeAuthorHtml } from "@aglyn/aglyn/app-utils/author-html";
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
- import { ESTIMATED_PAGE_TRANSFER_BYTES, FREE_AI_TASTE_CREDITS_PER_MONTH } from "@aglyn/aglyn/app-utils/plan-entitlements";
20
+ import { ESTIMATED_PAGE_TRANSFER_BYTES } from "@aglyn/aglyn/app-utils/plan-entitlements";
21
+ import { FREE_AI_TASTE_CREDITS_PER_MONTH } from "../plan-entitlements.js";
21
22
  import { REUSABLE_INSTANCE_COMPONENT_ID, REUSABLE_INSTANCE_PROP_VALUES_KEY } from "@aglyn/aglyn/app-utils/reusable-component-keys";
22
23
  import { parseBreakpointSpan } from "@aglyn/shared-data-enums/breakpoint-span";
23
24
  import { renderEmailHtml } from "@aglyn/shared-util-email/email-render";
24
- import { aiPlanCreateFor, aiPlanUndeclaredRefs, isAiPlanNewRef } from "../model/ai-build-plan.js";
25
+ 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";
25
26
  import { aiCreationNoun, aiPlanUncreatable, aiPlanUncreatableKind } from "../model/ai-plan-capabilities.js";
26
27
  import { AI_INSTANCE_REF_PROP, validateAiNodeTree } from "./ai-node-tree.js";
27
28
  import { AI_PAGE_SCROLL_TO_PROP, aiPageLinkTarget, aiStoredScrollTargets } from "./ai-page-links.js";
@@ -1421,7 +1422,7 @@ const GRID = 'muiGrid';
1421
1422
  {
1422
1423
  rule: 10,
1423
1424
  code: 'link-unrelated-screen',
1424
- message: `"${sent[0].label}" links the home page, which does not do what its words say. Link the screen that does, or leave the link out when the site has none.`,
1425
+ message: `"${sent[0].label}" links the home page, which does not do what its words say. Link the page that does, or leave the link out when the site has none.`,
1425
1426
  nodeIds: unique(sent.map((entry)=>entry.id))
1426
1427
  }
1427
1428
  ];
@@ -1505,7 +1506,7 @@ const GRID = 'muiGrid';
1505
1506
  violations.push({
1506
1507
  rule: 10,
1507
1508
  code: 'link-fragment',
1508
- message: `${wordsOf(first)} links "${first.value}", an anchor, and no element on a page carries an id an anchor could name, so it goes nowhere. ${context.pageSections ? `${toSection} Otherwise give it the "screenId" of a screen the site has that does what its words say, or take it out.` : 'Give it the "screenId" of a screen the site has that does what its words say, or an "href" that is a path on this site or an https: address the brief gives, or take it out.'}`,
1509
+ message: `${wordsOf(first)} links "${first.value}", an anchor, and no element on a page carries an id an anchor could name, so it goes nowhere. ${context.pageSections ? `${toSection} Otherwise give it the "screenId" of a page the site has that does what its words say, or take it out.` : 'Give it the "screenId" of a page the site has that does what its words say, or an "href" that is a path on this site or an https: address the brief gives, or take it out.'}`,
1509
1510
  nodeIds: unique(fragments.map((entry)=>entry.id))
1510
1511
  });
1511
1512
  }
@@ -1524,7 +1525,7 @@ const GRID = 'muiGrid';
1524
1525
  violations.push({
1525
1526
  rule: 10,
1526
1527
  code: 'link-without-destination',
1527
- message: `${wordsOf(first)} goes nowhere. Give it the "screenId" of a screen the site has that does what its words say, or an "href" that is a path on this site or an https: address the brief gives.${instead}`,
1528
+ message: `${wordsOf(first)} goes nowhere. Give it the "screenId" of a page the site has that does what its words say, or an "href" that is a path on this site or an https: address the brief gives.${instead}`,
1528
1529
  nodeIds: unique(dead.map((entry)=>entry.id))
1529
1530
  });
1530
1531
  }
@@ -1880,17 +1881,29 @@ const PURE_CONTAINERS = new Set([
1880
1881
  ]);
1881
1882
  const EMBED_COMPONENTS = new Set([
1882
1883
  'videoEmbed',
1883
- 'custom-html',
1884
- 'functionWidget'
1884
+ 'custom-html'
1885
1885
  ]);
1886
1886
  const DUPLICATE_SX_MIN_KEYS = 2;
1887
1887
  const DUPLICATE_SX_MIN_NODES = 4;
1888
+ /**
1889
+ * Whether a third-party player is one the confirmed plan lists (AGL-3433). A
1890
+ * Video embed is admitted where the plan lists a player, playing the video
1891
+ * the brief gave or none yet, which the site owner pastes. Raw HTML never is.
1892
+ */ function embedVerdict(node, planned) {
1893
+ var _node_props;
1894
+ if (node.componentId !== 'videoEmbed' || !planned.length) return 'unplanned';
1895
+ const url = typeof ((_node_props = node.props) == null ? void 0 : _node_props['url']) === 'string' ? node.props['url'].trim() : '';
1896
+ if (!url) return 'planned';
1897
+ const key = aiEmbedVideoKey(url);
1898
+ return key && planned.some((embed)=>embed.url && aiEmbedVideoKey(embed.url) === key) ? 'planned' : 'unplanned-link';
1899
+ }
1888
1900
  /**
1889
1901
  * Rule 16. The flattest tree that renders the design: no container that
1890
1902
  * wraps one other container and adds nothing, no empty containers, no inline
1891
1903
  * style repeated where a token or a component prop would carry it, no font
1892
1904
  * beyond the theme's, images lazy below the first, video by poster rather
1893
- * than autoplay, and no third-party embed the plan did not name.
1905
+ * than autoplay, no third-party player the confirmed plan does not list, and
1906
+ * none playing a video the brief did not give.
1894
1907
  */ export function detectHeavyDocument(tree, outputKind, context = {}) {
1895
1908
  const visits = walkTree(tree);
1896
1909
  const wrappers = [];
@@ -1899,6 +1912,7 @@ const DUPLICATE_SX_MIN_NODES = 4;
1899
1912
  const eager = [];
1900
1913
  const video = [];
1901
1914
  const embeds = [];
1915
+ const embedLinks = [];
1902
1916
  const sxSeen = new Map();
1903
1917
  let images = 0;
1904
1918
  for (const { id, node } of visits){
@@ -1921,7 +1935,12 @@ const DUPLICATE_SX_MIN_NODES = 4;
1921
1935
  if (node.componentId === 'video' && (((_node_props2 = node.props) == null ? void 0 : _node_props2['autoPlay']) === true || ((_node_props3 = node.props) == null ? void 0 : _node_props3['preload']) === 'auto' || typeof ((_node_props4 = node.props) == null ? void 0 : _node_props4['src']) === 'string' && node.props['src'] && !((_node_props5 = node.props) == null ? void 0 : _node_props5['poster']))) {
1922
1936
  video.push(id);
1923
1937
  }
1924
- if (EMBED_COMPONENTS.has(node.componentId) && !context.allowEmbeds) embeds.push(id);
1938
+ if (EMBED_COMPONENTS.has(node.componentId)) {
1939
+ var _context_plannedEmbeds;
1940
+ const verdict = embedVerdict(node, (_context_plannedEmbeds = context.plannedEmbeds) != null ? _context_plannedEmbeds : []);
1941
+ if (verdict === 'unplanned') embeds.push(id);
1942
+ if (verdict === 'unplanned-link') embedLinks.push(id);
1943
+ }
1925
1944
  const sx = (_node_sx1 = node.sx) != null ? _node_sx1 : {};
1926
1945
  if (Object.keys(sx).length >= DUPLICATE_SX_MIN_KEYS) {
1927
1946
  var _sxSeen_get;
@@ -1953,7 +1972,8 @@ const DUPLICATE_SX_MIN_NODES = 4;
1953
1972
  add('extra-font', fonts, "This names a font the theme does not load. Use the theme's typography.");
1954
1973
  add('eager-image', eager, 'An image below the first loads eagerly. Leave loading unset so images further down the page load when they are reached.');
1955
1974
  add('autoplay-video', video, 'A video loads its file before anyone plays it. Give it a poster, and let it play on click.');
1956
- add('third-party-embed', embeds, 'This embeds a third-party player or script, which loads its own code on every visit. Use a Video from the media library, or name the embed and its cost in the plan.');
1975
+ add('third-party-embed', embeds, "This embeds a third-party player, which loads its host's code on every visit, and the confirmed plan lists none here. Remove it; a film from the media library plays in a Video (video).");
1976
+ add('embed-link-unplanned', embedLinks, 'This player plays a video the brief did not give. Use the link the plan lists, or leave the link empty for the site owner to paste.');
1957
1977
  return violations;
1958
1978
  }
1959
1979
  /** The items a list or a row of cards repeats, as a re-ask names them. */ const ITEM_NOUNS = {
@@ -2514,7 +2534,7 @@ const FORM_SECTION_NAME = /\bform\b|\bsign[- ]?up\b|\bsubscribe\b|\bnewsletter\b
2514
2534
  violations.push({
2515
2535
  rule: 1,
2516
2536
  code: 'plan-section-across-screens',
2517
- message: `The "${entries[0].section.name}" section is planned on ${screens.size} screens. Plan it as one reusable component and place it on each.`,
2537
+ message: `The "${entries[0].section.name}" section is planned on ${screens.size} pages. Plan it as one reusable component and place it on each.`,
2518
2538
  paths: entries.map((entry)=>entry.path)
2519
2539
  });
2520
2540
  }
@@ -2639,7 +2659,7 @@ const FORM_SECTION_NAME = /\bform\b|\bsign[- ]?up\b|\bsubscribe\b|\bnewsletter\b
2639
2659
  violations.push({
2640
2660
  rule: 2,
2641
2661
  code: 'plan-screen-without-layout',
2642
- message: noLayoutToName ? 'A screen names a layout the site does not have, and this job may not create one. Leave the layout empty.' : mayCreateLayout ? "A screen names no layout the site has or the plan creates. Put every screen in the site's layout, or plan one." : 'A screen names no layout the site has, and this job may not create one. Put every screen in a layout the site has.',
2662
+ message: noLayoutToName ? 'A page names a layout the site does not have, and this job may not create one. Leave the layout empty.' : mayCreateLayout ? "A page names no layout the site has or the plan creates. Put every page in the site's layout, or plan one." : 'A page names no layout the site has, and this job may not create one. Put every page in a layout the site has.',
2643
2663
  paths: unlaid.map(({ index })=>`screens[${index}].layout`)
2644
2664
  });
2645
2665
  }
@@ -2648,7 +2668,7 @@ const FORM_SECTION_NAME = /\bform\b|\bsign[- ]?up\b|\bsubscribe\b|\bnewsletter\b
2648
2668
  violations.push({
2649
2669
  rule: 2,
2650
2670
  code: 'plan-layout-region-section',
2651
- message: 'A screen plans its own header, navigation or footer. Those live in the layout; take the section off the screen.',
2671
+ message: 'A page plans its own header, navigation or footer. Those live in the layout; take the section off the page.',
2652
2672
  paths: regions.map((entry)=>entry.path)
2653
2673
  });
2654
2674
  }
@@ -2657,7 +2677,7 @@ const FORM_SECTION_NAME = /\bform\b|\bsign[- ]?up\b|\bsubscribe\b|\bnewsletter\b
2657
2677
  violations.push({
2658
2678
  rule: 2,
2659
2679
  code: 'plan-layout-in-section',
2660
- message: "A section places a layout. A layout frames a whole screen and is never placed inside one: name it as the screen's layout, and take it out of the section's uses.",
2680
+ message: "A section places a layout. A layout frames a whole page and is never placed inside one: name it as the page's layout, and take it out of the section's uses.",
2661
2681
  paths: placed
2662
2682
  });
2663
2683
  }
@@ -2704,7 +2724,7 @@ const FORM_SECTION_NAME = /\bform\b|\bsign[- ]?up\b|\bsubscribe\b|\bnewsletter\b
2704
2724
  violations.push({
2705
2725
  rule: 4,
2706
2726
  code: 'plan-similar-pages',
2707
- message: `${indexes.length} screens share one shape. Plan one template and apply it ${indexes.length} times with each page's copy, or bind it to a collection.`,
2727
+ message: `${indexes.length} pages share one shape. Plan one template and apply it ${indexes.length} times with each page's copy, or bind it to a collection.`,
2708
2728
  paths: indexes.map((index)=>`screens[${index}].template`)
2709
2729
  });
2710
2730
  }
@@ -2713,7 +2733,7 @@ const FORM_SECTION_NAME = /\bform\b|\bsign[- ]?up\b|\bsubscribe\b|\bnewsletter\b
2713
2733
  violations.push({
2714
2734
  rule: 4,
2715
2735
  code: 'plan-template-in-section',
2716
- message: "A section places a template. A template is applied to a whole screen and is never placed inside one: take it out of the section's uses, and name it as the screen's template only when the whole screen is built from it.",
2736
+ message: "A section places a template. A template is applied to a whole page and is never placed inside one: take it out of the section's uses, and name it as the page's template only when the whole page is built from it.",
2717
2737
  paths: placed
2718
2738
  });
2719
2739
  }
@@ -2852,15 +2872,15 @@ const REUSE_BEFORE_CREATE_KINDS = new Set([
2852
2872
  const section = first.field === 'uses' ? screen.sections[first.sectionIndex] : null;
2853
2873
  const kind = section ? undeclaredPlacementKind(first.name, section, plan, kinds) : 'template';
2854
2874
  const refused = capabilities ? aiPlanUncreatableKind(plan, kind, capabilities) : null;
2855
- const where = section ? `${section.name ? `The "${section.name}" section` : 'A section'} places a creation named "${first.name}", but the plan never creates it` : `${screen.title ? `The screen "${screen.title}"` : 'A screen'} applies a template named "${first.name}", but the plan never creates it`;
2875
+ const where = section ? `${section.name ? `The "${section.name}" section` : 'A section'} places a creation named "${first.name}", but the plan never creates it` : `${screen.title ? `The page "${screen.title}"` : 'A page'} applies a template named "${first.name}", but the plan never creates it`;
2856
2876
  let message;
2857
2877
  if (!refused) {
2858
- const otherwise = section ? `place ${aiCreationNoun(kind)} the site already has by its id` : "leave the screen's template empty";
2878
+ const otherwise = section ? `place ${aiCreationNoun(kind)} the site already has by its id` : "leave the page's template empty";
2859
2879
  message = `${where}. Declare it in create as ${aiCreationNoun(kind)}, with why nothing the site has will do, or ${otherwise}.`;
2860
2880
  } else if (section) {
2861
2881
  message = `${where}, and ${refused.reason}. ${refused.instead} Take it out of the section's uses.`;
2862
2882
  } else {
2863
- message = `${where}, and ${refused.reason}. Leave the screen's template empty, or apply a template the site already has.`;
2883
+ message = `${where}, and ${refused.reason}. Leave the page's template empty, or apply a template the site already has.`;
2864
2884
  }
2865
2885
  return {
2866
2886
  rule: 7,
@@ -2952,7 +2972,7 @@ function slugKey(slug) {
2952
2972
  violations.push({
2953
2973
  rule: 10,
2954
2974
  code: 'plan-slug',
2955
- message: 'A screen has no usable address. Give each one a slug of lowercase words joined by hyphens.',
2975
+ message: 'A page has no usable address. Give each one a slug of lowercase words joined by hyphens.',
2956
2976
  paths: badSlugs
2957
2977
  });
2958
2978
  }
@@ -2960,7 +2980,7 @@ function slugKey(slug) {
2960
2980
  violations.push({
2961
2981
  rule: 10,
2962
2982
  code: 'plan-slug-taken',
2963
- message: 'A screen reuses an address the site or the plan already uses. Give each screen its own slug.',
2983
+ message: 'A page reuses an address the site or the plan already uses. Give each page its own slug.',
2964
2984
  paths: collisions
2965
2985
  });
2966
2986
  }
@@ -2968,7 +2988,7 @@ function slugKey(slug) {
2968
2988
  violations.push({
2969
2989
  rule: 10,
2970
2990
  code: 'plan-seo',
2971
- message: `A screen is missing its search title or description, or runs past ${AI_SEO_TITLE_MAX} and ${AI_SEO_DESCRIPTION_MAX} characters. Write both for every screen.`,
2991
+ message: `A page is missing its search title or description, or runs past ${AI_SEO_TITLE_MAX} and ${AI_SEO_DESCRIPTION_MAX} characters. Write both for every page.`,
2972
2992
  paths: seo
2973
2993
  });
2974
2994
  }
@@ -3015,7 +3035,7 @@ function slugKey(slug) {
3015
3035
  violations.push({
3016
3036
  rule: 15,
3017
3037
  code: 'plan-missed-duplicate',
3018
- message: `The site already has "${nearest.name}". Start the new screen from a duplicate of it, which keeps its bindings and SEO, and edit that.`,
3038
+ message: `The site already has "${nearest.name}". Start the new page from a duplicate of it, which keeps its bindings and SEO, and edit that.`,
3019
3039
  paths: [
3020
3040
  `screens[${index}].duplicateOf`
3021
3041
  ]
@@ -3156,6 +3176,141 @@ function slugKey(slug) {
3156
3176
  }
3157
3177
  ];
3158
3178
  }
3179
+ /**
3180
+ * Rule 16 (plan): a third-party player is planned where it can play
3181
+ * (AGL-3433). A screen of the plan or a component it creates carries at most
3182
+ * its kind's budget. A template renders for every record it serves and a
3183
+ * layout around every page, so neither carries one, and a form, an email or a
3184
+ * dataset has nowhere to. A link the plan gives is one its host plays.
3185
+ */ export function detectPlanEmbeds(plan) {
3186
+ var _plan_embeds;
3187
+ const nowhere = [];
3188
+ const elsewhere = [];
3189
+ const wrongHost = [];
3190
+ const placed = new Map();
3191
+ ((_plan_embeds = plan.embeds) != null ? _plan_embeds : []).forEach((embed, index)=>{
3192
+ var _placed_get;
3193
+ const path = `embeds[${index}]`;
3194
+ if (embed.url !== null && aiEmbedHostOf(embed.url) !== embed.host) wrongHost.push(`${path}.url`);
3195
+ let target = null;
3196
+ if (isAiPlanNewRef(embed.where)) {
3197
+ const creation = aiPlanCreateFor(plan, embed.where);
3198
+ if (creation && creation.kind !== 'component') {
3199
+ elsewhere.push({
3200
+ kind: creation.kind,
3201
+ path: `${path}.where`
3202
+ });
3203
+ return;
3204
+ }
3205
+ if (creation) target = {
3206
+ key: embed.where.trim().toLowerCase(),
3207
+ label: creation.name,
3208
+ kind: 'component'
3209
+ };
3210
+ } else {
3211
+ const screen = plan.screens.find((row)=>aiPlanSlugKey(row.slug) === aiPlanSlugKey(embed.where));
3212
+ if (screen) target = {
3213
+ key: aiPlanSlugKey(screen.slug),
3214
+ label: screen.slug,
3215
+ kind: 'page'
3216
+ };
3217
+ }
3218
+ if (!target) {
3219
+ nowhere.push(`${path}.where`);
3220
+ return;
3221
+ }
3222
+ const entry = (_placed_get = placed.get(target.key)) != null ? _placed_get : {
3223
+ label: target.label,
3224
+ kind: target.kind,
3225
+ paths: []
3226
+ };
3227
+ entry.paths.push(path);
3228
+ placed.set(target.key, entry);
3229
+ });
3230
+ const violations = [];
3231
+ if (nowhere.length) {
3232
+ violations.push({
3233
+ rule: 16,
3234
+ code: 'plan-embed-nowhere',
3235
+ message: 'A player is planned where the plan builds nothing. Name a screen of the plan by its slug, or new:<name> of a component it creates.',
3236
+ paths: nowhere
3237
+ });
3238
+ }
3239
+ if (elsewhere.length) {
3240
+ const nouns = [
3241
+ ...new Set(elsewhere.map((entry)=>AI_BUILD_PLAN_CREATION_NOUNS[entry.kind].noun))
3242
+ ];
3243
+ violations.push({
3244
+ rule: 16,
3245
+ code: 'plan-embed-refused',
3246
+ message: `Only a page or a component carries a third-party player, and this plan puts one on a ${nouns.join(' and a ')}. A template or a layout would load the player's code on every page it serves. Place it on a page or a component, or leave it off.`,
3247
+ paths: elsewhere.map((entry)=>entry.path)
3248
+ });
3249
+ }
3250
+ for (const entry of placed.values()){
3251
+ const limit = AI_OUTPUT_BUDGETS[entry.kind].embeds;
3252
+ if (entry.paths.length <= limit) continue;
3253
+ violations.push({
3254
+ rule: 16,
3255
+ code: 'plan-embed-over-budget',
3256
+ message: `${entry.label} is planned ${entry.paths.length} players, and a ${entry.kind} carries at most ${limit}. Keep the one the brief asks for most.`,
3257
+ paths: entry.paths
3258
+ });
3259
+ }
3260
+ if (wrongHost.length) {
3261
+ violations.push({
3262
+ rule: 16,
3263
+ code: 'plan-embed-link-host',
3264
+ message: `A planned player's link is not a video link of its host, ${Object.values(AI_BUILD_PLAN_EMBED_HOST_NAMES).join(' or ')}. Give the brief's own link for that host, or null.`,
3265
+ paths: wrongHost
3266
+ });
3267
+ }
3268
+ return violations;
3269
+ }
3270
+ /** Text as two copies of one phrase compare: lowercase, no quotes, one space. */ function phraseKey(text) {
3271
+ return text.toLowerCase().replace(/[“”"‘’'`]/g, '').replace(/\s+/g, ' ').trim();
3272
+ }
3273
+ /**
3274
+ * Rule 16 (plan, against the brief): a player is planned only where the brief
3275
+ * asks for one (AGL-3433). The plan quotes the brief's words, and those words
3276
+ * are found in the brief and ask for a video; a link it gives is one the brief
3277
+ * gives. The plan step holds this, being the one door that reads the brief
3278
+ * beside the plan.
3279
+ */ export function aiPlanEmbedBriefViolations(plan, brief) {
3280
+ var _brief_match, _plan_embeds;
3281
+ const said = phraseKey(brief);
3282
+ const given = new Set(((_brief_match = brief.match(/https:\/\/\S+/g)) != null ? _brief_match : []).map((link)=>aiEmbedVideoKey(link.replace(/[).,;!?]+$/, ''))).filter((key)=>key !== null));
3283
+ const unasked = [];
3284
+ const ungiven = [];
3285
+ ((_plan_embeds = plan.embeds) != null ? _plan_embeds : []).forEach((embed, index)=>{
3286
+ const asked = phraseKey(embed.asked);
3287
+ if (!asked || !said.includes(asked) || !AI_PLAN_ASKS_FOR_VIDEO.test(asked)) {
3288
+ unasked.push(`embeds[${index}].asked`);
3289
+ }
3290
+ if (embed.url !== null) {
3291
+ const key = aiEmbedVideoKey(embed.url);
3292
+ if (!key || !given.has(key)) ungiven.push(`embeds[${index}].url`);
3293
+ }
3294
+ });
3295
+ const violations = [];
3296
+ if (unasked.length) {
3297
+ violations.push({
3298
+ rule: 16,
3299
+ code: 'plan-embed-not-asked',
3300
+ message: 'A player is planned that the brief does not ask for: its words are not words of the brief asking for a video. Take it off the plan; a film from the media library plays in a Video.',
3301
+ paths: unasked
3302
+ });
3303
+ }
3304
+ if (ungiven.length) {
3305
+ violations.push({
3306
+ rule: 16,
3307
+ code: 'plan-embed-link-not-given',
3308
+ message: "A planned player's link is not one the brief gives. Give the brief's own link, or null for the site owner to paste.",
3309
+ paths: ungiven
3310
+ });
3311
+ }
3312
+ return violations;
3313
+ }
3159
3314
  /**
3160
3315
  * Every plan rule, against the site inventory the plan was made from and,
3161
3316
  * where the job read them, what it may create there (AGL-3030). `null`
@@ -3170,6 +3325,7 @@ function slugKey(slug) {
3170
3325
  ...detectPlanInlineForms(plan, inventory, capabilities),
3171
3326
  ...detectUntemplatedSimilarPages(plan, inventory),
3172
3327
  ...detectPlanLiteralColors(plan),
3328
+ ...detectPlanEmbeds(plan),
3173
3329
  ...detectCreateBeforeReuse(plan, inventory),
3174
3330
  ...detectPlanUncreatable(plan, capabilities),
3175
3331
  ...detectPlanUndeclaredCreations(plan, inventory, capabilities),