@aglyn/plugins-ai 1.0.0-beta.243 → 1.0.0-beta.246

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 (123) hide show
  1. package/package.json +17 -17
  2. package/src/lib/activity/ai-activity-actions.js +3 -1
  3. package/src/lib/activity/ai-activity-actions.js.map +1 -1
  4. package/src/lib/components/ai-job-links.js +7 -0
  5. package/src/lib/components/ai-job-links.js.map +1 -1
  6. package/src/lib/components/ai-product-zones.d.ts +3 -1
  7. package/src/lib/components/ai-product-zones.js.map +1 -1
  8. package/src/lib/components/ai-products-hub-card.component.js +1 -1
  9. package/src/lib/components/ai-products-hub-card.component.js.map +1 -1
  10. package/src/lib/components/ai-products-proposals.component.js +17 -9
  11. package/src/lib/components/ai-products-proposals.component.js.map +1 -1
  12. package/src/lib/components/ai-site-start-card.component.js +6 -4
  13. package/src/lib/components/ai-site-start-card.component.js.map +1 -1
  14. package/src/lib/components/ai-upsell-dialog.component.js +1 -1
  15. package/src/lib/components/ai-upsell-dialog.component.js.map +1 -1
  16. package/src/lib/jobs/ai-build-capabilities.js +162 -2
  17. package/src/lib/jobs/ai-build-capabilities.js.map +1 -1
  18. package/src/lib/jobs/ai-job-build-step.d.ts +7 -0
  19. package/src/lib/jobs/ai-job-build-step.js +85 -16
  20. package/src/lib/jobs/ai-job-build-step.js.map +1 -1
  21. package/src/lib/jobs/ai-job-duplicate-step.d.ts +123 -0
  22. package/src/lib/jobs/ai-job-duplicate-step.js +269 -0
  23. package/src/lib/jobs/ai-job-duplicate-step.js.map +1 -0
  24. package/src/lib/jobs/ai-job-layout-language.js +24 -6
  25. package/src/lib/jobs/ai-job-layout-language.js.map +1 -1
  26. package/src/lib/jobs/ai-job-navigation-step.d.ts +157 -0
  27. package/src/lib/jobs/ai-job-navigation-step.js +439 -0
  28. package/src/lib/jobs/ai-job-navigation-step.js.map +1 -0
  29. package/src/lib/jobs/ai-job-page-language.d.ts +5 -2
  30. package/src/lib/jobs/ai-job-page-language.js +38 -10
  31. package/src/lib/jobs/ai-job-page-language.js.map +1 -1
  32. package/src/lib/jobs/ai-job-page-sections.js +1 -1
  33. package/src/lib/jobs/ai-job-page-sections.js.map +1 -1
  34. package/src/lib/jobs/ai-job-page-step.js +11 -4
  35. package/src/lib/jobs/ai-job-page-step.js.map +1 -1
  36. package/src/lib/jobs/ai-job-plan-step.d.ts +4 -2
  37. package/src/lib/jobs/ai-job-plan-step.js +16 -7
  38. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  39. package/src/lib/jobs/ai-job-products-step.d.ts +2 -2
  40. package/src/lib/jobs/ai-job-products-step.js +2 -2
  41. package/src/lib/jobs/ai-job-products-step.js.map +1 -1
  42. package/src/lib/jobs/ai-job-settings-step.d.ts +272 -0
  43. package/src/lib/jobs/ai-job-settings-step.js +695 -0
  44. package/src/lib/jobs/ai-job-settings-step.js.map +1 -0
  45. package/src/lib/jobs/ai-job-site-content.d.ts +20 -6
  46. package/src/lib/jobs/ai-job-site-content.js +28 -12
  47. package/src/lib/jobs/ai-job-site-content.js.map +1 -1
  48. package/src/lib/jobs/ai-job-site-datasets.d.ts +2 -0
  49. package/src/lib/jobs/ai-job-site-datasets.js +10 -3
  50. package/src/lib/jobs/ai-job-site-datasets.js.map +1 -1
  51. package/src/lib/jobs/ai-job-site-step.d.ts +3 -1
  52. package/src/lib/jobs/ai-job-site-step.js +86 -12
  53. package/src/lib/jobs/ai-job-site-step.js.map +1 -1
  54. package/src/lib/jobs/ai-site-publish.d.ts +76 -4
  55. package/src/lib/jobs/ai-site-publish.js +231 -59
  56. package/src/lib/jobs/ai-site-publish.js.map +1 -1
  57. package/src/lib/layout-language/ai-layout-compiler.d.ts +7 -4
  58. package/src/lib/layout-language/ai-layout-compiler.js +26 -25
  59. package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
  60. package/src/lib/layout-language/ai-layout-frame-hollows.d.ts +41 -0
  61. package/src/lib/layout-language/ai-layout-frame-hollows.js +330 -0
  62. package/src/lib/layout-language/ai-layout-frame-hollows.js.map +1 -0
  63. package/src/lib/layout-language/ai-layout-frame.d.ts +18 -1
  64. package/src/lib/layout-language/ai-layout-frame.js +83 -13
  65. package/src/lib/layout-language/ai-layout-frame.js.map +1 -1
  66. package/src/lib/layout-language/ai-layout-links.js +5 -2
  67. package/src/lib/layout-language/ai-layout-links.js.map +1 -1
  68. package/src/lib/layout-language/ai-layout-listings.d.ts +35 -0
  69. package/src/lib/layout-language/ai-layout-listings.js +34 -0
  70. package/src/lib/layout-language/ai-layout-listings.js.map +1 -1
  71. package/src/lib/layout-language/ai-layout-tree.d.ts +2 -0
  72. package/src/lib/layout-language/ai-layout-tree.js +17 -0
  73. package/src/lib/layout-language/ai-layout-tree.js.map +1 -1
  74. package/src/lib/model/ai-build-job.d.ts +23 -0
  75. package/src/lib/model/ai-build-job.js +102 -13
  76. package/src/lib/model/ai-build-job.js.map +1 -1
  77. package/src/lib/model/ai-build-plan.d.ts +25 -0
  78. package/src/lib/model/ai-build-plan.js +45 -1
  79. package/src/lib/model/ai-build-plan.js.map +1 -1
  80. package/src/lib/model/ai-credit-estimate.d.ts +52 -0
  81. package/src/lib/model/ai-credit-estimate.js +51 -0
  82. package/src/lib/model/ai-credit-estimate.js.map +1 -1
  83. package/src/lib/model/ai-job-activity.js +4 -1
  84. package/src/lib/model/ai-job-activity.js.map +1 -1
  85. package/src/lib/model/ai-jobs.types.d.ts +10 -2
  86. package/src/lib/model/ai-jobs.types.js +4 -1
  87. package/src/lib/model/ai-jobs.types.js.map +1 -1
  88. package/src/lib/model/ai-layout-menus.d.ts +104 -0
  89. package/src/lib/model/ai-layout-menus.js +518 -0
  90. package/src/lib/model/ai-layout-menus.js.map +1 -0
  91. package/src/lib/model/ai-products.d.ts +31 -1
  92. package/src/lib/model/ai-products.js +34 -1
  93. package/src/lib/model/ai-products.js.map +1 -1
  94. package/src/lib/model/ai-site-inventory.d.ts +15 -0
  95. package/src/lib/model/ai-site-inventory.js.map +1 -1
  96. package/src/lib/model/ai-site-job.d.ts +10 -0
  97. package/src/lib/model/ai-site-job.js +36 -9
  98. package/src/lib/model/ai-site-job.js.map +1 -1
  99. package/src/lib/model/ai-site-store-pages.js +1 -1
  100. package/src/lib/model/ai-site-store-pages.js.map +1 -1
  101. package/src/lib/runtime/ai-doctrine-validators.d.ts +95 -1
  102. package/src/lib/runtime/ai-doctrine-validators.js +264 -36
  103. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  104. package/src/lib/runtime/ai-doctrine.js +7 -2
  105. package/src/lib/runtime/ai-doctrine.js.map +1 -1
  106. package/src/lib/runtime/ai-palette.generated.js +1 -1
  107. package/src/lib/runtime/ai-palette.generated.js.map +1 -1
  108. package/src/lib/runtime/ai-products-generation.d.ts +4 -2
  109. package/src/lib/runtime/ai-products-generation.js +12 -6
  110. package/src/lib/runtime/ai-products-generation.js.map +1 -1
  111. package/src/lib/runtime/site-inventory.d.ts +12 -0
  112. package/src/lib/runtime/site-inventory.js +29 -1
  113. package/src/lib/runtime/site-inventory.js.map +1 -1
  114. package/src/lib/server.js +12 -0
  115. package/src/lib/server.js.map +1 -1
  116. package/src/lib/testing/stand-in-dataset-index.js +37 -3
  117. package/src/lib/testing/stand-in-dataset-index.js.map +1 -1
  118. package/src/lib/tools/ai-products-tool.d.ts +3 -1
  119. package/src/lib/tools/ai-products-tool.js +15 -5
  120. package/src/lib/tools/ai-products-tool.js.map +1 -1
  121. package/src/lib/usage/ai-quota-checks.d.ts +13 -3
  122. package/src/lib/usage/ai-quota-checks.js +46 -8
  123. package/src/lib/usage/ai-quota-checks.js.map +1 -1
@@ -130,6 +130,14 @@ export interface AiDoctrineTreeContext extends AiNodeTreeContext {
130
130
  * to when the site has none for what it promises. Absent: none is known.
131
131
  */
132
132
  homeScreenIds?: readonly string[];
133
+ /**
134
+ * The site's pages that place its form, by id (AGL-3729): where a link whose
135
+ * words ask a visitor to get in touch (`aiAsksToGetInTouch`) does what it
136
+ * says, even when that page is home. A guided start's café put its
137
+ * pre-order form on Home, and its Menu page's "Request weekend pickup"
138
+ * links there. Absent: none is known, and a link home is held to its words.
139
+ */
140
+ formScreenIds?: readonly string[];
133
141
  /**
134
142
  * The node as the model wrote it, by the id the checked tree gives it
135
143
  * (AGL-3078), for a tree that was stored before it is checked: a page
@@ -472,6 +480,39 @@ export declare function isAiLinkElement(componentId: unknown): boolean;
472
480
  * a re-ask to inherit the band's contrast text or to set it.
473
481
  */
474
482
  export declare function detectInvisibleLinks(tree: AiDoctrineTree, outputKind: AiOutputKind): AiDoctrineViolation[];
483
+ /**
484
+ * Whether a link's words ask a visitor to get in touch — contact, book, quote,
485
+ * request, order, pre-order, enquire — which the site's form answers
486
+ * (AGL-3729). The layout language sends such a link to the form.
487
+ */
488
+ export declare function aiAsksToGetInTouch(label: string): boolean;
489
+ /**
490
+ * RULE 10'S LINK SENT HOME, REPAIRED IN CODE (AGL-3729).
491
+ *
492
+ * The rule's own advice is "link the page that does, or leave the link out",
493
+ * and both halves are something code can do once it knows where the site's
494
+ * form is. A Free guided start's café (beta.243, 2026-10-12) put its contact
495
+ * and pre-order form on Home; its Menu page's "Request weekend pickup" was
496
+ * sent to Home, and the page was refused on every answer, since the
497
+ * destination was the compiler's, not the model's. So, before the check reads
498
+ * the tree, each link `detectUnrelatedScreenLinks` would refuse is:
499
+ *
500
+ * - repointed to the page that places the site's form, when its words ask a
501
+ * visitor to get in touch and the site has such a page that is not the page
502
+ * being checked (`formScreenIds`, `ownScreenIds`). The platform reaches a
503
+ * section of another page by no anchor (AGL-2867), so the link names the
504
+ * page; a link to a section of its own page is the Scroll to element
505
+ * interaction, which this never writes;
506
+ * - otherwise left out: the node and everything under it are taken off the
507
+ * page, and out of its parent's children.
508
+ *
509
+ * The root is never taken out. A tree with nothing to repair comes back as
510
+ * the same object; a node that is changed is copied, never edited in place.
511
+ */
512
+ export declare function aiSettleUnrelatedScreenLinks<T extends AiDoctrineTree>(tree: T, outputKind: AiOutputKind, context?: Pick<AiDoctrineTreeContext, 'homeScreenIds' | 'formScreenIds'> & {
513
+ /** The page being checked, by every id it goes by: a link never goes to itself. */
514
+ ownScreenIds?: readonly string[];
515
+ }): T;
475
516
  /**
476
517
  * Rule 10, for a link sent where the site has nothing for it (AGL-3056). A
477
518
  * link whose purpose the site has no screen for is left out; it never goes to
@@ -483,7 +524,7 @@ export declare function detectInvisibleLinks(tree: AiDoctrineTree, outputKind: A
483
524
  * not say home is refused, with a re-ask to link the screen that does what it
484
525
  * says, or to leave the link out.
485
526
  */
486
- export declare function detectUnrelatedScreenLinks(tree: AiDoctrineTree, outputKind: AiOutputKind, context?: Pick<AiDoctrineTreeContext, 'homeScreenIds'>): AiDoctrineViolation[];
527
+ export declare function detectUnrelatedScreenLinks(tree: AiDoctrineTree, outputKind: AiOutputKind, context?: Pick<AiDoctrineTreeContext, 'homeScreenIds' | 'formScreenIds'>): AiDoctrineViolation[];
487
528
  /**
488
529
  * Rule 10, for a link that goes nowhere (AGL-3072). A Button or a Screen Link
489
530
  * goes where its `screenId` or its `href` says, and those are the only
@@ -849,6 +890,23 @@ export declare function aiSettlePlanFormDatasets(plan: AiBuildPlan, inventory: A
849
890
  * so the rule still asks.
850
891
  */
851
892
  export declare function aiSettlePlanLayouts(plan: AiBuildPlan, inventory: AiSiteInventory | null, capabilities?: AiPlanCapabilities | null): AiBuildPlan;
893
+ /**
894
+ * A page section that is a layout region — a header, navigation or footer,
895
+ * read by the name rule 2's `plan-layout-region-section` reads — is taken off
896
+ * the page before the rules read the plan (AGL-3750). The region is already
897
+ * the layout's, so the rule's own answer, "take the section off the page", is
898
+ * the only one: a live Free start of 2026-10-12 (a bakery that sells cakes to
899
+ * order) planned a Header and a Footer on its pages three times and stopped.
900
+ *
901
+ * Only a section that places nothing, or nothing but a layout (which rule 2
902
+ * refuses placing as well), is taken off: one that places a form or a
903
+ * component may be a band misnamed, so the rule still asks. A page whose
904
+ * every section is a region keeps them all, and is re-asked as before, since
905
+ * a page of no sections has no answer here. A home this leaves under its
906
+ * fewest sections, or a Free plan still past its wall, is the existing
907
+ * checks' to settle or refuse.
908
+ */
909
+ export declare function aiSettlePlanLayoutRegionSections(plan: AiBuildPlan, inventory: AiSiteInventory | null): AiBuildPlan;
852
910
  /** The contact form a plan is given when a section collects answers and the plan makes no form. */
853
911
  export declare const AI_PLAN_SETTLED_FORM: {
854
912
  readonly name: "Contact form";
@@ -986,6 +1044,42 @@ export declare function aiFreePageSectionsWithin(creations: {
986
1044
  layouts: number;
987
1045
  pages?: number;
988
1046
  }, credits?: Readonly<AiFreePageWorstCase>): number;
1047
+ /**
1048
+ * The most sections past the Free wall a plan is trimmed by rather than
1049
+ * re-asked (AGL-3737). The live candle-shop start of 2026-10-12 planned 9
1050
+ * sections against a wall of 8 on all three answers, each with a home of 7:
1051
+ * the model reaching for the budget the site redo names ("plan 7 to 8") and
1052
+ * landing one band past it, which no re-ask cured. Within two, the plan's
1053
+ * shape is right and only a band or two the brief needs least is extra, so
1054
+ * taking those from the end loses nothing the plan depends on. Further over,
1055
+ * the shape itself is off — a list planned as sections, a page the wall
1056
+ * cannot pay for — and cutting more would drop content the model meant, so
1057
+ * that plan is still re-asked.
1058
+ */
1059
+ export declare const AI_FREE_WALL_SETTLE_MAX_OVER = 2;
1060
+ /**
1061
+ * A Free plan a section or two past the wall is trimmed to fit before the
1062
+ * rules read the plan (AGL-3737), rather than re-asked: which sections go has
1063
+ * one answer, so the re-ask only costs the member a stopped start. The live
1064
+ * candle-shop start of 2026-10-12 asked for a home of 7 and a second page of
1065
+ * 2 against a wall of 8, three answers running, and stopped needing input.
1066
+ *
1067
+ * One section goes at a time, the last of its page that `freeWallKeeps`
1068
+ * does not hold, from the page furthest past its share: the home at / past
1069
+ * its fewest (`aiSiteHomeMinSections`, five on a site start whose wall pays
1070
+ * for a full home), any other page past two; on a tie, the longer page, then
1071
+ * the home. No home goes under its fewest and no page under one section. The
1072
+ * shares are what a site start is told to plan (a home of five or six, the
1073
+ * other page two or three): the candle shop's 7 + 2 becomes 6 + 2, never
1074
+ * 7 + 1, and the recorded yoga answer's 6 + 4 becomes 5 + 3, the shape its
1075
+ * own redo chose.
1076
+ *
1077
+ * A plan more than `AI_FREE_WALL_SETTLE_MAX_OVER` past the wall, a site
1078
+ * start past its page cap, a plan with a list split into sections, and a
1079
+ * plan the trim cannot fit without a section it keeps are returned
1080
+ * unchanged, for the wall to re-ask.
1081
+ */
1082
+ export declare function aiSettlePlanFreeWall(plan: AiBuildPlan, inventory: AiSiteInventory | null, capabilities?: AiPlanCapabilities | null): AiBuildPlan;
989
1083
  /**
990
1084
  * The Free wall (plan): a plan asks for no more sections than the Free taste
991
1085
  * pays for at its worst beside what the job builds first (AGL-3070). A live
@@ -26,6 +26,8 @@ import { renderEmailHtml } from "@aglyn/shared-util-email/email-render";
26
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
+ import { aiLayoutIsShopPage, aiLayoutShopGridSection } from "../layout-language/ai-layout-listings.js";
30
+ import { aiStorefrontSays } from "../layout-language/ai-layout-storefront.js";
29
31
  import { AI_INSTANCE_REF_PROP, validateAiNodeTree } from "./ai-node-tree.js";
30
32
  import { AI_PAGE_SCROLL_TO_PROP, aiPageLinkTarget, aiStoredScrollTargets } from "./ai-page-links.js";
31
33
  import { AI_EMAIL_CLIP_BYTES, AI_OUTPUT_BUDGETS, AI_OUTPUT_SURFACE, AI_TEXT_LIMITS, isHeadlineVariant } from "./ai-palette.js";
@@ -1841,20 +1843,23 @@ const GRID = 'muiGrid';
1841
1843
  ];
1842
1844
  }
1843
1845
  /** A link's words that say it goes home. */ const HOME_WORDS = /\bhome(?:\s?page)?\b/i;
1846
+ /** Words that ask a visitor to get in touch, which the site's form answers. */ const GET_IN_TOUCH_WORDS = /\b(?:contact|book|booking|call|quote|estimate|appointment|schedule|reserve|reservation|get in touch|request|visit|enquire|inquire|enquiry|inquiry|message|talk|consult|consultation|order|pre-?order|sign up|join|apply|start)\b/i;
1844
1847
  /**
1845
- * Rule 10, for a link sent where the site has nothing for it (AGL-3056). A
1846
- * link whose purpose the site has no screen for is left out; it never goes to
1847
- * a screen that does something else. The site inventory cannot say what each
1848
- * screen is for, but it can name the one a model falls back to: a live About
1849
- * page's footer sent "Request a Consultation" to the home screen, because the
1850
- * site had no consultation screen. So a Screen Link or a Button, outside the
1851
- * header and the navigation, that links the home screen with words that do
1852
- * not say home is refused, with a re-ask to link the screen that does what it
1853
- * says, or to leave the link out.
1854
- */ export function detectUnrelatedScreenLinks(tree, outputKind, context = {}) {
1855
- var _context_homeScreenIds;
1848
+ * Whether a link's words ask a visitor to get in touch — contact, book, quote,
1849
+ * request, order, pre-order, enquire — which the site's form answers
1850
+ * (AGL-3729). The layout language sends such a link to the form.
1851
+ */ export function aiAsksToGetInTouch(label) {
1852
+ return GET_IN_TOUCH_WORDS.test(label);
1853
+ }
1854
+ /**
1855
+ * The links rule 10 refuses as sent home for nothing (`detectUnrelatedScreenLinks`),
1856
+ * in document order. A link whose words ask a visitor to get in touch, sent to
1857
+ * a home page that places the site's form, does what it says (AGL-3729).
1858
+ */ function unrelatedScreenLinks(tree, outputKind, context) {
1859
+ var _context_homeScreenIds, _context_formScreenIds;
1856
1860
  if (outputKind === 'email' || outputKind === 'form') return [];
1857
1861
  const home = new Set((_context_homeScreenIds = context.homeScreenIds) != null ? _context_homeScreenIds : []);
1862
+ const forms = new Set((_context_formScreenIds = context.formScreenIds) != null ? _context_formScreenIds : []);
1858
1863
  const sent = [];
1859
1864
  for (const visit of walkTree(tree)){
1860
1865
  var _node_props, _node_props1, _node_props2;
@@ -1864,6 +1869,8 @@ const GRID = 'muiGrid';
1864
1869
  const linksHome = typeof screenId === 'string' && home.has(screenId) || ((_node_props1 = node.props) == null ? void 0 : _node_props1['href']) === '/';
1865
1870
  const label = typeof ((_node_props2 = node.props) == null ? void 0 : _node_props2['children']) === 'string' ? node.props['children'].trim() : '';
1866
1871
  if (!linksHome || !label || HOME_WORDS.test(label)) continue;
1872
+ // Home holds the site's form, and the words ask for what it does (AGL-3729).
1873
+ if (typeof screenId === 'string' && forms.has(screenId) && aiAsksToGetInTouch(label)) continue;
1867
1874
  // The header and the navigation link home by name and by the site's own brand.
1868
1875
  const inNavigation = hasAncestor(tree, visit, (ancestor)=>ancestor.componentId === 'muiAppBar' || [
1869
1876
  'header',
@@ -1874,6 +1881,88 @@ const GRID = 'muiGrid';
1874
1881
  label
1875
1882
  });
1876
1883
  }
1884
+ return sent;
1885
+ }
1886
+ /**
1887
+ * RULE 10'S LINK SENT HOME, REPAIRED IN CODE (AGL-3729).
1888
+ *
1889
+ * The rule's own advice is "link the page that does, or leave the link out",
1890
+ * and both halves are something code can do once it knows where the site's
1891
+ * form is. A Free guided start's café (beta.243, 2026-10-12) put its contact
1892
+ * and pre-order form on Home; its Menu page's "Request weekend pickup" was
1893
+ * sent to Home, and the page was refused on every answer, since the
1894
+ * destination was the compiler's, not the model's. So, before the check reads
1895
+ * the tree, each link `detectUnrelatedScreenLinks` would refuse is:
1896
+ *
1897
+ * - repointed to the page that places the site's form, when its words ask a
1898
+ * visitor to get in touch and the site has such a page that is not the page
1899
+ * being checked (`formScreenIds`, `ownScreenIds`). The platform reaches a
1900
+ * section of another page by no anchor (AGL-2867), so the link names the
1901
+ * page; a link to a section of its own page is the Scroll to element
1902
+ * interaction, which this never writes;
1903
+ * - otherwise left out: the node and everything under it are taken off the
1904
+ * page, and out of its parent's children.
1905
+ *
1906
+ * The root is never taken out. A tree with nothing to repair comes back as
1907
+ * the same object; a node that is changed is copied, never edited in place.
1908
+ */ export function aiSettleUnrelatedScreenLinks(tree, outputKind, context = {}) {
1909
+ var _context_ownScreenIds, _find, _context_formScreenIds;
1910
+ const sent = unrelatedScreenLinks(tree, outputKind, context);
1911
+ if (!sent.length) return tree;
1912
+ const own = new Set((_context_ownScreenIds = context.ownScreenIds) != null ? _context_ownScreenIds : []);
1913
+ const formScreen = (_find = ((_context_formScreenIds = context.formScreenIds) != null ? _context_formScreenIds : []).find((id)=>!own.has(id))) != null ? _find : null;
1914
+ const nodes = _extends({}, tree.nodes);
1915
+ const parents = new Map();
1916
+ for (const visit of walkTree(tree)){
1917
+ const parent = visit.ancestors[visit.ancestors.length - 1];
1918
+ if (parent !== undefined) parents.set(visit.id, parent);
1919
+ }
1920
+ for (const { id, label } of sent){
1921
+ var _nodes_parent_nodes;
1922
+ if (!nodes[id]) continue;
1923
+ if (formScreen && aiAsksToGetInTouch(label)) {
1924
+ var _nodes_id_props;
1925
+ const props = _extends({}, (_nodes_id_props = nodes[id].props) != null ? _nodes_id_props : {}, {
1926
+ screenId: formScreen
1927
+ });
1928
+ delete props['href'];
1929
+ nodes[id] = _extends({}, nodes[id], {
1930
+ props
1931
+ });
1932
+ continue;
1933
+ }
1934
+ const parent = parents.get(id);
1935
+ if (parent === undefined || !nodes[parent]) continue;
1936
+ nodes[parent] = _extends({}, nodes[parent], {
1937
+ nodes: ((_nodes_parent_nodes = nodes[parent].nodes) != null ? _nodes_parent_nodes : []).filter((child)=>child !== id)
1938
+ });
1939
+ const stack = [
1940
+ id
1941
+ ];
1942
+ while(stack.length){
1943
+ var _ref;
1944
+ var _nodes_next;
1945
+ const next = stack.pop();
1946
+ stack.push(...(_ref = (_nodes_next = nodes[next]) == null ? void 0 : _nodes_next.nodes) != null ? _ref : []);
1947
+ delete nodes[next];
1948
+ }
1949
+ }
1950
+ return _extends({}, tree, {
1951
+ nodes
1952
+ });
1953
+ }
1954
+ /**
1955
+ * Rule 10, for a link sent where the site has nothing for it (AGL-3056). A
1956
+ * link whose purpose the site has no screen for is left out; it never goes to
1957
+ * a screen that does something else. The site inventory cannot say what each
1958
+ * screen is for, but it can name the one a model falls back to: a live About
1959
+ * page's footer sent "Request a Consultation" to the home screen, because the
1960
+ * site had no consultation screen. So a Screen Link or a Button, outside the
1961
+ * header and the navigation, that links the home screen with words that do
1962
+ * not say home is refused, with a re-ask to link the screen that does what it
1963
+ * says, or to leave the link out.
1964
+ */ export function detectUnrelatedScreenLinks(tree, outputKind, context = {}) {
1965
+ const sent = unrelatedScreenLinks(tree, outputKind, context);
1877
1966
  if (!sent.length) return [];
1878
1967
  return [
1879
1968
  {
@@ -3682,6 +3771,37 @@ const FORM_SECTION_NAME = /\bform\b|\bsign[- ]?up\b|\bsubscribe\b|\bnewsletter\b
3682
3771
  }))
3683
3772
  });
3684
3773
  }
3774
+ /**
3775
+ * A page section that is a layout region — a header, navigation or footer,
3776
+ * read by the name rule 2's `plan-layout-region-section` reads — is taken off
3777
+ * the page before the rules read the plan (AGL-3750). The region is already
3778
+ * the layout's, so the rule's own answer, "take the section off the page", is
3779
+ * the only one: a live Free start of 2026-10-12 (a bakery that sells cakes to
3780
+ * order) planned a Header and a Footer on its pages three times and stopped.
3781
+ *
3782
+ * Only a section that places nothing, or nothing but a layout (which rule 2
3783
+ * refuses placing as well), is taken off: one that places a form or a
3784
+ * component may be a band misnamed, so the rule still asks. A page whose
3785
+ * every section is a region keeps them all, and is re-asked as before, since
3786
+ * a page of no sections has no answer here. A home this leaves under its
3787
+ * fewest sections, or a Free plan still past its wall, is the existing
3788
+ * checks' to settle or refuse.
3789
+ */ export function aiSettlePlanLayoutRegionSections(plan, inventory) {
3790
+ const kinds = inventoryKinds(inventory);
3791
+ const removable = (section)=>LAYOUT_REGION_NAME.test(section.name) && section.uses.every((ref)=>refKind(ref, plan, kinds) === 'layout');
3792
+ let changed = false;
3793
+ const screens = plan.screens.map((screen)=>{
3794
+ const kept = screen.sections.filter((section)=>!removable(section));
3795
+ if (kept.length === screen.sections.length || !kept.length) return screen;
3796
+ changed = true;
3797
+ return _extends({}, screen, {
3798
+ sections: kept
3799
+ });
3800
+ });
3801
+ return changed ? _extends({}, plan, {
3802
+ screens
3803
+ }) : plan;
3804
+ }
3685
3805
  /** Every saved form a plan could place, the plan's own creations first. */ function planFormChoices(plan, inventory) {
3686
3806
  var _ref;
3687
3807
  return [
@@ -4332,6 +4452,135 @@ function slugKey(slug) {
4332
4452
  const needs = !((_ref = inventory == null ? void 0 : inventory.layouts.length) != null ? _ref : 0) && capabilities.create.layout.allowed ? 1 : 0;
4333
4453
  return Math.max(creates, needs);
4334
4454
  }
4455
+ /** The Free wall a plan is held to, or null where the job is not on the Free taste. */ function freePlanWall(plan, inventory, capabilities) {
4456
+ if (!(capabilities == null ? void 0 : capabilities.freeTaste)) return null;
4457
+ const asked = plan.screens.reduce((sum, screen)=>sum + screen.sections.length, 0);
4458
+ const layouts = freePlanLayouts(plan, inventory, capabilities);
4459
+ const pages = Math.max(plan.screens.length, 1);
4460
+ // A Free site scaffold (AGL-3594) is held to its page cap first, and then to
4461
+ // what a SITE plan's worst case leaves for sections, with room for a retry.
4462
+ const sitePages = capabilities.freeSitePages;
4463
+ const overPages = sitePages !== undefined && plan.screens.length > sitePages;
4464
+ // A site start's saved form is a step of its own (AGL-3596), counted on the wall since AGL-3660.
4465
+ const forms = plan.create.filter((entry)=>entry.kind === 'form').length - aiPlanUncreatable(plan, capabilities).filter((entry)=>entry.kind === 'form').length;
4466
+ const fits = sitePages !== undefined ? aiFreeSiteSectionsWithin({
4467
+ layouts,
4468
+ pages,
4469
+ forms: Math.max(0, forms)
4470
+ }, FREE_AI_TASTE_CREDITS_PER_MONTH) : aiFreePageSectionsWithin({
4471
+ layouts,
4472
+ pages
4473
+ });
4474
+ // A site start's home keeps its full five (AGL-3660): told only "plan at
4475
+ // most 8", the live yoga start of 2026-10-08 cut a home of 6 to 4 and kept
4476
+ // its other page, so the redo names where the sections come from.
4477
+ const homeMin = sitePages !== undefined && plan.screens.some(aiSitePlanIsHome) ? aiSiteHomeMinSections({
4478
+ pages,
4479
+ across: fits
4480
+ }) : 0;
4481
+ return {
4482
+ asked,
4483
+ fits,
4484
+ layouts,
4485
+ pages,
4486
+ sitePages,
4487
+ overPages,
4488
+ homeMin
4489
+ };
4490
+ }
4491
+ /**
4492
+ * The most sections past the Free wall a plan is trimmed by rather than
4493
+ * re-asked (AGL-3737). The live candle-shop start of 2026-10-12 planned 9
4494
+ * sections against a wall of 8 on all three answers, each with a home of 7:
4495
+ * the model reaching for the budget the site redo names ("plan 7 to 8") and
4496
+ * landing one band past it, which no re-ask cured. Within two, the plan's
4497
+ * shape is right and only a band or two the brief needs least is extra, so
4498
+ * taking those from the end loses nothing the plan depends on. Further over,
4499
+ * the shape itself is off — a list planned as sections, a page the wall
4500
+ * cannot pay for — and cutting more would drop content the model meant, so
4501
+ * that plan is still re-asked.
4502
+ */ export const AI_FREE_WALL_SETTLE_MAX_OVER = 2;
4503
+ /** A section through which a visitor reaches the owner: contact, a visit, a booking. */ const REACH_SECTION_NAME = /\b(?:contact|get in touch|visit(?: us)?|find us|locations?|hours|book(?:ing)?|reserve|reservations?)\b/i;
4504
+ /**
4505
+ * Whether the Free wall's trim keeps a section of a page (AGL-3737): the
4506
+ * page's opening, any section that places something (a dataset it lists, a
4507
+ * form, a component — dropping it would leave a creation unplaced), one that
4508
+ * collects answers or says how to reach the owner, and a storefront's
4509
+ * listing parts — the bestsellers, the collections, the sign-up, and a Shop
4510
+ * page's product grid.
4511
+ */ function freeWallKeeps(screen, index) {
4512
+ const section = screen.sections[index];
4513
+ if (index === 0 || section.uses.length > 0) return true;
4514
+ if (FORM_SECTION_NAME.test(section.name) || REACH_SECTION_NAME.test(section.name)) return true;
4515
+ if (aiStorefrontSays('featured', section.name) || aiStorefrontSays('collections', section.name) || aiStorefrontSays('signup', section.name)) {
4516
+ return true;
4517
+ }
4518
+ return aiLayoutIsShopPage(screen) && aiLayoutShopGridSection(screen.sections) === index;
4519
+ }
4520
+ /**
4521
+ * A Free plan a section or two past the wall is trimmed to fit before the
4522
+ * rules read the plan (AGL-3737), rather than re-asked: which sections go has
4523
+ * one answer, so the re-ask only costs the member a stopped start. The live
4524
+ * candle-shop start of 2026-10-12 asked for a home of 7 and a second page of
4525
+ * 2 against a wall of 8, three answers running, and stopped needing input.
4526
+ *
4527
+ * One section goes at a time, the last of its page that `freeWallKeeps`
4528
+ * does not hold, from the page furthest past its share: the home at / past
4529
+ * its fewest (`aiSiteHomeMinSections`, five on a site start whose wall pays
4530
+ * for a full home), any other page past two; on a tie, the longer page, then
4531
+ * the home. No home goes under its fewest and no page under one section. The
4532
+ * shares are what a site start is told to plan (a home of five or six, the
4533
+ * other page two or three): the candle shop's 7 + 2 becomes 6 + 2, never
4534
+ * 7 + 1, and the recorded yoga answer's 6 + 4 becomes 5 + 3, the shape its
4535
+ * own redo chose.
4536
+ *
4537
+ * A plan more than `AI_FREE_WALL_SETTLE_MAX_OVER` past the wall, a site
4538
+ * start past its page cap, a plan with a list split into sections, and a
4539
+ * plan the trim cannot fit without a section it keeps are returned
4540
+ * unchanged, for the wall to re-ask.
4541
+ */ export function aiSettlePlanFreeWall(plan, inventory, capabilities = null) {
4542
+ const wall = freePlanWall(plan, inventory, capabilities);
4543
+ if (!wall || wall.overPages) return plan;
4544
+ const over = wall.asked - wall.fits;
4545
+ if (over <= 0 || over > AI_FREE_WALL_SETTLE_MAX_OVER) return plan;
4546
+ // A list split into sections is joined by its re-ask, which frees the room;
4547
+ // a trim would cut one of its items and hide the split from the rule.
4548
+ if (detectPlanSplitLists(plan, inventory, capabilities).length) return plan;
4549
+ const kept = plan.screens.map((screen)=>screen.sections.map((section, index)=>({
4550
+ section,
4551
+ index
4552
+ })));
4553
+ const home = plan.screens.findIndex(aiSitePlanIsHome);
4554
+ const floor = (screenIndex)=>screenIndex === home ? Math.max(wall.homeMin, 1) : 1;
4555
+ const target = (screenIndex)=>screenIndex === home ? Math.max(wall.homeMin, 1) : 2;
4556
+ // The last section of a page the trim may take, or -1.
4557
+ const last = (screenIndex)=>{
4558
+ const entries = kept[screenIndex];
4559
+ if (entries.length <= floor(screenIndex)) return -1;
4560
+ for(let at = entries.length - 1; at > 0; at -= 1){
4561
+ if (!freeWallKeeps(plan.screens[screenIndex], entries[at].index)) return at;
4562
+ }
4563
+ return -1;
4564
+ };
4565
+ for(let left = over; left > 0; left -= 1){
4566
+ let pick = -1;
4567
+ for(let index = 0; index < kept.length; index += 1){
4568
+ if (last(index) === -1) continue;
4569
+ const room = kept[index].length - target(index);
4570
+ const best = pick === -1 ? null : kept[pick].length - target(pick);
4571
+ if (best === null || room > best || room === best && (kept[index].length > kept[pick].length || kept[index].length === kept[pick].length && index === home)) {
4572
+ pick = index;
4573
+ }
4574
+ }
4575
+ if (pick === -1) return plan;
4576
+ kept[pick].splice(last(pick), 1);
4577
+ }
4578
+ return _extends({}, plan, {
4579
+ screens: plan.screens.map((screen, index)=>kept[index].length === screen.sections.length ? screen : _extends({}, screen, {
4580
+ sections: kept[index].map((entry)=>entry.section)
4581
+ }))
4582
+ });
4583
+ }
4335
4584
  /**
4336
4585
  * The Free wall (plan): a plan asks for no more sections than the Free taste
4337
4586
  * pays for at its worst beside what the job builds first (AGL-3070). A live
@@ -4342,14 +4591,10 @@ function slugKey(slug) {
4342
4591
  * proven with, so no plan the proof admits is refused. It is no building rule,
4343
4592
  * so it names none.
4344
4593
  */ export function detectPlanOverFreeWall(plan, inventory, capabilities) {
4345
- if (!(capabilities == null ? void 0 : capabilities.freeTaste)) return [];
4346
- const asked = plan.screens.reduce((sum, screen)=>sum + screen.sections.length, 0);
4347
- const layouts = freePlanLayouts(plan, inventory, capabilities);
4348
- const pages = Math.max(plan.screens.length, 1);
4349
- // A Free site scaffold (AGL-3594) is held to its page cap first, and then to
4350
- // what a SITE plan's worst case leaves for sections, with room for a retry.
4351
- const sitePages = capabilities.freeSitePages;
4352
- if (sitePages !== undefined && plan.screens.length > sitePages) {
4594
+ const wall = freePlanWall(plan, inventory, capabilities);
4595
+ if (!wall) return [];
4596
+ const { asked, layouts, pages, sitePages, fits, homeMin } = wall;
4597
+ if (sitePages !== undefined && wall.overPages) {
4353
4598
  return [
4354
4599
  {
4355
4600
  rule: null,
@@ -4361,26 +4606,9 @@ function slugKey(slug) {
4361
4606
  }
4362
4607
  ];
4363
4608
  }
4364
- // A site start's saved form is a step of its own (AGL-3596), counted on the wall since AGL-3660.
4365
- const forms = plan.create.filter((entry)=>entry.kind === 'form').length - aiPlanUncreatable(plan, capabilities).filter((entry)=>entry.kind === 'form').length;
4366
- const fits = sitePages !== undefined ? aiFreeSiteSectionsWithin({
4367
- layouts,
4368
- pages,
4369
- forms: Math.max(0, forms)
4370
- }, FREE_AI_TASTE_CREDITS_PER_MONTH) : aiFreePageSectionsWithin({
4371
- layouts,
4372
- pages
4373
- });
4374
4609
  if (asked <= fits) return [];
4375
4610
  const job = pages > 1 ? `a Free plan of ${pages} pages` : 'a Free page';
4376
4611
  const beside = layouts ? ` that creates ${layouts > 1 ? `${layouts} layouts` : 'its layout'}` : '';
4377
- // A site start's home keeps its full five (AGL-3660): told only "plan at
4378
- // most 8", the live yoga start of 2026-10-08 cut a home of 6 to 4 and kept
4379
- // its other page, so the redo names where the sections come from.
4380
- const homeMin = sitePages !== undefined && plan.screens.some(aiSitePlanIsHome) ? aiSiteHomeMinSections({
4381
- pages,
4382
- across: fits
4383
- }) : 0;
4384
4612
  const keepHome = homeMin ? ` Keep the home page at / at ${homeMin} or more sections, and take the rest from the other ${pages > 2 ? 'pages' : 'page'}.` : '';
4385
4613
  // A site start's redo keeps the plan full (AGL-3660): the wall's sections
4386
4614
  // are its budget to use, and a redo told only "at most" cuts far under it.