@aglyn/plugins-ai 1.0.0-beta.239 → 1.0.0-beta.241
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.d.ts +17 -0
- package/src/lib/components/ai-job-links.js +63 -0
- package/src/lib/components/ai-job-links.js.map +1 -1
- package/src/lib/components/ai-job-plan.component.js +8 -1
- package/src/lib/components/ai-job-plan.component.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-site-build-page.component.js +7 -1
- package/src/lib/components/ai-site-build-page.component.js.map +1 -1
- package/src/lib/components/ai-store-finish-card.component.d.ts +28 -0
- package/src/lib/components/ai-store-finish-card.component.js +116 -0
- package/src/lib/components/ai-store-finish-card.component.js.map +1 -0
- package/src/lib/components/assist-build-card.component.js +12 -1
- package/src/lib/components/assist-build-card.component.js.map +1 -1
- package/src/lib/components/assist-panel.component.js +14 -2
- package/src/lib/components/assist-panel.component.js.map +1 -1
- package/src/lib/jobs/ai-job-build-step.d.ts +45 -1
- package/src/lib/jobs/ai-job-build-step.js +283 -7
- package/src/lib/jobs/ai-job-build-step.js.map +1 -1
- package/src/lib/jobs/ai-job-drafts.d.ts +9 -1
- package/src/lib/jobs/ai-job-drafts.js +4 -1
- package/src/lib/jobs/ai-job-drafts.js.map +1 -1
- package/src/lib/jobs/ai-job-layout-language.d.ts +3 -0
- package/src/lib/jobs/ai-job-layout-language.js +11 -4
- package/src/lib/jobs/ai-job-layout-language.js.map +1 -1
- package/src/lib/jobs/ai-job-layout-step.js +3 -1
- package/src/lib/jobs/ai-job-layout-step.js.map +1 -1
- package/src/lib/jobs/ai-job-page-language.d.ts +2 -1
- package/src/lib/jobs/ai-job-page-language.js +26 -9
- package/src/lib/jobs/ai-job-page-language.js.map +1 -1
- package/src/lib/jobs/ai-job-page-sections.d.ts +11 -0
- package/src/lib/jobs/ai-job-page-sections.js +53 -5
- package/src/lib/jobs/ai-job-page-sections.js.map +1 -1
- package/src/lib/jobs/ai-job-page-step.d.ts +2 -1
- package/src/lib/jobs/ai-job-page-step.js +51 -10
- package/src/lib/jobs/ai-job-page-step.js.map +1 -1
- package/src/lib/jobs/ai-job-plan-step.d.ts +12 -0
- package/src/lib/jobs/ai-job-plan-step.js +41 -7
- package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
- package/src/lib/jobs/ai-job-site-datasets.d.ts +89 -2
- package/src/lib/jobs/ai-job-site-datasets.js +239 -15
- package/src/lib/jobs/ai-job-site-datasets.js.map +1 -1
- package/src/lib/jobs/ai-job-site-step.d.ts +18 -2
- package/src/lib/jobs/ai-job-site-step.js +109 -22
- package/src/lib/jobs/ai-job-site-step.js.map +1 -1
- package/src/lib/jobs/ai-job-site-store-pages.d.ts +75 -0
- package/src/lib/jobs/ai-job-site-store-pages.js +195 -0
- package/src/lib/jobs/ai-job-site-store-pages.js.map +1 -0
- package/src/lib/jobs/ai-job-store-links-layout.d.ts +71 -0
- package/src/lib/jobs/ai-job-store-links-layout.js +95 -0
- package/src/lib/jobs/ai-job-store-links-layout.js.map +1 -0
- package/src/lib/jobs/ai-layout-stock-photos.d.ts +69 -3
- package/src/lib/jobs/ai-layout-stock-photos.js +603 -42
- package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
- package/src/lib/jobs/ai-site-publish.d.ts +18 -0
- package/src/lib/jobs/ai-site-publish.js +76 -15
- package/src/lib/jobs/ai-site-publish.js.map +1 -1
- package/src/lib/jobs/fixtures/ai-stock-photo-ember-oak-hits.d.ts +43 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-ember-oak-hits.js +61 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-ember-oak-hits.js.map +1 -0
- package/src/lib/layout-language/ai-layout-compiler.js +25 -2
- package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
- package/src/lib/layout-language/ai-layout-frame.d.ts +10 -0
- package/src/lib/layout-language/ai-layout-frame.js +58 -4
- package/src/lib/layout-language/ai-layout-frame.js.map +1 -1
- package/src/lib/layout-language/ai-layout-listings.d.ts +6 -0
- package/src/lib/layout-language/ai-layout-listings.js +3 -0
- 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 +23 -3
- package/src/lib/model/ai-build-job.js.map +1 -1
- package/src/lib/model/ai-build-plan.d.ts +7 -0
- package/src/lib/model/ai-build-plan.js.map +1 -1
- package/src/lib/model/ai-build-progress.js +3 -1
- package/src/lib/model/ai-build-progress.js.map +1 -1
- package/src/lib/model/ai-build-store-pages.d.ts +133 -0
- package/src/lib/model/ai-build-store-pages.js +209 -0
- package/src/lib/model/ai-build-store-pages.js.map +1 -0
- package/src/lib/model/ai-layout-store-links.d.ts +64 -0
- package/src/lib/model/ai-layout-store-links.js +165 -0
- package/src/lib/model/ai-layout-store-links.js.map +1 -0
- package/src/lib/model/ai-site-inventory.d.ts +5 -0
- package/src/lib/model/ai-site-inventory.js.map +1 -1
- package/src/lib/model/ai-site-store-pages.d.ts +192 -0
- package/src/lib/model/ai-site-store-pages.js +383 -0
- package/src/lib/model/ai-site-store-pages.js.map +1 -0
- package/src/lib/runtime/ai-doctrine-validators.d.ts +44 -0
- package/src/lib/runtime/ai-doctrine-validators.js +177 -0
- package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
- package/src/lib/runtime/ai-doctrine.js +3 -2
- package/src/lib/runtime/ai-doctrine.js.map +1 -1
- package/src/lib/runtime/site-inventory.js +8 -3
- package/src/lib/runtime/site-inventory.js.map +1 -1
- package/src/lib/testing/stand-in-dataset-index.js +8 -4
- package/src/lib/testing/stand-in-dataset-index.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. */
|
|
@@ -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
|