@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.
- package/package.json +17 -17
- package/src/lib/activity/ai-activity-actions.js +3 -1
- package/src/lib/activity/ai-activity-actions.js.map +1 -1
- package/src/lib/components/ai-job-links.js +7 -0
- package/src/lib/components/ai-job-links.js.map +1 -1
- package/src/lib/components/ai-product-zones.d.ts +3 -1
- package/src/lib/components/ai-product-zones.js.map +1 -1
- package/src/lib/components/ai-products-hub-card.component.js +1 -1
- package/src/lib/components/ai-products-hub-card.component.js.map +1 -1
- package/src/lib/components/ai-products-proposals.component.js +17 -9
- package/src/lib/components/ai-products-proposals.component.js.map +1 -1
- package/src/lib/components/ai-site-start-card.component.js +6 -4
- package/src/lib/components/ai-site-start-card.component.js.map +1 -1
- package/src/lib/components/ai-upsell-dialog.component.js +1 -1
- package/src/lib/components/ai-upsell-dialog.component.js.map +1 -1
- package/src/lib/jobs/ai-build-capabilities.js +162 -2
- package/src/lib/jobs/ai-build-capabilities.js.map +1 -1
- package/src/lib/jobs/ai-job-build-step.d.ts +7 -0
- package/src/lib/jobs/ai-job-build-step.js +85 -16
- package/src/lib/jobs/ai-job-build-step.js.map +1 -1
- package/src/lib/jobs/ai-job-duplicate-step.d.ts +123 -0
- package/src/lib/jobs/ai-job-duplicate-step.js +269 -0
- package/src/lib/jobs/ai-job-duplicate-step.js.map +1 -0
- package/src/lib/jobs/ai-job-layout-language.js +24 -6
- package/src/lib/jobs/ai-job-layout-language.js.map +1 -1
- package/src/lib/jobs/ai-job-navigation-step.d.ts +157 -0
- package/src/lib/jobs/ai-job-navigation-step.js +439 -0
- package/src/lib/jobs/ai-job-navigation-step.js.map +1 -0
- package/src/lib/jobs/ai-job-page-language.d.ts +5 -2
- package/src/lib/jobs/ai-job-page-language.js +38 -10
- package/src/lib/jobs/ai-job-page-language.js.map +1 -1
- package/src/lib/jobs/ai-job-page-sections.js +1 -1
- package/src/lib/jobs/ai-job-page-sections.js.map +1 -1
- package/src/lib/jobs/ai-job-page-step.js +11 -4
- package/src/lib/jobs/ai-job-page-step.js.map +1 -1
- package/src/lib/jobs/ai-job-plan-step.d.ts +4 -2
- package/src/lib/jobs/ai-job-plan-step.js +16 -7
- package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
- package/src/lib/jobs/ai-job-products-step.d.ts +2 -2
- package/src/lib/jobs/ai-job-products-step.js +2 -2
- package/src/lib/jobs/ai-job-products-step.js.map +1 -1
- package/src/lib/jobs/ai-job-settings-step.d.ts +272 -0
- package/src/lib/jobs/ai-job-settings-step.js +695 -0
- package/src/lib/jobs/ai-job-settings-step.js.map +1 -0
- package/src/lib/jobs/ai-job-site-content.d.ts +20 -6
- package/src/lib/jobs/ai-job-site-content.js +28 -12
- package/src/lib/jobs/ai-job-site-content.js.map +1 -1
- package/src/lib/jobs/ai-job-site-datasets.d.ts +2 -0
- package/src/lib/jobs/ai-job-site-datasets.js +10 -3
- package/src/lib/jobs/ai-job-site-datasets.js.map +1 -1
- package/src/lib/jobs/ai-job-site-step.d.ts +3 -1
- package/src/lib/jobs/ai-job-site-step.js +86 -12
- package/src/lib/jobs/ai-job-site-step.js.map +1 -1
- package/src/lib/jobs/ai-site-publish.d.ts +76 -4
- package/src/lib/jobs/ai-site-publish.js +231 -59
- package/src/lib/jobs/ai-site-publish.js.map +1 -1
- package/src/lib/layout-language/ai-layout-compiler.d.ts +7 -4
- package/src/lib/layout-language/ai-layout-compiler.js +26 -25
- package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
- package/src/lib/layout-language/ai-layout-frame-hollows.d.ts +41 -0
- package/src/lib/layout-language/ai-layout-frame-hollows.js +330 -0
- package/src/lib/layout-language/ai-layout-frame-hollows.js.map +1 -0
- package/src/lib/layout-language/ai-layout-frame.d.ts +18 -1
- package/src/lib/layout-language/ai-layout-frame.js +83 -13
- package/src/lib/layout-language/ai-layout-frame.js.map +1 -1
- package/src/lib/layout-language/ai-layout-links.js +5 -2
- package/src/lib/layout-language/ai-layout-links.js.map +1 -1
- package/src/lib/layout-language/ai-layout-listings.d.ts +35 -0
- package/src/lib/layout-language/ai-layout-listings.js +34 -0
- package/src/lib/layout-language/ai-layout-listings.js.map +1 -1
- package/src/lib/layout-language/ai-layout-tree.d.ts +2 -0
- package/src/lib/layout-language/ai-layout-tree.js +17 -0
- package/src/lib/layout-language/ai-layout-tree.js.map +1 -1
- package/src/lib/model/ai-build-job.d.ts +23 -0
- package/src/lib/model/ai-build-job.js +102 -13
- package/src/lib/model/ai-build-job.js.map +1 -1
- package/src/lib/model/ai-build-plan.d.ts +25 -0
- package/src/lib/model/ai-build-plan.js +45 -1
- package/src/lib/model/ai-build-plan.js.map +1 -1
- package/src/lib/model/ai-credit-estimate.d.ts +52 -0
- package/src/lib/model/ai-credit-estimate.js +51 -0
- package/src/lib/model/ai-credit-estimate.js.map +1 -1
- package/src/lib/model/ai-job-activity.js +4 -1
- package/src/lib/model/ai-job-activity.js.map +1 -1
- package/src/lib/model/ai-jobs.types.d.ts +10 -2
- package/src/lib/model/ai-jobs.types.js +4 -1
- package/src/lib/model/ai-jobs.types.js.map +1 -1
- package/src/lib/model/ai-layout-menus.d.ts +104 -0
- package/src/lib/model/ai-layout-menus.js +518 -0
- package/src/lib/model/ai-layout-menus.js.map +1 -0
- package/src/lib/model/ai-products.d.ts +31 -1
- package/src/lib/model/ai-products.js +34 -1
- package/src/lib/model/ai-products.js.map +1 -1
- package/src/lib/model/ai-site-inventory.d.ts +15 -0
- package/src/lib/model/ai-site-inventory.js.map +1 -1
- package/src/lib/model/ai-site-job.d.ts +10 -0
- package/src/lib/model/ai-site-job.js +36 -9
- package/src/lib/model/ai-site-job.js.map +1 -1
- package/src/lib/model/ai-site-store-pages.js +1 -1
- package/src/lib/model/ai-site-store-pages.js.map +1 -1
- package/src/lib/runtime/ai-doctrine-validators.d.ts +95 -1
- package/src/lib/runtime/ai-doctrine-validators.js +264 -36
- package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
- package/src/lib/runtime/ai-doctrine.js +7 -2
- package/src/lib/runtime/ai-doctrine.js.map +1 -1
- package/src/lib/runtime/ai-palette.generated.js +1 -1
- package/src/lib/runtime/ai-palette.generated.js.map +1 -1
- package/src/lib/runtime/ai-products-generation.d.ts +4 -2
- package/src/lib/runtime/ai-products-generation.js +12 -6
- package/src/lib/runtime/ai-products-generation.js.map +1 -1
- package/src/lib/runtime/site-inventory.d.ts +12 -0
- package/src/lib/runtime/site-inventory.js +29 -1
- package/src/lib/runtime/site-inventory.js.map +1 -1
- package/src/lib/server.js +12 -0
- package/src/lib/server.js.map +1 -1
- package/src/lib/testing/stand-in-dataset-index.js +37 -3
- package/src/lib/testing/stand-in-dataset-index.js.map +1 -1
- package/src/lib/tools/ai-products-tool.d.ts +3 -1
- package/src/lib/tools/ai-products-tool.js +15 -5
- package/src/lib/tools/ai-products-tool.js.map +1 -1
- package/src/lib/usage/ai-quota-checks.d.ts +13 -3
- package/src/lib/usage/ai-quota-checks.js +46 -8
- 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
|
-
*
|
|
1846
|
-
*
|
|
1847
|
-
*
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
*
|
|
1853
|
-
*
|
|
1854
|
-
|
|
1855
|
-
|
|
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
|
-
|
|
4346
|
-
|
|
4347
|
-
const layouts
|
|
4348
|
-
|
|
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.
|