@aglyn/plugins-ai 1.0.0-beta.232 → 1.0.0-beta.234
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 +6 -0
- package/src/lib/components/ai-job-links.js +9 -0
- package/src/lib/components/ai-job-links.js.map +1 -1
- package/src/lib/components/ai-site-build-page.component.js +41 -3
- package/src/lib/components/ai-site-build-page.component.js.map +1 -1
- package/src/lib/components/ai-site-start-card.component.js +91 -8
- package/src/lib/components/ai-site-start-card.component.js.map +1 -1
- package/src/lib/declarations.server.js +7 -4
- package/src/lib/declarations.server.js.map +1 -1
- package/src/lib/jobs/ai-build-capabilities.js +8 -2
- package/src/lib/jobs/ai-build-capabilities.js.map +1 -1
- package/src/lib/jobs/ai-job-admission.d.ts +2 -1
- package/src/lib/jobs/ai-job-admission.js.map +1 -1
- package/src/lib/jobs/ai-job-email-step.d.ts +39 -0
- package/src/lib/jobs/ai-job-email-step.js +116 -5
- package/src/lib/jobs/ai-job-email-step.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 +3 -1
- package/src/lib/jobs/ai-job-layout-language.js.map +1 -1
- package/src/lib/jobs/ai-job-layout-site-pages.d.ts +2 -0
- package/src/lib/jobs/ai-job-layout-site-pages.js +23 -11
- package/src/lib/jobs/ai-job-layout-site-pages.js.map +1 -1
- package/src/lib/jobs/ai-job-layout-step.js +3 -2
- package/src/lib/jobs/ai-job-layout-step.js.map +1 -1
- package/src/lib/jobs/ai-job-page-language.d.ts +16 -0
- package/src/lib/jobs/ai-job-page-language.js +35 -6
- package/src/lib/jobs/ai-job-page-language.js.map +1 -1
- package/src/lib/jobs/ai-job-page-step.d.ts +3 -0
- package/src/lib/jobs/ai-job-page-step.js +18 -4
- package/src/lib/jobs/ai-job-page-step.js.map +1 -1
- package/src/lib/jobs/ai-job-plan-step.d.ts +40 -1
- package/src/lib/jobs/ai-job-plan-step.js +121 -16
- package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
- package/src/lib/jobs/ai-job-site-content.d.ts +6 -6
- package/src/lib/jobs/ai-job-site-content.js +6 -12
- package/src/lib/jobs/ai-job-site-content.js.map +1 -1
- package/src/lib/jobs/ai-job-site-step.d.ts +11 -0
- package/src/lib/jobs/ai-job-site-step.js +96 -44
- package/src/lib/jobs/ai-job-site-step.js.map +1 -1
- package/src/lib/jobs/ai-jobs-notify.d.ts +14 -4
- package/src/lib/jobs/ai-jobs-notify.js +70 -4
- package/src/lib/jobs/ai-jobs-notify.js.map +1 -1
- package/src/lib/jobs/ai-jobs.d.ts +22 -4
- package/src/lib/jobs/ai-jobs.js +47 -12
- package/src/lib/jobs/ai-jobs.js.map +1 -1
- package/src/lib/jobs/ai-layout-stock-photos.d.ts +98 -0
- package/src/lib/jobs/ai-layout-stock-photos.js +248 -0
- package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -0
- package/src/lib/jobs/ai-site-publish.d.ts +11 -0
- package/src/lib/jobs/ai-site-publish.js +31 -1
- package/src/lib/jobs/ai-site-publish.js.map +1 -1
- package/src/lib/jobs/fixtures/ai-juniper-portfolio-page-recording.d.ts +151 -0
- package/src/lib/jobs/fixtures/ai-juniper-portfolio-page-recording.js +183 -0
- package/src/lib/jobs/fixtures/ai-juniper-portfolio-page-recording.js.map +1 -0
- package/src/lib/jobs/fixtures/ai-slow-roads-site-plan-recording.d.ts +27 -0
- package/src/lib/jobs/fixtures/ai-slow-roads-site-plan-recording.js +192 -0
- package/src/lib/jobs/fixtures/ai-slow-roads-site-plan-recording.js.map +1 -0
- package/src/lib/jobs/fixtures/ai-yoga-site-plan-recording.d.ts +27 -0
- package/src/lib/jobs/fixtures/ai-yoga-site-plan-recording.js +222 -0
- package/src/lib/jobs/fixtures/ai-yoga-site-plan-recording.js.map +1 -0
- package/src/lib/layout-language/ai-layout-compiler.d.ts +29 -0
- package/src/lib/layout-language/ai-layout-compiler.js +698 -12
- package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
- package/src/lib/layout-language/ai-layout-design.d.ts +107 -0
- package/src/lib/layout-language/ai-layout-design.js +197 -0
- package/src/lib/layout-language/ai-layout-design.js.map +1 -0
- package/src/lib/layout-language/ai-layout-frame.d.ts +6 -0
- package/src/lib/layout-language/ai-layout-frame.js +22 -6
- package/src/lib/layout-language/ai-layout-frame.js.map +1 -1
- package/src/lib/layout-language/ai-layout-language.d.ts +2 -0
- package/src/lib/layout-language/ai-layout-language.js +15 -1
- package/src/lib/layout-language/ai-layout-language.js.map +1 -1
- package/src/lib/layout-language/ai-layout-links.d.ts +11 -0
- package/src/lib/layout-language/ai-layout-links.js +4 -0
- package/src/lib/layout-language/ai-layout-links.js.map +1 -1
- package/src/lib/layout-language/ai-layout-pictures.d.ts +11 -5
- package/src/lib/layout-language/ai-layout-pictures.js +80 -17
- package/src/lib/layout-language/ai-layout-pictures.js.map +1 -1
- package/src/lib/model/ai-job-notice.d.ts +9 -1
- package/src/lib/model/ai-job-notice.js +23 -0
- package/src/lib/model/ai-job-notice.js.map +1 -1
- package/src/lib/model/ai-plan-capabilities.d.ts +23 -0
- package/src/lib/model/ai-plan-capabilities.js +5 -0
- package/src/lib/model/ai-plan-capabilities.js.map +1 -1
- package/src/lib/model/ai-site-build-progress.d.ts +12 -2
- package/src/lib/model/ai-site-build-progress.js +78 -4
- package/src/lib/model/ai-site-build-progress.js.map +1 -1
- package/src/lib/model/ai-site-job.d.ts +209 -22
- package/src/lib/model/ai-site-job.js +247 -20
- package/src/lib/model/ai-site-job.js.map +1 -1
- package/src/lib/model/ai-site-kinds.js +6 -1
- package/src/lib/model/ai-site-kinds.js.map +1 -1
- package/src/lib/model/ai-site-look.d.ts +21 -0
- package/src/lib/model/ai-site-look.js +63 -14
- package/src/lib/model/ai-site-look.js.map +1 -1
- package/src/lib/operator-alerts.d.ts +18 -0
- package/src/lib/operator-alerts.js +42 -0
- package/src/lib/operator-alerts.js.map +1 -1
- package/src/lib/runtime/ai-doctrine-validators.d.ts +39 -1
- package/src/lib/runtime/ai-doctrine-validators.js +236 -6
- package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
- package/src/lib/runtime/ai-doctrine.js +5 -3
- package/src/lib/runtime/ai-doctrine.js.map +1 -1
- package/src/lib/runtime/ai-palette.generated.d.ts +2 -0
- package/src/lib/runtime/ai-palette.generated.js +18 -2
- package/src/lib/runtime/ai-palette.generated.js.map +1 -1
- package/src/lib/server/ai-jobs-resume.js.map +1 -1
- package/src/lib/server/ai-jobs-route.js +15 -3
- package/src/lib/server/ai-jobs-route.js.map +1 -1
- package/src/lib/server/assist-edit.js +5 -2
- package/src/lib/server/assist-edit.js.map +1 -1
- package/src/lib/usage/free-ai-credits-left.d.ts +54 -0
- package/src/lib/usage/free-ai-credits-left.js +62 -0
- package/src/lib/usage/free-ai-credits-left.js.map +1 -0
|
@@ -49,7 +49,7 @@ export declare const AI_SITE_PAGES: {
|
|
|
49
49
|
* and the one page the brief most needs. A product decision, not a figure the
|
|
50
50
|
* wall derives — the wall's proof (`jobs/ai-job-free-site.spec.ts`) shows a
|
|
51
51
|
* plan and a build of this many pages fit the Free taste with room for one
|
|
52
|
-
* retried
|
|
52
|
+
* retried page.
|
|
53
53
|
*/
|
|
54
54
|
export declare const AI_SITE_FREE_PAGES: {
|
|
55
55
|
readonly min: 1;
|
|
@@ -86,50 +86,237 @@ export interface AiFreeSiteWorstCase {
|
|
|
86
86
|
plan: number;
|
|
87
87
|
/** The site's look (AGL-3660): its one answer at its ceiling, on the fast tier. */
|
|
88
88
|
look: number;
|
|
89
|
-
/**
|
|
89
|
+
/**
|
|
90
|
+
* The layout a site with none has built first (AGL-3660): its header and
|
|
91
|
+
* footer as ONE answer in the layout language, writing its cached prefix,
|
|
92
|
+
* at the most a live frame answer wrote, and never under what the live
|
|
93
|
+
* guided start's layout step metered.
|
|
94
|
+
*/
|
|
90
95
|
layout: number;
|
|
91
|
-
/**
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
|
|
96
|
+
/** The saved form a guided start makes (AGL-3596), at what the live guided start's form step metered. */
|
|
97
|
+
form: number;
|
|
98
|
+
/**
|
|
99
|
+
* A language page's answer before its sections (AGL-3660): its cached
|
|
100
|
+
* prefix written, its turn, and the whole room its ceiling keeps for
|
|
101
|
+
* thinking. A page is ONE answer, not a pass a section.
|
|
102
|
+
*/
|
|
103
|
+
page: number;
|
|
104
|
+
/** Each planned section's words in that answer, at the most a section is written in. */
|
|
105
|
+
section: number;
|
|
106
|
+
/**
|
|
107
|
+
* Room for one page asked again: the answer once more, reading its cached
|
|
108
|
+
* prefix, with its thinking room and a full page of sections.
|
|
109
|
+
*/
|
|
110
|
+
retry: number;
|
|
95
111
|
/** A page's listing. */
|
|
96
112
|
listing: number;
|
|
97
113
|
}
|
|
98
114
|
/**
|
|
99
|
-
* The Free site start's wall at its worst (AGL-3594
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
* the
|
|
103
|
-
*
|
|
115
|
+
* The Free site start's wall at its worst (AGL-3594, re-derived for the
|
|
116
|
+
* layout language in AGL-3660): the plan and the look as
|
|
117
|
+
* `jobs/ai-job-free-site.spec.ts` derives them from their requests at their
|
|
118
|
+
* ceilings; the layout, form and pages from the language requests as they
|
|
119
|
+
* stand, priced as each field says. The spec fails when a figure it derives
|
|
104
120
|
* moves and this does not.
|
|
105
121
|
*/
|
|
106
122
|
export declare const AI_FREE_SITE_WORST_CASE_CREDITS: Readonly<AiFreeSiteWorstCase>;
|
|
107
123
|
/**
|
|
108
|
-
* The most sections a Free site
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
* page's first pass is counted as a cache write, which errs dear.
|
|
124
|
+
* The most sections a Free site plans across its pages (AGL-3660), whatever
|
|
125
|
+
* the wall leaves: a full home of five and a page of two or three. A product
|
|
126
|
+
* figure, so the spend a site EXPECTS stays near what it measured; the wall
|
|
127
|
+
* proves it fits.
|
|
113
128
|
*/
|
|
114
|
-
export declare
|
|
129
|
+
export declare const AI_FREE_SITE_MAX_SECTIONS = 8;
|
|
130
|
+
/** What a Free site start builds before its sections: the layouts it builds first, its forms and its pages. */
|
|
131
|
+
export interface AiFreeSiteCreations {
|
|
115
132
|
layouts: number;
|
|
116
133
|
pages: number;
|
|
117
|
-
|
|
134
|
+
/** The saved forms it makes; a guided start makes one where it places a form. */
|
|
135
|
+
forms: number;
|
|
136
|
+
}
|
|
137
|
+
/** A Free site start's credits at its worst for this many sections, its room for one retried page included. */
|
|
138
|
+
export declare function aiFreeSiteWorstCaseCredits(creations: AiFreeSiteCreations, sections: number, credits?: Readonly<AiFreeSiteWorstCase>): number;
|
|
139
|
+
/**
|
|
140
|
+
* The most sections a Free site's plan fits in the Free taste at its worst,
|
|
141
|
+
* across all its pages (AGL-3660): the plan, the look, the layout and form it
|
|
142
|
+
* builds first, each page's answer and listing, ROOM FOR ONE RETRIED PAGE,
|
|
143
|
+
* and as many sections as the rest pays for — at most
|
|
144
|
+
* `AI_FREE_SITE_MAX_SECTIONS`, and at most what the pages hold. None when the
|
|
145
|
+
* rest is already past the wall.
|
|
146
|
+
*/
|
|
147
|
+
export declare function aiFreeSiteSectionsWithin(creations: AiFreeSiteCreations, taste: number, credits?: Readonly<AiFreeSiteWorstCase>): number;
|
|
118
148
|
/** Sections a Free site's page is assumed to hold before its plan names them. */
|
|
119
|
-
export declare const AI_FREE_SITE_NOMINAL_SECTIONS =
|
|
149
|
+
export declare const AI_FREE_SITE_NOMINAL_SECTIONS = 4;
|
|
120
150
|
/**
|
|
121
151
|
* About what a Free site start of this many pages costs at its worst, in
|
|
122
|
-
* credits, for the dialog that asks for one: the
|
|
123
|
-
*
|
|
124
|
-
*
|
|
152
|
+
* credits, for the dialog that asks for one: the plan, its look, the layout
|
|
153
|
+
* and form a new site builds, each page's answer and listing, and its
|
|
154
|
+
* sections at the nominal count, from the figures the wall is proven with
|
|
155
|
+
* rather than the nominal credits a paid estimate counts. No retry room: the
|
|
156
|
+
* dialog says what a start costs, not what the wall holds back.
|
|
125
157
|
*/
|
|
126
158
|
export declare function aiFreeSiteCreditEstimate(pages: number, credits?: Readonly<AiFreeSiteWorstCase>): number;
|
|
159
|
+
/**
|
|
160
|
+
* What a Free workspace has left of its Free AI credits this month: the less
|
|
161
|
+
* of its own band's balance and its owner's, since the owner's allowance is
|
|
162
|
+
* shared by every Free workspace they hold (AGL-2925) and the reservation
|
|
163
|
+
* refuses at whichever wall is reached first. `resetsOn` is the UTC day the
|
|
164
|
+
* month — and with it both balances — rolls over, `YYYY-MM-DD`.
|
|
165
|
+
*/
|
|
166
|
+
export interface AiFreeCreditsLeft {
|
|
167
|
+
left: number;
|
|
168
|
+
total: number;
|
|
169
|
+
resetsOn: string;
|
|
170
|
+
}
|
|
171
|
+
/** The first UTC day of the month after `now`, `YYYY-MM-DD`: when the Free credits come back. */
|
|
172
|
+
export declare function aiFreeCreditsResetOn(now: Date): string;
|
|
173
|
+
/** A reset day as a person reads it: "November 1". */
|
|
174
|
+
export declare function aiFreeCreditsResetLabel(resetsOn: string): string;
|
|
175
|
+
/**
|
|
176
|
+
* The credits a Free site start needs and does not have (AGL-3660), or `null`
|
|
177
|
+
* when what is left covers it — or when nothing is known about what is left,
|
|
178
|
+
* since the reservation still refuses at the wall. The figure is the one the
|
|
179
|
+
* dialog quotes (`aiFreeSiteCreditEstimate`), so the number a person is shown
|
|
180
|
+
* is the number they are held to.
|
|
181
|
+
*/
|
|
182
|
+
export declare function aiFreeSiteShortfall(credits: Pick<AiFreeCreditsLeft, 'left'> | null | undefined, pages: number): {
|
|
183
|
+
needed: number;
|
|
184
|
+
left: number;
|
|
185
|
+
} | null;
|
|
186
|
+
/** Why a Free site start is refused before it spends: what is left, when it comes back, and the way on. */
|
|
187
|
+
export declare function aiFreeSiteShortfallText(shortfall: {
|
|
188
|
+
needed: number;
|
|
189
|
+
left: number;
|
|
190
|
+
}, resetsOn: string): string;
|
|
127
191
|
/**
|
|
128
192
|
* The most sections one scaffolded page may hold. The plan model allows more
|
|
129
193
|
* for a page job, which builds one page; a scaffold builds eight, and each
|
|
130
194
|
* section is a pass of its own.
|
|
131
195
|
*/
|
|
132
196
|
export declare const AI_SITE_MAX_SECTIONS = 8;
|
|
197
|
+
/**
|
|
198
|
+
* The fewest sections a site start's home page is planned with (AGL-3660).
|
|
199
|
+
* A new site is a full website (Zach, 2026-10-04): its home reads as one — a
|
|
200
|
+
* hero, then bands such as the offer, why us, social proof, and a closing
|
|
201
|
+
* call to action. A plan is an outline and nothing asked for more than a
|
|
202
|
+
* ceiling, so a live Free yoga start (2026-10-07) planned its home with two
|
|
203
|
+
* sections. The plan step names this count in the plan's turn and re-asks a
|
|
204
|
+
* first answer whose home is under it (`aiSiteThinHomeViolations`).
|
|
205
|
+
*/
|
|
206
|
+
export declare const AI_SITE_HOME_MIN_SECTIONS = 5;
|
|
207
|
+
/**
|
|
208
|
+
* The blog a paid guided start writes its first posts into (AGL-3676), and
|
|
209
|
+
* the addresses it may answer at, in order of preference: the first one no
|
|
210
|
+
* planned page takes.
|
|
211
|
+
*/
|
|
212
|
+
export declare const AI_SITE_BLOG_NAME = "Blog";
|
|
213
|
+
export declare const AI_SITE_BLOG_SLUGS: readonly ["blog", "posts", "journal", "articles", "writing", "stories"];
|
|
214
|
+
/** The address the blog answers at, beside these planned pages: the first of `AI_SITE_BLOG_SLUGS` none of them takes. */
|
|
215
|
+
export declare function aiSiteBlogSlug(screens: ReadonlyArray<{
|
|
216
|
+
slug: string;
|
|
217
|
+
}>): string;
|
|
218
|
+
/** The header's link to the blog (AGL-3660): a path, not a page, since the blog is the posts collection. */
|
|
219
|
+
export declare const AI_SITE_BLOG_NAV_ID = "aiSiteBlog";
|
|
220
|
+
/** The nav entry a site whose start writes posts links its blog by. */
|
|
221
|
+
export declare function aiSiteBlogNavPage(screens: ReadonlyArray<{
|
|
222
|
+
slug: string;
|
|
223
|
+
}>): {
|
|
224
|
+
id: string;
|
|
225
|
+
label: string;
|
|
226
|
+
slug: string;
|
|
227
|
+
href: string;
|
|
228
|
+
};
|
|
229
|
+
/** The code a planned page that stands in for the written blog is re-asked under. */
|
|
230
|
+
export declare const AI_SITE_BLOG_PAGE_CODE = "plan-blog-page-duplicate";
|
|
231
|
+
/**
|
|
232
|
+
* A site plan's pages that stand in for the blog its start writes (AGL-3660):
|
|
233
|
+
* the live Slow Roads start (2026-10-08) planned an "Articles" page of
|
|
234
|
+
* featured cards beside the posts it wrote at /blog, and the header linked
|
|
235
|
+
* Articles and never the blog. A page whose address is one of the blog's, or
|
|
236
|
+
* whose name says it lists posts, duplicates it; the home page never does.
|
|
237
|
+
*/
|
|
238
|
+
export declare function aiSiteBlogStandInViolations(plan: Pick<AiBuildPlan, 'screens'>): Array<{
|
|
239
|
+
rule: null;
|
|
240
|
+
code: string;
|
|
241
|
+
message: string;
|
|
242
|
+
paths: string[];
|
|
243
|
+
}>;
|
|
244
|
+
/** The code a site plan's work section planned without its pieces is re-asked under. */
|
|
245
|
+
export declare const AI_SITE_EMPTY_GALLERY_CODE = "plan-empty-gallery";
|
|
246
|
+
/** The pieces a section that shows the work is planned with (AGL-3660). */
|
|
247
|
+
export declare const AI_SITE_GALLERY_MIN_ITEMS = 3;
|
|
248
|
+
export declare const AI_SITE_GALLERY_MAX_ITEMS = 6;
|
|
249
|
+
/** Whether a planned section's name says it shows the work. */
|
|
250
|
+
export declare function aiSiteSectionShowsWork(name: string): boolean;
|
|
251
|
+
/** The sentence a site plan's turn states about a section that shows the work. */
|
|
252
|
+
export declare const AI_SITE_GALLERY_SENTENCE = "A section that shows the work \u2014 a gallery, portfolio, collection or selected works \u2014 counts its pieces in its items: 3 to 6.";
|
|
253
|
+
/**
|
|
254
|
+
* A site plan's sections that show the work with fewer than three pieces
|
|
255
|
+
* (AGL-3660). The live Juniper Clay start (2026-10-08) planned its Portfolio
|
|
256
|
+
* page's "Works Gallery" with no items, and the page's works lost their place
|
|
257
|
+
* to an inquiry form: a page about the work showed none.
|
|
258
|
+
*/
|
|
259
|
+
export declare function aiSiteEmptyGalleryViolations(plan: Pick<AiBuildPlan, 'screens'>): Array<{
|
|
260
|
+
rule: null;
|
|
261
|
+
code: string;
|
|
262
|
+
message: string;
|
|
263
|
+
paths: string[];
|
|
264
|
+
}>;
|
|
265
|
+
/** The violation a site plan whose home page is under its fewest sections is re-asked under. */
|
|
266
|
+
export declare const AI_SITE_THIN_HOME_CODE = "plan-thin-home";
|
|
267
|
+
/** What a site plan's home is composed of, in the words the plan's turn and its re-ask both use. */
|
|
268
|
+
export declare const AI_SITE_HOME_BANDS = "a hero first, then bands such as services or the offer, about or why us, and testimonials or other social proof, and a closing call to action or contact band last";
|
|
269
|
+
/**
|
|
270
|
+
* The fewest sections this site start's home is held to (AGL-3660): five, or
|
|
271
|
+
* 0 — no minimum — where the Free wall cannot pay for a full home beside the
|
|
272
|
+
* other pages. `across` is the sections the Free taste fits across `pages`
|
|
273
|
+
* pages, each other page keeping at least one; `null` is a paid start, which
|
|
274
|
+
* no wall shares out.
|
|
275
|
+
*
|
|
276
|
+
* ⛔ NEVER a lowered minimum. A Free guided start creates its site empty and
|
|
277
|
+
* plans its layout, and the wall then fits 4 sections across two pages. Told
|
|
278
|
+
* "a home of at least 3" beside that, the live yoga start of 2026-10-08
|
|
279
|
+
* planned 4 + 3 and then 4 + 2, was refused for the wall twice, and stopped:
|
|
280
|
+
* a home asked for in bands the wall cannot pay for is a start that fails.
|
|
281
|
+
*/
|
|
282
|
+
export declare function aiSiteHomeMinSections(input: {
|
|
283
|
+
pages: number;
|
|
284
|
+
across: number | null;
|
|
285
|
+
}): number;
|
|
286
|
+
/**
|
|
287
|
+
* What a site start's plan is told to BUILD with (AGL-3660): the sections are
|
|
288
|
+
* a budget to use, never a ceiling to stay under. Told only "at most 8", the
|
|
289
|
+
* live prod start of 2026-10-08 planned a home of two sections, and Zach: "that
|
|
290
|
+
* didn't mean do as little as possible." On the Free taste the sentence shares
|
|
291
|
+
* the wall's sections out — a home of five or six, the other page two or
|
|
292
|
+
* three, about seven or eight in all; a paid start's home is asked to be rich.
|
|
293
|
+
* `min` is the home's fewest (`aiSiteHomeMinSections`), 0 where the plan
|
|
294
|
+
* plans no home or the wall cannot pay for a full one.
|
|
295
|
+
*/
|
|
296
|
+
export declare function aiSiteFullPlanSentence(input: {
|
|
297
|
+
pages: number;
|
|
298
|
+
across: number | null;
|
|
299
|
+
min: number;
|
|
300
|
+
}): string;
|
|
301
|
+
/** Whether a planned page is the site's home: the one at `/`. */
|
|
302
|
+
export declare function aiSitePlanIsHome(screen: {
|
|
303
|
+
slug: string;
|
|
304
|
+
}): boolean;
|
|
305
|
+
/**
|
|
306
|
+
* A site plan's home page under its fewest sections (AGL-3660), as the plan
|
|
307
|
+
* step's re-ask names it. A plan with no page at `/` — the owner's own home
|
|
308
|
+
* stays — has no home to hold. `across` is the Free wall's section count, so
|
|
309
|
+
* the re-ask says where the sections come from rather than tripping the wall.
|
|
310
|
+
*/
|
|
311
|
+
export declare function aiSiteThinHomeViolations(plan: Pick<AiBuildPlan, 'screens'>, options: {
|
|
312
|
+
min: number;
|
|
313
|
+
across: number | null;
|
|
314
|
+
}): Array<{
|
|
315
|
+
rule: null;
|
|
316
|
+
code: string;
|
|
317
|
+
message: string;
|
|
318
|
+
paths: string[];
|
|
319
|
+
}>;
|
|
133
320
|
/**
|
|
134
321
|
* Sections a page is assumed to hold before a plan names them, for the
|
|
135
322
|
* estimate a member reads when they ask for a scaffold. A declared
|
|
@@ -49,7 +49,7 @@ import { AI_PAGE_CREATE_KINDS } from "./ai-page-job.js";
|
|
|
49
49
|
* and the one page the brief most needs. A product decision, not a figure the
|
|
50
50
|
* wall derives — the wall's proof (`jobs/ai-job-free-site.spec.ts`) shows a
|
|
51
51
|
* plan and a build of this many pages fit the Free taste with room for one
|
|
52
|
-
* retried
|
|
52
|
+
* retried page.
|
|
53
53
|
*/ export const AI_SITE_FREE_PAGES = {
|
|
54
54
|
min: 1,
|
|
55
55
|
max: 2
|
|
@@ -75,47 +75,274 @@ import { AI_PAGE_CREATE_KINDS } from "./ai-page-job.js";
|
|
|
75
75
|
paid: 4000
|
|
76
76
|
};
|
|
77
77
|
/**
|
|
78
|
-
* The Free site start's wall at its worst (AGL-3594
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
* the
|
|
82
|
-
*
|
|
78
|
+
* The Free site start's wall at its worst (AGL-3594, re-derived for the
|
|
79
|
+
* layout language in AGL-3660): the plan and the look as
|
|
80
|
+
* `jobs/ai-job-free-site.spec.ts` derives them from their requests at their
|
|
81
|
+
* ceilings; the layout, form and pages from the language requests as they
|
|
82
|
+
* stand, priced as each field says. The spec fails when a figure it derives
|
|
83
83
|
* moves and this does not.
|
|
84
84
|
*/ export const AI_FREE_SITE_WORST_CASE_CREDITS = {
|
|
85
85
|
plan: 35,
|
|
86
86
|
look: 7,
|
|
87
|
-
layout:
|
|
88
|
-
|
|
89
|
-
|
|
87
|
+
layout: 23,
|
|
88
|
+
form: 29,
|
|
89
|
+
page: 43,
|
|
90
|
+
section: 4,
|
|
91
|
+
retry: 64,
|
|
90
92
|
listing: 3
|
|
91
93
|
};
|
|
94
|
+
/**
|
|
95
|
+
* The most sections a Free site plans across its pages (AGL-3660), whatever
|
|
96
|
+
* the wall leaves: a full home of five and a page of two or three. A product
|
|
97
|
+
* figure, so the spend a site EXPECTS stays near what it measured; the wall
|
|
98
|
+
* proves it fits.
|
|
99
|
+
*/ export const AI_FREE_SITE_MAX_SECTIONS = 8;
|
|
100
|
+
/** A Free site start's credits at its worst for this many sections, its room for one retried page included. */ export function aiFreeSiteWorstCaseCredits(creations, sections, credits = AI_FREE_SITE_WORST_CASE_CREDITS) {
|
|
101
|
+
const pages = Math.max(1, Math.floor(creations.pages));
|
|
102
|
+
return credits.plan + credits.look + creations.layouts * credits.layout + creations.forms * credits.form + pages * (credits.page + credits.listing) + sections * credits.section + credits.retry;
|
|
103
|
+
}
|
|
92
104
|
/**
|
|
93
105
|
* The most sections a Free site's plan fits in the Free taste at its worst,
|
|
94
|
-
* across all its pages: the plan, the look, the
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
106
|
+
* across all its pages (AGL-3660): the plan, the look, the layout and form it
|
|
107
|
+
* builds first, each page's answer and listing, ROOM FOR ONE RETRIED PAGE,
|
|
108
|
+
* and as many sections as the rest pays for — at most
|
|
109
|
+
* `AI_FREE_SITE_MAX_SECTIONS`, and at most what the pages hold. None when the
|
|
110
|
+
* rest is already past the wall.
|
|
98
111
|
*/ export function aiFreeSiteSectionsWithin(creations, taste, credits = AI_FREE_SITE_WORST_CASE_CREDITS) {
|
|
99
112
|
const pages = Math.max(1, Math.floor(creations.pages));
|
|
100
|
-
const before =
|
|
113
|
+
const before = aiFreeSiteWorstCaseCredits(creations, 0, credits);
|
|
101
114
|
if (before > taste) return 0;
|
|
102
|
-
|
|
115
|
+
const paid = credits.section > 0 ? Math.floor((taste - before) / credits.section) : Number.POSITIVE_INFINITY;
|
|
116
|
+
return Math.min(AI_FREE_SITE_MAX_SECTIONS, pages * AI_SITE_MAX_SECTIONS, paid);
|
|
103
117
|
}
|
|
104
|
-
/** Sections a Free site's page is assumed to hold before its plan names them. */ export const AI_FREE_SITE_NOMINAL_SECTIONS =
|
|
118
|
+
/** Sections a Free site's page is assumed to hold before its plan names them. */ export const AI_FREE_SITE_NOMINAL_SECTIONS = 4;
|
|
105
119
|
/**
|
|
106
120
|
* About what a Free site start of this many pages costs at its worst, in
|
|
107
|
-
* credits, for the dialog that asks for one: the
|
|
108
|
-
*
|
|
109
|
-
*
|
|
121
|
+
* credits, for the dialog that asks for one: the plan, its look, the layout
|
|
122
|
+
* and form a new site builds, each page's answer and listing, and its
|
|
123
|
+
* sections at the nominal count, from the figures the wall is proven with
|
|
124
|
+
* rather than the nominal credits a paid estimate counts. No retry room: the
|
|
125
|
+
* dialog says what a start costs, not what the wall holds back.
|
|
110
126
|
*/ export function aiFreeSiteCreditEstimate(pages, credits = AI_FREE_SITE_WORST_CASE_CREDITS) {
|
|
111
127
|
const count = Math.max(1, Math.floor(pages));
|
|
112
|
-
return
|
|
128
|
+
return aiFreeSiteWorstCaseCredits({
|
|
129
|
+
layouts: 1,
|
|
130
|
+
forms: 1,
|
|
131
|
+
pages: count
|
|
132
|
+
}, count * AI_FREE_SITE_NOMINAL_SECTIONS, credits) - credits.retry;
|
|
133
|
+
}
|
|
134
|
+
/** The first UTC day of the month after `now`, `YYYY-MM-DD`: when the Free credits come back. */ export function aiFreeCreditsResetOn(now) {
|
|
135
|
+
const next = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + 1, 1));
|
|
136
|
+
return next.toISOString().slice(0, 10);
|
|
137
|
+
}
|
|
138
|
+
/** A reset day as a person reads it: "November 1". */ export function aiFreeCreditsResetLabel(resetsOn) {
|
|
139
|
+
const at = new Date(`${resetsOn}T00:00:00.000Z`);
|
|
140
|
+
return Number.isFinite(at.getTime()) ? at.toLocaleDateString('en-US', {
|
|
141
|
+
month: 'long',
|
|
142
|
+
day: 'numeric',
|
|
143
|
+
timeZone: 'UTC'
|
|
144
|
+
}) : 'the first of next month';
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* The credits a Free site start needs and does not have (AGL-3660), or `null`
|
|
148
|
+
* when what is left covers it — or when nothing is known about what is left,
|
|
149
|
+
* since the reservation still refuses at the wall. The figure is the one the
|
|
150
|
+
* dialog quotes (`aiFreeSiteCreditEstimate`), so the number a person is shown
|
|
151
|
+
* is the number they are held to.
|
|
152
|
+
*/ export function aiFreeSiteShortfall(credits, pages) {
|
|
153
|
+
if (!credits || !Number.isFinite(credits.left)) return null;
|
|
154
|
+
const needed = aiFreeSiteCreditEstimate(pages);
|
|
155
|
+
const left = Math.max(0, Math.floor(credits.left));
|
|
156
|
+
return left < needed ? {
|
|
157
|
+
needed,
|
|
158
|
+
left
|
|
159
|
+
} : null;
|
|
160
|
+
}
|
|
161
|
+
/** Why a Free site start is refused before it spends: what is left, when it comes back, and the way on. */ export function aiFreeSiteShortfallText(shortfall, resetsOn) {
|
|
162
|
+
const left = shortfall.left === 1 ? '1 is' : `${shortfall.left.toLocaleString('en-US')} are`;
|
|
163
|
+
return `Building this site can take up to about ${shortfall.needed.toLocaleString('en-US')} AI credits, ` + `and only ${left} left of your free AI credits this month. They are shared by all your Free ` + `workspaces and reset on ${aiFreeCreditsResetLabel(resetsOn)}. Upgrade this workspace to build ` + 'your site now, or start from the starter site and try AI again after the reset.';
|
|
113
164
|
}
|
|
114
165
|
/**
|
|
115
166
|
* The most sections one scaffolded page may hold. The plan model allows more
|
|
116
167
|
* for a page job, which builds one page; a scaffold builds eight, and each
|
|
117
168
|
* section is a pass of its own.
|
|
118
169
|
*/ export const AI_SITE_MAX_SECTIONS = 8;
|
|
170
|
+
/**
|
|
171
|
+
* The fewest sections a site start's home page is planned with (AGL-3660).
|
|
172
|
+
* A new site is a full website (Zach, 2026-10-04): its home reads as one — a
|
|
173
|
+
* hero, then bands such as the offer, why us, social proof, and a closing
|
|
174
|
+
* call to action. A plan is an outline and nothing asked for more than a
|
|
175
|
+
* ceiling, so a live Free yoga start (2026-10-07) planned its home with two
|
|
176
|
+
* sections. The plan step names this count in the plan's turn and re-asks a
|
|
177
|
+
* first answer whose home is under it (`aiSiteThinHomeViolations`).
|
|
178
|
+
*/ export const AI_SITE_HOME_MIN_SECTIONS = 5;
|
|
179
|
+
/**
|
|
180
|
+
* The blog a paid guided start writes its first posts into (AGL-3676), and
|
|
181
|
+
* the addresses it may answer at, in order of preference: the first one no
|
|
182
|
+
* planned page takes.
|
|
183
|
+
*/ export const AI_SITE_BLOG_NAME = 'Blog';
|
|
184
|
+
export const AI_SITE_BLOG_SLUGS = [
|
|
185
|
+
'blog',
|
|
186
|
+
'posts',
|
|
187
|
+
'journal',
|
|
188
|
+
'articles',
|
|
189
|
+
'writing',
|
|
190
|
+
'stories'
|
|
191
|
+
];
|
|
192
|
+
/** A planned page's first path segment, lower-cased: `/journal/2026` → `journal`. */ const firstSegment = (slug)=>slug.trim().replace(/^\/+/, '').split('/')[0].toLowerCase();
|
|
193
|
+
/** The address the blog answers at, beside these planned pages: the first of `AI_SITE_BLOG_SLUGS` none of them takes. */ export function aiSiteBlogSlug(screens) {
|
|
194
|
+
var _AI_SITE_BLOG_SLUGS_find;
|
|
195
|
+
const taken = new Set(screens.map((screen)=>firstSegment(screen.slug)).filter(Boolean));
|
|
196
|
+
return (_AI_SITE_BLOG_SLUGS_find = AI_SITE_BLOG_SLUGS.find((slug)=>!taken.has(slug))) != null ? _AI_SITE_BLOG_SLUGS_find : AI_SITE_BLOG_SLUGS[0];
|
|
197
|
+
}
|
|
198
|
+
/** The header's link to the blog (AGL-3660): a path, not a page, since the blog is the posts collection. */ export const AI_SITE_BLOG_NAV_ID = 'aiSiteBlog';
|
|
199
|
+
/** The nav entry a site whose start writes posts links its blog by. */ export function aiSiteBlogNavPage(screens) {
|
|
200
|
+
const slug = aiSiteBlogSlug(screens);
|
|
201
|
+
return {
|
|
202
|
+
id: AI_SITE_BLOG_NAV_ID,
|
|
203
|
+
label: AI_SITE_BLOG_NAME,
|
|
204
|
+
slug: `/${slug}`,
|
|
205
|
+
href: `/${slug}`
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
/** The code a planned page that stands in for the written blog is re-asked under. */ export const AI_SITE_BLOG_PAGE_CODE = 'plan-blog-page-duplicate';
|
|
209
|
+
/** A page's name that says it lists the posts. */ const BLOG_PAGE_WORDS = /\b(blog|posts?|articles?|journal|writing|stories|news)\b/i;
|
|
210
|
+
/**
|
|
211
|
+
* A site plan's pages that stand in for the blog its start writes (AGL-3660):
|
|
212
|
+
* the live Slow Roads start (2026-10-08) planned an "Articles" page of
|
|
213
|
+
* featured cards beside the posts it wrote at /blog, and the header linked
|
|
214
|
+
* Articles and never the blog. A page whose address is one of the blog's, or
|
|
215
|
+
* whose name says it lists posts, duplicates it; the home page never does.
|
|
216
|
+
*/ export function aiSiteBlogStandInViolations(plan) {
|
|
217
|
+
const blogSlugs = new Set(AI_SITE_BLOG_SLUGS);
|
|
218
|
+
const standIns = plan.screens.flatMap((screen, index)=>{
|
|
219
|
+
if (aiSitePlanIsHome(screen)) return [];
|
|
220
|
+
const segment = firstSegment(screen.slug);
|
|
221
|
+
return blogSlugs.has(segment) || BLOG_PAGE_WORDS.test(screen.title) ? [
|
|
222
|
+
{
|
|
223
|
+
screen,
|
|
224
|
+
index
|
|
225
|
+
}
|
|
226
|
+
] : [];
|
|
227
|
+
});
|
|
228
|
+
if (!standIns.length) return [];
|
|
229
|
+
const names = standIns.map(({ screen })=>`"${screen.title}" at ${screen.slug}`).join(', ');
|
|
230
|
+
return [
|
|
231
|
+
{
|
|
232
|
+
rule: null,
|
|
233
|
+
code: AI_SITE_BLOG_PAGE_CODE,
|
|
234
|
+
message: `${names} ${standIns.length === 1 ? 'stands' : 'stand'} in for the blog this site already gets: its first posts are written at /blog, and the header links it. Take ${standIns.length === 1 ? 'that page' : 'those pages'} out, and plan another page the brief needs, or feature the posts in a section of the home page.`,
|
|
235
|
+
paths: standIns.map(({ index })=>`screens[${index}]`)
|
|
236
|
+
}
|
|
237
|
+
];
|
|
238
|
+
}
|
|
239
|
+
/** The code a site plan's work section planned without its pieces is re-asked under. */ export const AI_SITE_EMPTY_GALLERY_CODE = 'plan-empty-gallery';
|
|
240
|
+
/** The pieces a section that shows the work is planned with (AGL-3660). */ export const AI_SITE_GALLERY_MIN_ITEMS = 3;
|
|
241
|
+
export const AI_SITE_GALLERY_MAX_ITEMS = 6;
|
|
242
|
+
/**
|
|
243
|
+
* A planned section that shows the work: a gallery, a portfolio, a
|
|
244
|
+
* collection, selected or featured work. "Galleries" — an audience, as in
|
|
245
|
+
* "Why galleries choose us" — and "how we work" are not.
|
|
246
|
+
*/ const GALLERY_SECTION = /\b(gallery|portfolio|collections?|lookbook|showcase|works|projects|(?:selected|featured|recent|our|past) (?:work|pieces|projects))\b/i;
|
|
247
|
+
/** A section that only opens, introduces or closes a page, whatever page it is on: "Portfolio Hero". */ const FRAMING_SECTION = /\b(hero|intro(?:duction)?|banner|header|cta|call to action|contact|inquiry|enquiry|about)\b/i;
|
|
248
|
+
/** Whether a planned section's name says it shows the work. */ export function aiSiteSectionShowsWork(name) {
|
|
249
|
+
return GALLERY_SECTION.test(name) && !FRAMING_SECTION.test(name);
|
|
250
|
+
}
|
|
251
|
+
/** The sentence a site plan's turn states about a section that shows the work. */ export const AI_SITE_GALLERY_SENTENCE = `A section that shows the work — a gallery, portfolio, collection or selected works — counts its pieces in its items: ${AI_SITE_GALLERY_MIN_ITEMS} to ${AI_SITE_GALLERY_MAX_ITEMS}.`;
|
|
252
|
+
/**
|
|
253
|
+
* A site plan's sections that show the work with fewer than three pieces
|
|
254
|
+
* (AGL-3660). The live Juniper Clay start (2026-10-08) planned its Portfolio
|
|
255
|
+
* page's "Works Gallery" with no items, and the page's works lost their place
|
|
256
|
+
* to an inquiry form: a page about the work showed none.
|
|
257
|
+
*/ export function aiSiteEmptyGalleryViolations(plan) {
|
|
258
|
+
const thin = plan.screens.flatMap((screen, screenIndex)=>screen.sections.flatMap((section, sectionIndex)=>aiSiteSectionShowsWork(section.name) && section.items < AI_SITE_GALLERY_MIN_ITEMS ? [
|
|
259
|
+
{
|
|
260
|
+
name: `"${section.name}" on ${screen.title}`,
|
|
261
|
+
path: `screens[${screenIndex}].sections[${sectionIndex}].items`
|
|
262
|
+
}
|
|
263
|
+
] : []));
|
|
264
|
+
if (!thin.length) return [];
|
|
265
|
+
const names = thin.map((entry)=>entry.name).join(', ');
|
|
266
|
+
return [
|
|
267
|
+
{
|
|
268
|
+
rule: null,
|
|
269
|
+
code: AI_SITE_EMPTY_GALLERY_CODE,
|
|
270
|
+
message: `${names} ${thin.length === 1 ? 'shows' : 'show'} the work with fewer than ${AI_SITE_GALLERY_MIN_ITEMS} pieces. Plan ${thin.length === 1 ? 'it' : 'each'} with ${AI_SITE_GALLERY_MIN_ITEMS} to ${AI_SITE_GALLERY_MAX_ITEMS} items, one for each piece it shows.`,
|
|
271
|
+
paths: thin.map((entry)=>entry.path)
|
|
272
|
+
}
|
|
273
|
+
];
|
|
274
|
+
}
|
|
275
|
+
/** The violation a site plan whose home page is under its fewest sections is re-asked under. */ export const AI_SITE_THIN_HOME_CODE = 'plan-thin-home';
|
|
276
|
+
/** What a site plan's home is composed of, in the words the plan's turn and its re-ask both use. */ export const AI_SITE_HOME_BANDS = 'a hero first, then bands such as services or the offer, about or why us, and testimonials or other social proof, and a closing call to action or contact band last';
|
|
277
|
+
/**
|
|
278
|
+
* The fewest sections this site start's home is held to (AGL-3660): five, or
|
|
279
|
+
* 0 — no minimum — where the Free wall cannot pay for a full home beside the
|
|
280
|
+
* other pages. `across` is the sections the Free taste fits across `pages`
|
|
281
|
+
* pages, each other page keeping at least one; `null` is a paid start, which
|
|
282
|
+
* no wall shares out.
|
|
283
|
+
*
|
|
284
|
+
* ⛔ NEVER a lowered minimum. A Free guided start creates its site empty and
|
|
285
|
+
* plans its layout, and the wall then fits 4 sections across two pages. Told
|
|
286
|
+
* "a home of at least 3" beside that, the live yoga start of 2026-10-08
|
|
287
|
+
* planned 4 + 3 and then 4 + 2, was refused for the wall twice, and stopped:
|
|
288
|
+
* a home asked for in bands the wall cannot pay for is a start that fails.
|
|
289
|
+
*/ export function aiSiteHomeMinSections(input) {
|
|
290
|
+
if (input.across === null) return AI_SITE_HOME_MIN_SECTIONS;
|
|
291
|
+
const others = Math.max(0, Math.floor(input.pages) - 1);
|
|
292
|
+
return input.across - others >= AI_SITE_HOME_MIN_SECTIONS ? AI_SITE_HOME_MIN_SECTIONS : 0;
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* What a site start's plan is told to BUILD with (AGL-3660): the sections are
|
|
296
|
+
* a budget to use, never a ceiling to stay under. Told only "at most 8", the
|
|
297
|
+
* live prod start of 2026-10-08 planned a home of two sections, and Zach: "that
|
|
298
|
+
* didn't mean do as little as possible." On the Free taste the sentence shares
|
|
299
|
+
* the wall's sections out — a home of five or six, the other page two or
|
|
300
|
+
* three, about seven or eight in all; a paid start's home is asked to be rich.
|
|
301
|
+
* `min` is the home's fewest (`aiSiteHomeMinSections`), 0 where the plan
|
|
302
|
+
* plans no home or the wall cannot pay for a full one.
|
|
303
|
+
*/ export function aiSiteFullPlanSentence(input) {
|
|
304
|
+
const { across, min } = input;
|
|
305
|
+
if (across === null) {
|
|
306
|
+
return min > 0 ? `Plan a full website, never a minimal one: a rich home page of ${min + 1} or more sections where the brief gives them, and every page with the sections it needs.` : 'Plan a full website, never a minimal one: every page with the sections it needs.';
|
|
307
|
+
}
|
|
308
|
+
const lead = `The ${across} sections are the budget to use, not a ceiling to stay under: plan a full website, never a minimal one`;
|
|
309
|
+
const others = Math.max(0, Math.floor(input.pages) - 1);
|
|
310
|
+
if (min <= 0 || across < min) return `${lead}.`;
|
|
311
|
+
const homeHi = Math.max(min, Math.min(min + 1, across - others));
|
|
312
|
+
if (!others) return `${lead}: the home page at / with ${min} to ${homeHi} sections.`;
|
|
313
|
+
const otherHi = Math.max(1, Math.min(3, Math.floor((across - min) / others)));
|
|
314
|
+
const otherLo = Math.max(1, Math.min(2, otherHi, Math.floor((across - homeHi) / others)));
|
|
315
|
+
const totalLo = Math.max(min + others, across - 1);
|
|
316
|
+
const range = (lo, hi)=>lo === hi ? `${lo}` : `${lo} to ${hi}`;
|
|
317
|
+
const pagesWord = others === 1 ? 'the other page' : 'each other page';
|
|
318
|
+
return `${lead}: the home page at / with ${range(min, homeHi)} sections, ${pagesWord} ${range(otherLo, otherHi)}, about ${range(totalLo, across)} in total.`;
|
|
319
|
+
}
|
|
320
|
+
/** Whether a planned page is the site's home: the one at `/`. */ export function aiSitePlanIsHome(screen) {
|
|
321
|
+
const slug = screen.slug.trim();
|
|
322
|
+
return slug === '/' || slug === '';
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* A site plan's home page under its fewest sections (AGL-3660), as the plan
|
|
326
|
+
* step's re-ask names it. A plan with no page at `/` — the owner's own home
|
|
327
|
+
* stays — has no home to hold. `across` is the Free wall's section count, so
|
|
328
|
+
* the re-ask says where the sections come from rather than tripping the wall.
|
|
329
|
+
*/ export function aiSiteThinHomeViolations(plan, options) {
|
|
330
|
+
const index = plan.screens.findIndex(aiSitePlanIsHome);
|
|
331
|
+
if (index < 0) return [];
|
|
332
|
+
const count = plan.screens[index].sections.length;
|
|
333
|
+
if (count >= options.min) return [];
|
|
334
|
+
const within = options.across === null ? '' : ` Keep the whole plan within ${options.across} sections: take them from the other page where you must.`;
|
|
335
|
+
return [
|
|
336
|
+
{
|
|
337
|
+
rule: null,
|
|
338
|
+
code: AI_SITE_THIN_HOME_CODE,
|
|
339
|
+
message: `The home page has ${count} ${count === 1 ? 'section' : 'sections'}, and a new site's home page reads as a full website with at least ${options.min}: ${AI_SITE_HOME_BANDS}.${within}`,
|
|
340
|
+
paths: [
|
|
341
|
+
`screens[${index}].sections`
|
|
342
|
+
]
|
|
343
|
+
}
|
|
344
|
+
];
|
|
345
|
+
}
|
|
119
346
|
/**
|
|
120
347
|
* Sections a page is assumed to hold before a plan names them, for the
|
|
121
348
|
* estimate a member reads when they ask for a scaffold. A declared
|