@aglyn/plugins-ai 1.0.0-beta.238 → 1.0.0-beta.240
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/components/ai-job-links.js +8 -0
- package/src/lib/components/ai-job-links.js.map +1 -1
- package/src/lib/components/ai-job-requests.d.ts +5 -0
- package/src/lib/components/ai-job-requests.js +4 -2
- package/src/lib/components/ai-job-requests.js.map +1 -1
- package/src/lib/components/ai-jobs-store.d.ts +12 -0
- package/src/lib/components/ai-jobs-store.js +15 -0
- package/src/lib/components/ai-jobs-store.js.map +1 -1
- package/src/lib/components/ai-usage-meter-refresh.d.ts +35 -0
- package/src/lib/components/ai-usage-meter-refresh.js +63 -0
- package/src/lib/components/ai-usage-meter-refresh.js.map +1 -0
- package/src/lib/components/assist-panel.component.js +17 -1
- package/src/lib/components/assist-panel.component.js.map +1 -1
- package/src/lib/components/use-ai-usage-meter.d.ts +2 -0
- package/src/lib/components/use-ai-usage-meter.js +8 -3
- package/src/lib/components/use-ai-usage-meter.js.map +1 -1
- package/src/lib/jobs/ai-build-unit-outcome.d.ts +3 -1
- package/src/lib/jobs/ai-build-unit-outcome.js +19 -3
- package/src/lib/jobs/ai-build-unit-outcome.js.map +1 -1
- package/src/lib/jobs/ai-job-build-step.js +29 -2
- package/src/lib/jobs/ai-job-build-step.js.map +1 -1
- package/src/lib/jobs/ai-job-draft-ids.js +7 -2
- package/src/lib/jobs/ai-job-draft-ids.js.map +1 -1
- package/src/lib/jobs/ai-job-form-dataset.d.ts +100 -0
- package/src/lib/jobs/ai-job-form-dataset.js +161 -0
- package/src/lib/jobs/ai-job-form-dataset.js.map +1 -0
- package/src/lib/jobs/ai-job-form-step.d.ts +15 -3
- package/src/lib/jobs/ai-job-form-step.js +37 -9
- package/src/lib/jobs/ai-job-form-step.js.map +1 -1
- package/src/lib/jobs/ai-job-layout-language.js +7 -2
- package/src/lib/jobs/ai-job-layout-language.js.map +1 -1
- package/src/lib/jobs/ai-job-page-language.d.ts +9 -0
- package/src/lib/jobs/ai-job-page-language.js +44 -4
- package/src/lib/jobs/ai-job-page-language.js.map +1 -1
- package/src/lib/jobs/ai-job-page-sections.js +13 -2
- package/src/lib/jobs/ai-job-page-sections.js.map +1 -1
- package/src/lib/jobs/ai-job-page-step.js +32 -2
- package/src/lib/jobs/ai-job-page-step.js.map +1 -1
- package/src/lib/jobs/ai-job-plan-step.d.ts +18 -0
- package/src/lib/jobs/ai-job-plan-step.js +41 -4
- package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
- package/src/lib/jobs/ai-job-site-datasets.d.ts +183 -0
- package/src/lib/jobs/ai-job-site-datasets.js +530 -0
- package/src/lib/jobs/ai-job-site-datasets.js.map +1 -0
- package/src/lib/jobs/ai-job-site-step.d.ts +15 -8
- package/src/lib/jobs/ai-job-site-step.js +148 -29
- package/src/lib/jobs/ai-job-site-step.js.map +1 -1
- package/src/lib/jobs/fixtures/ai-dataset-site-plan-recordings.d.ts +30 -0
- package/src/lib/jobs/fixtures/ai-dataset-site-plan-recordings.js +637 -0
- package/src/lib/jobs/fixtures/ai-dataset-site-plan-recordings.js.map +1 -0
- package/src/lib/layout-language/ai-layout-compiler.d.ts +10 -0
- package/src/lib/layout-language/ai-layout-compiler.js +93 -1
- package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
- package/src/lib/layout-language/ai-layout-gaps.js +17 -1
- package/src/lib/layout-language/ai-layout-gaps.js.map +1 -1
- package/src/lib/layout-language/ai-layout-listings.d.ts +32 -3
- package/src/lib/layout-language/ai-layout-listings.js +62 -9
- package/src/lib/layout-language/ai-layout-listings.js.map +1 -1
- package/src/lib/layout-language/ai-layout-pictures.js +12 -4
- package/src/lib/layout-language/ai-layout-pictures.js.map +1 -1
- package/src/lib/model/ai-build-job.d.ts +2 -0
- package/src/lib/model/ai-build-job.js +16 -1
- package/src/lib/model/ai-build-job.js.map +1 -1
- package/src/lib/model/ai-build-plan.d.ts +23 -3
- package/src/lib/model/ai-build-plan.js +47 -6
- package/src/lib/model/ai-build-plan.js.map +1 -1
- package/src/lib/model/ai-plan-capabilities.d.ts +7 -0
- package/src/lib/model/ai-plan-capabilities.js.map +1 -1
- package/src/lib/model/ai-site-job.d.ts +19 -2
- package/src/lib/model/ai-site-job.js +21 -6
- package/src/lib/model/ai-site-job.js.map +1 -1
- package/src/lib/runtime/ai-dataset-generation.d.ts +77 -0
- package/src/lib/runtime/ai-dataset-generation.js +127 -0
- package/src/lib/runtime/ai-dataset-generation.js.map +1 -0
- package/src/lib/runtime/ai-doctrine-validators.d.ts +84 -0
- package/src/lib/runtime/ai-doctrine-validators.js +307 -1
- package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
- package/src/lib/runtime/ai-doctrine.js +10 -4
- package/src/lib/runtime/ai-doctrine.js.map +1 -1
- package/src/lib/runtime/ai-node-tree.d.ts +2 -0
- package/src/lib/runtime/ai-node-tree.js +16 -0
- package/src/lib/runtime/ai-node-tree.js.map +1 -1
- package/src/lib/tools/ai-dataset-tool.d.ts +68 -0
- package/src/lib/tools/ai-dataset-tool.js +231 -0
- package/src/lib/tools/ai-dataset-tool.js.map +1 -0
- package/src/lib/usage/free-ai-credits-left.js +26 -2
- package/src/lib/usage/free-ai-credits-left.js.map +1 -1
|
@@ -689,6 +689,50 @@ export declare function aiSettleDisagreeingNodes(input: unknown): unknown;
|
|
|
689
689
|
* re-ask. Reads and returns the tree as the model wrote it.
|
|
690
690
|
*/
|
|
691
691
|
export declare function aiSettleCutHeadings(input: unknown): unknown;
|
|
692
|
+
/**
|
|
693
|
+
* THE RULES A CHECK WOULD REFUSE FOR AN ATTRIBUTE, SETTLED IN CODE (AGL-3660).
|
|
694
|
+
*
|
|
695
|
+
* A finding whose fix is one attribute, one level or one key, with nothing to
|
|
696
|
+
* decide about what the page says, is applied before the check reads the
|
|
697
|
+
* tree, as `aiSettleGridItems` sizes a container's items. A live Free guided
|
|
698
|
+
* start's Portfolio page (2026-10-10) was refused for rule 16's `eager-image`:
|
|
699
|
+
* the pictures step loaded every photo of the opening section eagerly, the
|
|
700
|
+
* last pass reads the stored page and asks no model, and Try again could only
|
|
701
|
+
* refuse the same page for the same reason. None of these is a choice a model
|
|
702
|
+
* makes better than the code that knows the answer:
|
|
703
|
+
*
|
|
704
|
+
* - `eager-image` (16): only the tree's first image may load eagerly; every
|
|
705
|
+
* later one has `loading` unset, so the renderer loads it when reached;
|
|
706
|
+
* - `autoplay-video` (16), its attribute half: `autoPlay` and `preload: auto`
|
|
707
|
+
* are taken off. A film with no poster still needs a picture, and is left;
|
|
708
|
+
* - `extra-font` (16): an `sx` font family outside an email is taken off, so
|
|
709
|
+
* the element reads the theme's typography;
|
|
710
|
+
* - `multiple-h1` and `skipped-heading` (11): every top-level heading after a
|
|
711
|
+
* page's first renders as an h2 (a layout's all do, a component's after its
|
|
712
|
+
* first), and a heading after it that skips a level renders one below the
|
|
713
|
+
* heading before it. Only the element changes (`component`): the variant,
|
|
714
|
+
* and so the look, is kept. A heading before a page's h1, and a page with no
|
|
715
|
+
* h1, are the model's to mend;
|
|
716
|
+
* - `multiple-main` and `landmark-in-fragment` (11): a `main` landmark after
|
|
717
|
+
* the first, and any in a component, a form or an email, is unset;
|
|
718
|
+
* - `grid-gap` (12): a Grid container's `sx` gap is taken off and becomes its
|
|
719
|
+
* `spacing` where it sets none, a number gap as itself, anything else 2,
|
|
720
|
+
* which is what the re-ask asks for;
|
|
721
|
+
* - `link-color-on-band` (5): a link or a button whose words are drawn in its
|
|
722
|
+
* band's own family is given that family's contrast text as its `sx` color,
|
|
723
|
+
* which is what the re-ask asks for.
|
|
724
|
+
*
|
|
725
|
+
* A tree with nothing to settle comes back as the same object, so settling is
|
|
726
|
+
* idempotent; a node that is changed is copied, never edited in place.
|
|
727
|
+
*/
|
|
728
|
+
export declare function aiSettleMechanicalRules<T extends AiDoctrineTree>(tree: T, outputKind: AiOutputKind): T;
|
|
729
|
+
/**
|
|
730
|
+
* `aiSettleMechanicalRules` on a tree as the model wrote it, before the
|
|
731
|
+
* palette validator reads it (AGL-3660), as `aiSettleWrittenGridItems` settles
|
|
732
|
+
* its items. Anything that is no flat node map comes back as it was, and a
|
|
733
|
+
* node whose shape is not a palette node's is read as nothing and left as written.
|
|
734
|
+
*/
|
|
735
|
+
export declare function aiSettleWrittenMechanicalRules(input: unknown, outputKind: AiOutputKind): unknown;
|
|
692
736
|
export interface AiDoctrineTreeReport {
|
|
693
737
|
ok: boolean;
|
|
694
738
|
/** The palette validator's result: the tree to store, its repairs and its id map. */
|
|
@@ -754,6 +798,46 @@ export declare function detectPlanSplitLists(plan: AiBuildPlan, inventory: AiSit
|
|
|
754
798
|
* can answer with the form's bare name, which is no reference at all.
|
|
755
799
|
*/
|
|
756
800
|
export declare function aiSettlePlanRefs(plan: AiBuildPlan, inventory?: AiSiteInventory | null): AiBuildPlan;
|
|
801
|
+
/**
|
|
802
|
+
* What customers say, which no dataset of the site's holds: a review, a
|
|
803
|
+
* testimonial, a rating, a quote (AGL-3616). A job never writes them for the
|
|
804
|
+
* people who did not, so a dataset of them is one nothing can seed.
|
|
805
|
+
*/
|
|
806
|
+
export declare const AI_DATASET_SAID_NAME: RegExp;
|
|
807
|
+
/**
|
|
808
|
+
* A plan that creates more datasets than this job may make is settled in
|
|
809
|
+
* code, never re-asked (AGL-3616). The live eval of 2026-10-10 refused a
|
|
810
|
+
* dental practice's plan four times over for a fourth dataset, and a food
|
|
811
|
+
* bank's re-ask for one broke a layout rule its first answer had kept.
|
|
812
|
+
*
|
|
813
|
+
* A dataset of what customers say (`AI_DATASET_SAID_NAME`, in its name or a
|
|
814
|
+
* field) is let go first, whatever the cap: both of those plans made one of
|
|
815
|
+
* testimonials, which nothing may seed.
|
|
816
|
+
*
|
|
817
|
+
* The datasets kept are the most useful: the ones the most sections list,
|
|
818
|
+
* a record template counting double, plan order breaking ties. Each one let
|
|
819
|
+
* go leaves the plan whole — its `new:` reference comes off every section,
|
|
820
|
+
* a record template of it becomes a page of its own, and a section that
|
|
821
|
+
* listed it shows fewer items than rule 8 binds, so its items are written
|
|
822
|
+
* out on the page. Where the job may create no dataset at all, rule 7 still
|
|
823
|
+
* asks, as it does for any creation.
|
|
824
|
+
*/
|
|
825
|
+
export declare function aiSettlePlanDatasets(plan: AiBuildPlan, capabilities?: AiPlanCapabilities | null): AiBuildPlan;
|
|
826
|
+
/**
|
|
827
|
+
* A form's dataset, settled in code before the rules read the plan
|
|
828
|
+
* (AGL-3616, Zach 2026-10-10: Aglyn AI's forms write to its datasets).
|
|
829
|
+
*
|
|
830
|
+
* - A `writesTo` that names nothing the form can write to comes off: a
|
|
831
|
+
* `new:` of no dataset this plan creates, an id the inventory does not
|
|
832
|
+
* hold, or a dataset this job may not create. The form is then the
|
|
833
|
+
* form it always was, its submissions in the Inbox. A dataset the
|
|
834
|
+
* inventory holds, named by its name rather than its id, is given its id.
|
|
835
|
+
* - A dataset this plan creates for a form holds the people who send it:
|
|
836
|
+
* no page lists it and none is its record template, so its `new:` comes
|
|
837
|
+
* off every section and record that named it, and a section that listed
|
|
838
|
+
* it shows fewer items than rule 8 binds.
|
|
839
|
+
*/
|
|
840
|
+
export declare function aiSettlePlanFormDatasets(plan: AiBuildPlan, inventory: AiSiteInventory | null, capabilities?: AiPlanCapabilities | null): AiBuildPlan;
|
|
757
841
|
/**
|
|
758
842
|
* A page that names no layout, on a site where only one layout can frame it,
|
|
759
843
|
* is put in that layout before the rules read the plan (2026-10-07). The
|
|
@@ -23,7 +23,7 @@ import { FREE_AI_TASTE_CREDITS_PER_MONTH } from "../plan-entitlements.js";
|
|
|
23
23
|
import { REUSABLE_INSTANCE_COMPONENT_ID, REUSABLE_INSTANCE_PROP_VALUES_KEY } from "@aglyn/aglyn/app-utils/reusable-component-keys";
|
|
24
24
|
import { parseBreakpointSpan } from "@aglyn/shared-data-enums/breakpoint-span";
|
|
25
25
|
import { renderEmailHtml } from "@aglyn/shared-util-email/email-render";
|
|
26
|
-
import { AI_BUILD_PLAN_CREATION_NOUNS, AI_BUILD_PLAN_EMBED_HOST_NAMES, AI_BUILD_PLAN_LIMITS, AI_PLAN_ASKS_FOR_VIDEO, aiEmbedHostOf, aiEmbedVideoKey, aiPlanCreateFor, aiPlanItemFor, aiPlanSlugKey, aiPlanUndeclaredRefs, isAiPlanNewRef } from "../model/ai-build-plan.js";
|
|
26
|
+
import { AI_BUILD_PLAN_CREATION_NOUNS, AI_BUILD_PLAN_EMBED_HOST_NAMES, AI_BUILD_PLAN_LIMITS, AI_PLAN_ASKS_FOR_VIDEO, aiEmbedHostOf, aiEmbedVideoKey, 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
29
|
import { AI_INSTANCE_REF_PROP, validateAiNodeTree } from "./ai-node-tree.js";
|
|
@@ -2958,6 +2958,183 @@ const BUDGET_METRICS = [
|
|
|
2958
2958
|
nodes
|
|
2959
2959
|
}) : input;
|
|
2960
2960
|
}
|
|
2961
|
+
// ── Mechanical rules, settled in code (AGL-3660) ─────────────────────────
|
|
2962
|
+
/**
|
|
2963
|
+
* THE RULES A CHECK WOULD REFUSE FOR AN ATTRIBUTE, SETTLED IN CODE (AGL-3660).
|
|
2964
|
+
*
|
|
2965
|
+
* A finding whose fix is one attribute, one level or one key, with nothing to
|
|
2966
|
+
* decide about what the page says, is applied before the check reads the
|
|
2967
|
+
* tree, as `aiSettleGridItems` sizes a container's items. A live Free guided
|
|
2968
|
+
* start's Portfolio page (2026-10-10) was refused for rule 16's `eager-image`:
|
|
2969
|
+
* the pictures step loaded every photo of the opening section eagerly, the
|
|
2970
|
+
* last pass reads the stored page and asks no model, and Try again could only
|
|
2971
|
+
* refuse the same page for the same reason. None of these is a choice a model
|
|
2972
|
+
* makes better than the code that knows the answer:
|
|
2973
|
+
*
|
|
2974
|
+
* - `eager-image` (16): only the tree's first image may load eagerly; every
|
|
2975
|
+
* later one has `loading` unset, so the renderer loads it when reached;
|
|
2976
|
+
* - `autoplay-video` (16), its attribute half: `autoPlay` and `preload: auto`
|
|
2977
|
+
* are taken off. A film with no poster still needs a picture, and is left;
|
|
2978
|
+
* - `extra-font` (16): an `sx` font family outside an email is taken off, so
|
|
2979
|
+
* the element reads the theme's typography;
|
|
2980
|
+
* - `multiple-h1` and `skipped-heading` (11): every top-level heading after a
|
|
2981
|
+
* page's first renders as an h2 (a layout's all do, a component's after its
|
|
2982
|
+
* first), and a heading after it that skips a level renders one below the
|
|
2983
|
+
* heading before it. Only the element changes (`component`): the variant,
|
|
2984
|
+
* and so the look, is kept. A heading before a page's h1, and a page with no
|
|
2985
|
+
* h1, are the model's to mend;
|
|
2986
|
+
* - `multiple-main` and `landmark-in-fragment` (11): a `main` landmark after
|
|
2987
|
+
* the first, and any in a component, a form or an email, is unset;
|
|
2988
|
+
* - `grid-gap` (12): a Grid container's `sx` gap is taken off and becomes its
|
|
2989
|
+
* `spacing` where it sets none, a number gap as itself, anything else 2,
|
|
2990
|
+
* which is what the re-ask asks for;
|
|
2991
|
+
* - `link-color-on-band` (5): a link or a button whose words are drawn in its
|
|
2992
|
+
* band's own family is given that family's contrast text as its `sx` color,
|
|
2993
|
+
* which is what the re-ask asks for.
|
|
2994
|
+
*
|
|
2995
|
+
* A tree with nothing to settle comes back as the same object, so settling is
|
|
2996
|
+
* idempotent; a node that is changed is copied, never edited in place.
|
|
2997
|
+
*/ export function aiSettleMechanicalRules(tree, outputKind) {
|
|
2998
|
+
if (!tree.nodes[tree.rootId]) return tree;
|
|
2999
|
+
const nodes = _extends({}, tree.nodes);
|
|
3000
|
+
const copied = new Set();
|
|
3001
|
+
const edit = (id)=>{
|
|
3002
|
+
if (!copied.has(id)) {
|
|
3003
|
+
const node = nodes[id];
|
|
3004
|
+
nodes[id] = _extends({}, node, node.props ? {
|
|
3005
|
+
props: _extends({}, node.props)
|
|
3006
|
+
} : {}, node.sx ? {
|
|
3007
|
+
sx: _extends({}, node.sx)
|
|
3008
|
+
} : {});
|
|
3009
|
+
copied.add(id);
|
|
3010
|
+
}
|
|
3011
|
+
return nodes[id];
|
|
3012
|
+
};
|
|
3013
|
+
const unsetProp = (id, name)=>{
|
|
3014
|
+
var _nodes_id_props;
|
|
3015
|
+
if (((_nodes_id_props = nodes[id].props) == null ? void 0 : _nodes_id_props[name]) === undefined) return;
|
|
3016
|
+
delete edit(id).props[name];
|
|
3017
|
+
};
|
|
3018
|
+
const setProp = (id, name, value)=>{
|
|
3019
|
+
var _node_props;
|
|
3020
|
+
var _nodes_id_props;
|
|
3021
|
+
if (((_nodes_id_props = nodes[id].props) == null ? void 0 : _nodes_id_props[name]) === value) return;
|
|
3022
|
+
const node = edit(id);
|
|
3023
|
+
node.props = _extends({}, (_node_props = node.props) != null ? _node_props : {}, {
|
|
3024
|
+
[name]: value
|
|
3025
|
+
});
|
|
3026
|
+
};
|
|
3027
|
+
const unsetSx = (id, name)=>{
|
|
3028
|
+
var _nodes_id_sx;
|
|
3029
|
+
if (((_nodes_id_sx = nodes[id].sx) == null ? void 0 : _nodes_id_sx[name]) === undefined) return;
|
|
3030
|
+
delete edit(id).sx[name];
|
|
3031
|
+
};
|
|
3032
|
+
const visits = walkTree(tree);
|
|
3033
|
+
const email = outputKind === 'email';
|
|
3034
|
+
const framed = !email && outputKind !== 'form';
|
|
3035
|
+
const fragment = outputKind === 'component' || email || outputKind === 'form';
|
|
3036
|
+
let images = 0;
|
|
3037
|
+
let mains = 0;
|
|
3038
|
+
for (const { id, node } of visits){
|
|
3039
|
+
var _node_sx, _node_props;
|
|
3040
|
+
// Rule 16: one eager image, the first; the rest load when they are reached.
|
|
3041
|
+
if (node.componentId === 'image') {
|
|
3042
|
+
var _node_props1;
|
|
3043
|
+
images += 1;
|
|
3044
|
+
if (images > 1 && ((_node_props1 = node.props) == null ? void 0 : _node_props1['loading']) === 'eager') unsetProp(id, 'loading');
|
|
3045
|
+
}
|
|
3046
|
+
// Rule 16: a film loads nothing before it is played.
|
|
3047
|
+
if (node.componentId === 'video') {
|
|
3048
|
+
var _node_props2, _node_props3;
|
|
3049
|
+
if (((_node_props2 = node.props) == null ? void 0 : _node_props2['autoPlay']) === true) unsetProp(id, 'autoPlay');
|
|
3050
|
+
if (((_node_props3 = node.props) == null ? void 0 : _node_props3['preload']) === 'auto') unsetProp(id, 'preload');
|
|
3051
|
+
}
|
|
3052
|
+
// Rule 16: the theme's typography, outside an email.
|
|
3053
|
+
if (!email && ((_node_sx = node.sx) == null ? void 0 : _node_sx['fontFamily']) !== undefined) unsetSx(id, 'fontFamily');
|
|
3054
|
+
// Rule 11: one main landmark, and none in what is placed inside pages.
|
|
3055
|
+
if (elementOf(node) === 'main') {
|
|
3056
|
+
mains += 1;
|
|
3057
|
+
if (fragment || mains > 1) {
|
|
3058
|
+
var _node_props4, _node_props5;
|
|
3059
|
+
if (((_node_props4 = node.props) == null ? void 0 : _node_props4['component']) === 'main') unsetProp(id, 'component');
|
|
3060
|
+
if (((_node_props5 = node.props) == null ? void 0 : _node_props5['element']) === 'main') unsetProp(id, 'element');
|
|
3061
|
+
}
|
|
3062
|
+
}
|
|
3063
|
+
// Rule 12: a container is spaced by its `spacing`, never an sx gap.
|
|
3064
|
+
if (framed && node.componentId === GRID && ((_node_props = node.props) == null ? void 0 : _node_props['container']) === true) {
|
|
3065
|
+
const gap = GRID_GAP_SX_KEYS.find((key)=>{
|
|
3066
|
+
var _node_sx;
|
|
3067
|
+
return ((_node_sx = node.sx) == null ? void 0 : _node_sx[key]) !== undefined;
|
|
3068
|
+
});
|
|
3069
|
+
if (gap) {
|
|
3070
|
+
var _node_sx1, _node_props6;
|
|
3071
|
+
const value = (_node_sx1 = node.sx) == null ? void 0 : _node_sx1[gap];
|
|
3072
|
+
for (const key of GRID_GAP_SX_KEYS)unsetSx(id, key);
|
|
3073
|
+
if (((_node_props6 = node.props) == null ? void 0 : _node_props6['spacing']) === undefined) setProp(id, 'spacing', typeof value === 'number' ? value : 2);
|
|
3074
|
+
}
|
|
3075
|
+
}
|
|
3076
|
+
}
|
|
3077
|
+
// Rule 11: the heading outline, in document order.
|
|
3078
|
+
if (framed) {
|
|
3079
|
+
const headings = visits.map((visit)=>({
|
|
3080
|
+
id: visit.id,
|
|
3081
|
+
level: aiHeadingLevel(nodes[visit.id])
|
|
3082
|
+
})).filter((entry)=>entry.level !== null);
|
|
3083
|
+
const page = outputKind === 'page' || outputKind === 'template';
|
|
3084
|
+
const firstH1 = headings.findIndex((heading)=>heading.level === 1);
|
|
3085
|
+
// A page's outline is settled from its h1 on; a layout's and a component's from their first heading.
|
|
3086
|
+
const start = page ? firstH1 : 0;
|
|
3087
|
+
let previous = page ? 0 : null;
|
|
3088
|
+
for(let index = Math.max(start, 0); start >= 0 && index < headings.length; index += 1){
|
|
3089
|
+
const heading = headings[index];
|
|
3090
|
+
// A page keeps its first h1, a component its first, and a layout none.
|
|
3091
|
+
const keepsH1 = outputKind !== 'layout' && index === firstH1;
|
|
3092
|
+
let level = heading.level === 1 && !keepsH1 ? 2 : heading.level;
|
|
3093
|
+
previous != null ? previous : previous = level - 1;
|
|
3094
|
+
if (level > previous + 1) level = previous + 1;
|
|
3095
|
+
if (level !== heading.level) setProp(heading.id, 'component', `h${level}`);
|
|
3096
|
+
previous = level;
|
|
3097
|
+
}
|
|
3098
|
+
}
|
|
3099
|
+
// Rule 5: a link's words in its band's contrast text, read on the tree as settled so far.
|
|
3100
|
+
if (framed) {
|
|
3101
|
+
const settled = {
|
|
3102
|
+
rootId: tree.rootId,
|
|
3103
|
+
nodes
|
|
3104
|
+
};
|
|
3105
|
+
for (const visit of walkTree(settled)){
|
|
3106
|
+
var _node_sx2;
|
|
3107
|
+
if (!LINK_COMPONENTS.has(visit.node.componentId)) continue;
|
|
3108
|
+
const family = linkWordsFamily(visit.node);
|
|
3109
|
+
const band = family ? bandOf(settled, visit) : null;
|
|
3110
|
+
if (!band || band.family !== family) continue;
|
|
3111
|
+
const node = edit(visit.id);
|
|
3112
|
+
node.sx = _extends({}, (_node_sx2 = node.sx) != null ? _node_sx2 : {}, {
|
|
3113
|
+
color: `${family}.contrastText`
|
|
3114
|
+
});
|
|
3115
|
+
}
|
|
3116
|
+
}
|
|
3117
|
+
return copied.size ? _extends({}, tree, {
|
|
3118
|
+
nodes
|
|
3119
|
+
}) : tree;
|
|
3120
|
+
}
|
|
3121
|
+
/**
|
|
3122
|
+
* `aiSettleMechanicalRules` on a tree as the model wrote it, before the
|
|
3123
|
+
* palette validator reads it (AGL-3660), as `aiSettleWrittenGridItems` settles
|
|
3124
|
+
* its items. Anything that is no flat node map comes back as it was, and a
|
|
3125
|
+
* node whose shape is not a palette node's is read as nothing and left as written.
|
|
3126
|
+
*/ export function aiSettleWrittenMechanicalRules(input, outputKind) {
|
|
3127
|
+
if (!isRecord(input) || typeof input['rootId'] !== 'string' || !isRecord(input['nodes'])) return input;
|
|
3128
|
+
const readable = Object.entries(input['nodes']).filter(([, node])=>isRecord(node) && typeof node['componentId'] === 'string' && (node['props'] === undefined || isRecord(node['props'])) && (node['sx'] === undefined || isRecord(node['sx'])) && (node['nodes'] === undefined || Array.isArray(node['nodes']) && node['nodes'].every((child)=>typeof child === 'string')));
|
|
3129
|
+
const tree = {
|
|
3130
|
+
rootId: input['rootId'],
|
|
3131
|
+
nodes: Object.fromEntries(readable)
|
|
3132
|
+
};
|
|
3133
|
+
const settled = aiSettleMechanicalRules(tree, outputKind);
|
|
3134
|
+
return settled === tree ? input : _extends({}, input, {
|
|
3135
|
+
nodes: _extends({}, input['nodes'], settled.nodes)
|
|
3136
|
+
});
|
|
3137
|
+
}
|
|
2961
3138
|
/**
|
|
2962
3139
|
* The whole check on one emitted tree: the palette validator first, then
|
|
2963
3140
|
* every tree rule, then the budget. `otherPages` are the trees generated
|
|
@@ -3324,6 +3501,135 @@ const FORM_SECTION_NAME = /\bform\b|\bsign[- ]?up\b|\bsubscribe\b|\bnewsletter\b
|
|
|
3324
3501
|
screens
|
|
3325
3502
|
}) : plan;
|
|
3326
3503
|
}
|
|
3504
|
+
/**
|
|
3505
|
+
* What customers say, which no dataset of the site's holds: a review, a
|
|
3506
|
+
* testimonial, a rating, a quote (AGL-3616). A job never writes them for the
|
|
3507
|
+
* people who did not, so a dataset of them is one nothing can seed.
|
|
3508
|
+
*/ export const AI_DATASET_SAID_NAME = /\b(reviews?|testimonials?|ratings?|quotes?|stars?|endorsements?)\b/i;
|
|
3509
|
+
/**
|
|
3510
|
+
* A plan that creates more datasets than this job may make is settled in
|
|
3511
|
+
* code, never re-asked (AGL-3616). The live eval of 2026-10-10 refused a
|
|
3512
|
+
* dental practice's plan four times over for a fourth dataset, and a food
|
|
3513
|
+
* bank's re-ask for one broke a layout rule its first answer had kept.
|
|
3514
|
+
*
|
|
3515
|
+
* A dataset of what customers say (`AI_DATASET_SAID_NAME`, in its name or a
|
|
3516
|
+
* field) is let go first, whatever the cap: both of those plans made one of
|
|
3517
|
+
* testimonials, which nothing may seed.
|
|
3518
|
+
*
|
|
3519
|
+
* The datasets kept are the most useful: the ones the most sections list,
|
|
3520
|
+
* a record template counting double, plan order breaking ties. Each one let
|
|
3521
|
+
* go leaves the plan whole — its `new:` reference comes off every section,
|
|
3522
|
+
* a record template of it becomes a page of its own, and a section that
|
|
3523
|
+
* listed it shows fewer items than rule 8 binds, so its items are written
|
|
3524
|
+
* out on the page. Where the job may create no dataset at all, rule 7 still
|
|
3525
|
+
* asks, as it does for any creation.
|
|
3526
|
+
*/ export function aiSettlePlanDatasets(plan, capabilities = null) {
|
|
3527
|
+
var _ref, _entry_left;
|
|
3528
|
+
const entry = capabilities == null ? void 0 : capabilities.create.dataset;
|
|
3529
|
+
if (!(entry == null ? void 0 : entry.allowed)) return plan;
|
|
3530
|
+
const cap = Math.min((_ref = capabilities == null ? void 0 : capabilities.datasetsMax) != null ? _ref : Infinity, (_entry_left = entry.left) != null ? _entry_left : Infinity);
|
|
3531
|
+
const datasets = plan.create.filter((one)=>one.kind === 'dataset');
|
|
3532
|
+
// A dataset a form writes to holds what visitors send (AGL-3616): its
|
|
3533
|
+
// fields may well be a rating or a quote request, and nothing seeds it.
|
|
3534
|
+
const forForms = aiPlanFormDatasetNames(plan);
|
|
3535
|
+
const said = (one)=>!forForms.has(one.name.trim().toLowerCase()) && (AI_DATASET_SAID_NAME.test(one.name) || one.fields.some((field)=>AI_DATASET_SAID_NAME.test(field)));
|
|
3536
|
+
if (!datasets.some(said) && (!Number.isFinite(cap) || datasets.length <= cap)) return plan;
|
|
3537
|
+
const named = (ref, name)=>!!ref && isAiPlanNewRef(ref) && ref.slice('new:'.length).trim().toLowerCase() === name.trim().toLowerCase();
|
|
3538
|
+
// A form's dataset counts as much as a record template: the form the
|
|
3539
|
+
// person asked for is built around it.
|
|
3540
|
+
const score = (name)=>(forForms.has(name.trim().toLowerCase()) ? 2 : 0) + plan.screens.reduce((total, screen)=>{
|
|
3541
|
+
var _screen_record;
|
|
3542
|
+
return total + (named((_screen_record = screen.record) == null ? void 0 : _screen_record.dataset, name) ? 2 : 0) + screen.sections.filter((section)=>section.uses.some((ref)=>named(ref, name))).length;
|
|
3543
|
+
}, 0);
|
|
3544
|
+
const ranked = datasets.filter((one)=>!said(one)).map((one, index)=>({
|
|
3545
|
+
name: one.name,
|
|
3546
|
+
index,
|
|
3547
|
+
score: score(one.name)
|
|
3548
|
+
})).sort((a, b)=>b.score - a.score || a.index - b.index);
|
|
3549
|
+
const kept = new Set(ranked.slice(0, Math.max(0, cap)).map((one)=>one.name.trim().toLowerCase()));
|
|
3550
|
+
const dropped = new Set(datasets.map((one)=>one.name.trim().toLowerCase()).filter((name)=>!kept.has(name)));
|
|
3551
|
+
const isDropped = (ref)=>[
|
|
3552
|
+
...dropped
|
|
3553
|
+
].some((name)=>named(ref, name));
|
|
3554
|
+
return _extends({}, plan, {
|
|
3555
|
+
create: plan.create.filter((one)=>one.kind !== 'dataset' || !dropped.has(one.name.trim().toLowerCase()))// A form whose dataset was let go sends its submissions to the Inbox only.
|
|
3556
|
+
.map((one)=>one.kind === 'form' && isDropped(one.writesTo) ? withoutWritesTo(one) : one),
|
|
3557
|
+
screens: plan.screens.map((screen)=>_extends({}, screen, {
|
|
3558
|
+
record: screen.record && isDropped(screen.record.dataset) ? null : screen.record,
|
|
3559
|
+
sections: screen.sections.map((section)=>section.uses.some(isDropped) ? _extends({}, section, {
|
|
3560
|
+
uses: section.uses.filter((ref)=>!isDropped(ref)),
|
|
3561
|
+
items: Math.min(section.items, AI_TYPED_LIST_MIN_ITEMS - 1)
|
|
3562
|
+
}) : section)
|
|
3563
|
+
}))
|
|
3564
|
+
});
|
|
3565
|
+
}
|
|
3566
|
+
/** A creation with no `writesTo`, as a plan that never said one reads. */ function withoutWritesTo(creation) {
|
|
3567
|
+
const rest = _extends({}, creation);
|
|
3568
|
+
delete rest.writesTo;
|
|
3569
|
+
return rest;
|
|
3570
|
+
}
|
|
3571
|
+
/**
|
|
3572
|
+
* A form's dataset, settled in code before the rules read the plan
|
|
3573
|
+
* (AGL-3616, Zach 2026-10-10: Aglyn AI's forms write to its datasets).
|
|
3574
|
+
*
|
|
3575
|
+
* - A `writesTo` that names nothing the form can write to comes off: a
|
|
3576
|
+
* `new:` of no dataset this plan creates, an id the inventory does not
|
|
3577
|
+
* hold, or a dataset this job may not create. The form is then the
|
|
3578
|
+
* form it always was, its submissions in the Inbox. A dataset the
|
|
3579
|
+
* inventory holds, named by its name rather than its id, is given its id.
|
|
3580
|
+
* - A dataset this plan creates for a form holds the people who send it:
|
|
3581
|
+
* no page lists it and none is its record template, so its `new:` comes
|
|
3582
|
+
* off every section and record that named it, and a section that listed
|
|
3583
|
+
* it shows fewer items than rule 8 binds.
|
|
3584
|
+
*/ export function aiSettlePlanFormDatasets(plan, inventory, capabilities = null) {
|
|
3585
|
+
var _ref;
|
|
3586
|
+
if (!plan.create.some((one)=>one.kind === 'form' && one.writesTo)) return plan;
|
|
3587
|
+
const held = (_ref = inventory == null ? void 0 : inventory.datasets) != null ? _ref : [];
|
|
3588
|
+
const creatable = !capabilities || capabilities.create.dataset.allowed;
|
|
3589
|
+
let changed = false;
|
|
3590
|
+
const create = plan.create.map((one)=>{
|
|
3591
|
+
if (one.kind !== 'form' || !one.writesTo) return one;
|
|
3592
|
+
const ref = one.writesTo.trim();
|
|
3593
|
+
if (isAiPlanNewRef(ref)) {
|
|
3594
|
+
var _aiPlanCreateFor, _aiPlanItemFor;
|
|
3595
|
+
if (creatable && ((_aiPlanCreateFor = aiPlanCreateFor(plan, ref)) == null ? void 0 : _aiPlanCreateFor.kind) === 'dataset') return one;
|
|
3596
|
+
// A build's dataset item, which the data plugin's operation makes.
|
|
3597
|
+
if (((_aiPlanItemFor = aiPlanItemFor(plan, ref)) == null ? void 0 : _aiPlanItemFor.op) === AI_PLAN_DATASET_ITEM_OP) return one;
|
|
3598
|
+
} else {
|
|
3599
|
+
if (held.some((row)=>row.id === ref)) return one;
|
|
3600
|
+
const byName = held.find((row)=>aiNamesMatch(row.name, ref));
|
|
3601
|
+
if (byName) {
|
|
3602
|
+
changed = true;
|
|
3603
|
+
return _extends({}, one, {
|
|
3604
|
+
writesTo: byName.id
|
|
3605
|
+
});
|
|
3606
|
+
}
|
|
3607
|
+
}
|
|
3608
|
+
changed = true;
|
|
3609
|
+
return withoutWritesTo(one);
|
|
3610
|
+
});
|
|
3611
|
+
const settled = _extends({}, plan, {
|
|
3612
|
+
create
|
|
3613
|
+
});
|
|
3614
|
+
const forForms = aiPlanFormDatasetNames(settled);
|
|
3615
|
+
const listsOne = (ref)=>isAiPlanNewRef(ref) && forForms.has(ref.slice('new:'.length).trim().toLowerCase());
|
|
3616
|
+
const screens = plan.screens.map((screen)=>{
|
|
3617
|
+
const record = screen.record && listsOne(screen.record.dataset) ? null : screen.record;
|
|
3618
|
+
const sections = screen.sections.map((section)=>section.uses.some(listsOne) ? _extends({}, section, {
|
|
3619
|
+
uses: section.uses.filter((ref)=>!listsOne(ref)),
|
|
3620
|
+
items: Math.min(section.items, AI_TYPED_LIST_MIN_ITEMS - 1)
|
|
3621
|
+
}) : section);
|
|
3622
|
+
if (record === screen.record && sections.every((section, index)=>section === screen.sections[index])) return screen;
|
|
3623
|
+
changed = true;
|
|
3624
|
+
return _extends({}, screen, {
|
|
3625
|
+
record,
|
|
3626
|
+
sections
|
|
3627
|
+
});
|
|
3628
|
+
});
|
|
3629
|
+
return changed ? _extends({}, settled, {
|
|
3630
|
+
screens
|
|
3631
|
+
}) : plan;
|
|
3632
|
+
}
|
|
3327
3633
|
/**
|
|
3328
3634
|
* A page that names no layout, on a site where only one layout can frame it,
|
|
3329
3635
|
* is put in that layout before the rules read the plan (2026-10-07). The
|