@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.
Files changed (115) hide show
  1. package/package.json +17 -17
  2. package/src/lib/components/ai-job-links.d.ts +6 -0
  3. package/src/lib/components/ai-job-links.js +9 -0
  4. package/src/lib/components/ai-job-links.js.map +1 -1
  5. package/src/lib/components/ai-site-build-page.component.js +41 -3
  6. package/src/lib/components/ai-site-build-page.component.js.map +1 -1
  7. package/src/lib/components/ai-site-start-card.component.js +91 -8
  8. package/src/lib/components/ai-site-start-card.component.js.map +1 -1
  9. package/src/lib/declarations.server.js +7 -4
  10. package/src/lib/declarations.server.js.map +1 -1
  11. package/src/lib/jobs/ai-build-capabilities.js +8 -2
  12. package/src/lib/jobs/ai-build-capabilities.js.map +1 -1
  13. package/src/lib/jobs/ai-job-admission.d.ts +2 -1
  14. package/src/lib/jobs/ai-job-admission.js.map +1 -1
  15. package/src/lib/jobs/ai-job-email-step.d.ts +39 -0
  16. package/src/lib/jobs/ai-job-email-step.js +116 -5
  17. package/src/lib/jobs/ai-job-email-step.js.map +1 -1
  18. package/src/lib/jobs/ai-job-layout-language.d.ts +3 -0
  19. package/src/lib/jobs/ai-job-layout-language.js +3 -1
  20. package/src/lib/jobs/ai-job-layout-language.js.map +1 -1
  21. package/src/lib/jobs/ai-job-layout-site-pages.d.ts +2 -0
  22. package/src/lib/jobs/ai-job-layout-site-pages.js +23 -11
  23. package/src/lib/jobs/ai-job-layout-site-pages.js.map +1 -1
  24. package/src/lib/jobs/ai-job-layout-step.js +3 -2
  25. package/src/lib/jobs/ai-job-layout-step.js.map +1 -1
  26. package/src/lib/jobs/ai-job-page-language.d.ts +16 -0
  27. package/src/lib/jobs/ai-job-page-language.js +35 -6
  28. package/src/lib/jobs/ai-job-page-language.js.map +1 -1
  29. package/src/lib/jobs/ai-job-page-step.d.ts +3 -0
  30. package/src/lib/jobs/ai-job-page-step.js +18 -4
  31. package/src/lib/jobs/ai-job-page-step.js.map +1 -1
  32. package/src/lib/jobs/ai-job-plan-step.d.ts +40 -1
  33. package/src/lib/jobs/ai-job-plan-step.js +121 -16
  34. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  35. package/src/lib/jobs/ai-job-site-content.d.ts +6 -6
  36. package/src/lib/jobs/ai-job-site-content.js +6 -12
  37. package/src/lib/jobs/ai-job-site-content.js.map +1 -1
  38. package/src/lib/jobs/ai-job-site-step.d.ts +11 -0
  39. package/src/lib/jobs/ai-job-site-step.js +96 -44
  40. package/src/lib/jobs/ai-job-site-step.js.map +1 -1
  41. package/src/lib/jobs/ai-jobs-notify.d.ts +14 -4
  42. package/src/lib/jobs/ai-jobs-notify.js +70 -4
  43. package/src/lib/jobs/ai-jobs-notify.js.map +1 -1
  44. package/src/lib/jobs/ai-jobs.d.ts +22 -4
  45. package/src/lib/jobs/ai-jobs.js +47 -12
  46. package/src/lib/jobs/ai-jobs.js.map +1 -1
  47. package/src/lib/jobs/ai-layout-stock-photos.d.ts +98 -0
  48. package/src/lib/jobs/ai-layout-stock-photos.js +248 -0
  49. package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -0
  50. package/src/lib/jobs/ai-site-publish.d.ts +11 -0
  51. package/src/lib/jobs/ai-site-publish.js +31 -1
  52. package/src/lib/jobs/ai-site-publish.js.map +1 -1
  53. package/src/lib/jobs/fixtures/ai-juniper-portfolio-page-recording.d.ts +151 -0
  54. package/src/lib/jobs/fixtures/ai-juniper-portfolio-page-recording.js +183 -0
  55. package/src/lib/jobs/fixtures/ai-juniper-portfolio-page-recording.js.map +1 -0
  56. package/src/lib/jobs/fixtures/ai-slow-roads-site-plan-recording.d.ts +27 -0
  57. package/src/lib/jobs/fixtures/ai-slow-roads-site-plan-recording.js +192 -0
  58. package/src/lib/jobs/fixtures/ai-slow-roads-site-plan-recording.js.map +1 -0
  59. package/src/lib/jobs/fixtures/ai-yoga-site-plan-recording.d.ts +27 -0
  60. package/src/lib/jobs/fixtures/ai-yoga-site-plan-recording.js +222 -0
  61. package/src/lib/jobs/fixtures/ai-yoga-site-plan-recording.js.map +1 -0
  62. package/src/lib/layout-language/ai-layout-compiler.d.ts +29 -0
  63. package/src/lib/layout-language/ai-layout-compiler.js +698 -12
  64. package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
  65. package/src/lib/layout-language/ai-layout-design.d.ts +107 -0
  66. package/src/lib/layout-language/ai-layout-design.js +197 -0
  67. package/src/lib/layout-language/ai-layout-design.js.map +1 -0
  68. package/src/lib/layout-language/ai-layout-frame.d.ts +6 -0
  69. package/src/lib/layout-language/ai-layout-frame.js +22 -6
  70. package/src/lib/layout-language/ai-layout-frame.js.map +1 -1
  71. package/src/lib/layout-language/ai-layout-language.d.ts +2 -0
  72. package/src/lib/layout-language/ai-layout-language.js +15 -1
  73. package/src/lib/layout-language/ai-layout-language.js.map +1 -1
  74. package/src/lib/layout-language/ai-layout-links.d.ts +11 -0
  75. package/src/lib/layout-language/ai-layout-links.js +4 -0
  76. package/src/lib/layout-language/ai-layout-links.js.map +1 -1
  77. package/src/lib/layout-language/ai-layout-pictures.d.ts +11 -5
  78. package/src/lib/layout-language/ai-layout-pictures.js +80 -17
  79. package/src/lib/layout-language/ai-layout-pictures.js.map +1 -1
  80. package/src/lib/model/ai-job-notice.d.ts +9 -1
  81. package/src/lib/model/ai-job-notice.js +23 -0
  82. package/src/lib/model/ai-job-notice.js.map +1 -1
  83. package/src/lib/model/ai-plan-capabilities.d.ts +23 -0
  84. package/src/lib/model/ai-plan-capabilities.js +5 -0
  85. package/src/lib/model/ai-plan-capabilities.js.map +1 -1
  86. package/src/lib/model/ai-site-build-progress.d.ts +12 -2
  87. package/src/lib/model/ai-site-build-progress.js +78 -4
  88. package/src/lib/model/ai-site-build-progress.js.map +1 -1
  89. package/src/lib/model/ai-site-job.d.ts +209 -22
  90. package/src/lib/model/ai-site-job.js +247 -20
  91. package/src/lib/model/ai-site-job.js.map +1 -1
  92. package/src/lib/model/ai-site-kinds.js +6 -1
  93. package/src/lib/model/ai-site-kinds.js.map +1 -1
  94. package/src/lib/model/ai-site-look.d.ts +21 -0
  95. package/src/lib/model/ai-site-look.js +63 -14
  96. package/src/lib/model/ai-site-look.js.map +1 -1
  97. package/src/lib/operator-alerts.d.ts +18 -0
  98. package/src/lib/operator-alerts.js +42 -0
  99. package/src/lib/operator-alerts.js.map +1 -1
  100. package/src/lib/runtime/ai-doctrine-validators.d.ts +39 -1
  101. package/src/lib/runtime/ai-doctrine-validators.js +236 -6
  102. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  103. package/src/lib/runtime/ai-doctrine.js +5 -3
  104. package/src/lib/runtime/ai-doctrine.js.map +1 -1
  105. package/src/lib/runtime/ai-palette.generated.d.ts +2 -0
  106. package/src/lib/runtime/ai-palette.generated.js +18 -2
  107. package/src/lib/runtime/ai-palette.generated.js.map +1 -1
  108. package/src/lib/server/ai-jobs-resume.js.map +1 -1
  109. package/src/lib/server/ai-jobs-route.js +15 -3
  110. package/src/lib/server/ai-jobs-route.js.map +1 -1
  111. package/src/lib/server/assist-edit.js +5 -2
  112. package/src/lib/server/assist-edit.js.map +1 -1
  113. package/src/lib/usage/free-ai-credits-left.d.ts +54 -0
  114. package/src/lib/usage/free-ai-credits-left.js +62 -0
  115. 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 section.
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
- /** The one layout a Free plan includes, built first on a site with none. */
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
- /** A page's first section pass, which writes the cached prefix. */
92
- firstSection: number;
93
- /** Each later section pass. */
94
- laterSection: number;
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): the site plan as
100
- * `jobs/ai-job-free-site.spec.ts` derives it from the plan request as it
101
- * stands, on the model a Free site plans with, every answer at its ceiling;
102
- * the build's exchanges are the Free page's (`AI_FREE_PAGE_WORST_CASE_CREDITS`),
103
- * which the same spec holds these to. The spec fails when a figure it derives
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's plan fits in the Free taste at its worst,
109
- * across all its pages: the plan, the look, the layouts it builds first, each page's
110
- * listing and first pass, and ROOM FOR ONE RETRIED SECTION, then as many later
111
- * passes as the rest pays for; none when that is already past the wall. Every
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 function aiFreeSiteSectionsWithin(creations: {
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
- }, taste: number, credits?: Readonly<AiFreeSiteWorstCase>): number;
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 = 3;
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 site plan, its look, each page's
123
- * listing and its sections at the nominal count, from the figures the wall is
124
- * proven with rather than the nominal credits a paid estimate counts.
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 section.
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): the site plan as
79
- * `jobs/ai-job-free-site.spec.ts` derives it from the plan request as it
80
- * stands, on the model a Free site plans with, every answer at its ceiling;
81
- * the build's exchanges are the Free page's (`AI_FREE_PAGE_WORST_CASE_CREDITS`),
82
- * which the same spec holds these to. The spec fails when a figure it derives
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: 64,
88
- firstSection: 44,
89
- laterSection: 20,
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 layouts it builds first, each page's
95
- * listing and first pass, and ROOM FOR ONE RETRIED SECTION, then as many later
96
- * passes as the rest pays for; none when that is already past the wall. Every
97
- * page's first pass is counted as a cache write, which errs dear.
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 = credits.plan + credits.look + creations.layouts * credits.layout + pages * (credits.listing + credits.firstSection) + credits.firstSection;
113
+ const before = aiFreeSiteWorstCaseCredits(creations, 0, credits);
101
114
  if (before > taste) return 0;
102
- return pages + Math.floor((taste - before) / credits.laterSection);
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 = 3;
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 site plan, its look, each page's
108
- * listing and its sections at the nominal count, from the figures the wall is
109
- * proven with rather than the nominal credits a paid estimate counts.
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 credits.plan + credits.look + count * (credits.listing + credits.firstSection) + count * (AI_FREE_SITE_NOMINAL_SECTIONS - 1) * credits.laterSection;
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