@aglyn/plugins-ai 1.0.0-beta.264 → 1.0.0-beta.266

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 (98) hide show
  1. package/package.json +17 -17
  2. package/src/lib/assets/site-styles/beauty.webp +0 -0
  3. package/src/lib/assets/site-styles/blog.webp +0 -0
  4. package/src/lib/assets/site-styles/business.webp +0 -0
  5. package/src/lib/assets/site-styles/coming-soon.webp +0 -0
  6. package/src/lib/assets/site-styles/education.webp +0 -0
  7. package/src/lib/assets/site-styles/events.webp +0 -0
  8. package/src/lib/assets/site-styles/fitness.webp +0 -0
  9. package/src/lib/assets/site-styles/landing.webp +0 -0
  10. package/src/lib/assets/site-styles/music.webp +0 -0
  11. package/src/lib/assets/site-styles/nonprofit.webp +0 -0
  12. package/src/lib/assets/site-styles/personal.webp +0 -0
  13. package/src/lib/assets/site-styles/photography.webp +0 -0
  14. package/src/lib/assets/site-styles/portfolio.webp +0 -0
  15. package/src/lib/assets/site-styles/professional.webp +0 -0
  16. package/src/lib/assets/site-styles/realestate.webp +0 -0
  17. package/src/lib/assets/site-styles/restaurant.webp +0 -0
  18. package/src/lib/assets/site-styles/store.webp +0 -0
  19. package/src/lib/assets/site-styles/studio.webp +0 -0
  20. package/src/lib/assets/site-styles/trades.webp +0 -0
  21. package/src/lib/assets/site-styles/wellness.webp +0 -0
  22. package/src/lib/assets/site-styles/yoga.webp +0 -0
  23. package/src/lib/components/ai-site-start-card.component.d.ts +12 -0
  24. package/src/lib/components/ai-site-start-card.component.js +256 -236
  25. package/src/lib/components/ai-site-start-card.component.js.map +1 -1
  26. package/src/lib/components/ai-site-start-steps.component.d.ts +108 -0
  27. package/src/lib/components/ai-site-start-steps.component.js +451 -0
  28. package/src/lib/components/ai-site-start-steps.component.js.map +1 -0
  29. package/src/lib/components/ai-site-start-tracking.d.ts +114 -0
  30. package/src/lib/components/ai-site-start-tracking.js +277 -0
  31. package/src/lib/components/ai-site-start-tracking.js.map +1 -0
  32. package/src/lib/components/ai-site-style-previews.d.ts +26 -0
  33. package/src/lib/components/ai-site-style-previews.js +115 -0
  34. package/src/lib/components/ai-site-style-previews.js.map +1 -0
  35. package/src/lib/insights/ai-insight-digest.d.ts +9 -0
  36. package/src/lib/insights/ai-insight-digest.js +35 -11
  37. package/src/lib/insights/ai-insight-digest.js.map +1 -1
  38. package/src/lib/jobs/ai-job-page-language.d.ts +9 -0
  39. package/src/lib/jobs/ai-job-page-language.js +20 -2
  40. package/src/lib/jobs/ai-job-page-language.js.map +1 -1
  41. package/src/lib/jobs/ai-job-plan-step.d.ts +43 -8
  42. package/src/lib/jobs/ai-job-plan-step.js +99 -18
  43. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  44. package/src/lib/jobs/ai-job-site-datasets.d.ts +82 -1
  45. package/src/lib/jobs/ai-job-site-datasets.js +276 -14
  46. package/src/lib/jobs/ai-job-site-datasets.js.map +1 -1
  47. package/src/lib/jobs/ai-layout-stock-photos.d.ts +108 -10
  48. package/src/lib/jobs/ai-layout-stock-photos.js +729 -87
  49. package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
  50. package/src/lib/jobs/fixtures/ai-site-foreign-datasets-recording.d.ts +58 -0
  51. package/src/lib/jobs/fixtures/ai-site-foreign-datasets-recording.js +678 -0
  52. package/src/lib/jobs/fixtures/ai-site-foreign-datasets-recording.js.map +1 -0
  53. package/src/lib/jobs/fixtures/ai-stock-photo-agl3856-beta265-recordings.d.ts +33 -0
  54. package/src/lib/jobs/fixtures/ai-stock-photo-agl3856-beta265-recordings.js +270 -0
  55. package/src/lib/jobs/fixtures/ai-stock-photo-agl3856-beta265-recordings.js.map +1 -0
  56. package/src/lib/jobs/fixtures/ai-stock-photo-agl3856-people-recordings.d.ts +33 -0
  57. package/src/lib/jobs/fixtures/ai-stock-photo-agl3856-people-recordings.js +277 -0
  58. package/src/lib/jobs/fixtures/ai-stock-photo-agl3856-people-recordings.js.map +1 -0
  59. package/src/lib/layout-language/ai-layout-compiler.js +49 -12
  60. package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
  61. package/src/lib/layout-language/ai-layout-design.d.ts +6 -3
  62. package/src/lib/layout-language/ai-layout-design.js +14 -13
  63. package/src/lib/layout-language/ai-layout-design.js.map +1 -1
  64. package/src/lib/layout-language/ai-layout-listings.d.ts +48 -0
  65. package/src/lib/layout-language/ai-layout-listings.js +91 -5
  66. package/src/lib/layout-language/ai-layout-listings.js.map +1 -1
  67. package/src/lib/layout-language/ai-layout-pictures.d.ts +15 -0
  68. package/src/lib/layout-language/ai-layout-pictures.js +6 -2
  69. package/src/lib/layout-language/ai-layout-pictures.js.map +1 -1
  70. package/src/lib/model/ai-site-home-arcs.d.ts +75 -0
  71. package/src/lib/model/ai-site-home-arcs.js +741 -0
  72. package/src/lib/model/ai-site-home-arcs.js.map +1 -0
  73. package/src/lib/model/ai-site-inventory.d.ts +18 -0
  74. package/src/lib/model/ai-site-inventory.js.map +1 -1
  75. package/src/lib/model/ai-site-job.d.ts +9 -6
  76. package/src/lib/model/ai-site-job.js +8 -5
  77. package/src/lib/model/ai-site-job.js.map +1 -1
  78. package/src/lib/model/ai-site-kinds.js +24 -24
  79. package/src/lib/model/ai-site-kinds.js.map +1 -1
  80. package/src/lib/model/ai-site-start-steps.d.ts +67 -0
  81. package/src/lib/model/ai-site-start-steps.js +127 -0
  82. package/src/lib/model/ai-site-start-steps.js.map +1 -0
  83. package/src/lib/model/ai-site-start.js +4 -1
  84. package/src/lib/model/ai-site-start.js.map +1 -1
  85. package/src/lib/runtime/ai-doctrine-validators.d.ts +32 -0
  86. package/src/lib/runtime/ai-doctrine-validators.js +119 -2
  87. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  88. package/src/lib/runtime/ai-doctrine.d.ts +6 -1
  89. package/src/lib/runtime/ai-doctrine.js +9 -5
  90. package/src/lib/runtime/ai-doctrine.js.map +1 -1
  91. package/src/lib/runtime/ai-palette.generated.js +5 -2
  92. package/src/lib/runtime/ai-palette.generated.js.map +1 -1
  93. package/src/lib/runtime/site-inventory.js +6 -0
  94. package/src/lib/runtime/site-inventory.js.map +1 -1
  95. package/src/lib/server/ai-insight-digest-route.js +2 -2
  96. package/src/lib/server/ai-insight-digest-route.js.map +1 -1
  97. package/src/lib/usage/assist-free-taste.js +3 -0
  98. package/src/lib/usage/assist-free-taste.js.map +1 -1
@@ -14,12 +14,14 @@
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
16
  */
17
+ import { type AiSiteHomeArc } from '../model/ai-site-home-arcs';
17
18
  import type { AglynOrgBilling } from '@aglyn/aglyn/foundation/definitions/org-billing.types';
18
19
  import { type AiBuildPlan } from '../model/ai-build-plan';
19
20
  import type { AiJob, AiJobKind, AiJobPlan, AiJobStatus } from '../model/ai-jobs.types';
20
21
  import { readFreeAiCreditsLeft } from '../usage/free-ai-credits-left';
21
22
  import { type AiPlanCapabilities, type AiPlanJobScope } from '../model/ai-plan-capabilities';
22
23
  import type { AiSiteInventory } from '../model/ai-site-inventory';
24
+ import { type AiDatasetRecordCounter } from './ai-job-site-datasets';
23
25
  import { type AiDoctrineViolation } from '../runtime/ai-doctrine-validators';
24
26
  import type { AiEffort, AiTool } from '../providers/contract';
25
27
  import type { AiSystemBlock } from '../runtime/ai-runtime';
@@ -89,7 +91,7 @@ export declare const AI_JOB_PLAN_REVIEW_COPY = "The plan is ready. Review what t
89
91
  * (AGL-3030). Per workspace and per site, which is why they ride here and
90
92
  * never in a system block the platform's cache entry is keyed on.
91
93
  */
92
- export declare function aiJobPlanPrompt(job: Pick<AiJob, 'kind' | 'brief' | 'inputs'>, capabilities?: AiPlanCapabilities | null, inventory?: AiSiteInventory | null, ops?: AiBuildOps | null, now?: Date | null): string;
94
+ export declare function aiJobPlanPrompt(job: Pick<AiJob, 'kind' | 'brief' | 'inputs'> & Partial<Pick<AiJob, '$id'>>, capabilities?: AiPlanCapabilities | null, inventory?: AiSiteInventory | null, ops?: AiBuildOps | null, now?: Date | null): string;
93
95
  /**
94
96
  * What a BUILD job's plan is told (AGL-3616): that one request becomes pages,
95
97
  * creations and items, how many pages it may hold, and the item operations
@@ -110,7 +112,7 @@ export declare function aiPlanTodaySentence(now: Date): string;
110
112
  * home there and was refused for the clash on every new site. Kept off a page
111
113
  * job's turn, whose length the Free page's wall is proven at.
112
114
  */
113
- export declare function aiPlanSiteLines(job: Pick<AiJob, 'kind'> & Partial<Pick<AiJob, 'inputs' | 'brief'>>, inventory: AiSiteInventory | null, capabilities: AiPlanCapabilities | null): string[];
115
+ export declare function aiPlanSiteLines(job: Pick<AiJob, 'kind'> & Partial<Pick<AiJob, 'inputs' | 'brief' | '$id'>>, inventory: AiSiteInventory | null, capabilities: AiPlanCapabilities | null): string[];
114
116
  /**
115
117
  * The settling every plan is given before the doctrine reads it (AGL-3887):
116
118
  * no section named a placeholder, and on a site start, where the site and
@@ -126,10 +128,33 @@ export declare function aiPlanSaidSettle(job: Pick<AiJob, 'kind' | 'brief' | 'in
126
128
  export declare const AI_SITE_NO_CUSTOMER_WORDS_SENTENCE = "This site has no reviews or testimonials yet, and the build never writes them: plan no testimonials, reviews or client feedback section, and plan what working together is like instead. Never name a section a placeholder.";
127
129
  /**
128
130
  * The sentence a site plan's turn states about a section that collects
129
- * answers (AGL-3660): it places the saved form by reference, and one form
130
- * may be placed on more than one page.
131
- */
132
- export declare const AI_SITE_FORM_SENTENCE = "A section that asks visitors for something (contact, inquiry, commissions, booking, sign-up) places the form: put new:<the form's name> or the site's form id in its uses. One form may be placed on several pages.";
131
+ * answers (AGL-3660): it places the saved form by reference — on the page a
132
+ * visitor opens to get in touch, never on the home page as well (AGL-3899).
133
+ * Told "one form may be placed on several pages", 17 of the 22 newest
134
+ * production homes carried the contact form, 16 of them in their last band,
135
+ * so every site ended the same way.
136
+ */
137
+ export declare const AI_SITE_FORM_SENTENCE = "A section that asks visitors for something (contact, inquiry, commissions, booking, sign-up) places the form: put new:<the form's name> or the site's form id in its uses.";
138
+ /** Where the form goes, said after {@link AI_SITE_FORM_SENTENCE} on a site whose home does not keep it. */
139
+ export declare const AI_SITE_FORM_PAGE_SENTENCE = "Place it on the page a visitor opens to get in touch (such as Contact, Book, Visit or Enroll), and nowhere on the home page: the home's call to action is a button to that page.";
140
+ /** What a site plan is told about its form: where it goes, as {@link aiSiteFormStaysHome} decides. */
141
+ export declare function aiSiteFormSentence(onHome: boolean): string;
142
+ /**
143
+ * What a site plan is told about repeated items (AGL-3899): as many as the
144
+ * content has. Told nothing, the newest production plans gave 45 of 62 home
145
+ * groups three items, and the pages drew 47 of 55 groups with three.
146
+ */
147
+ export declare const AI_SITE_ITEMS_SENTENCE = "Give each section that repeats items as many as its content has, from 2 to 6 (two offers, four steps, five questions, six pieces of work): vary the counts from section to section, never 3 everywhere.";
148
+ /**
149
+ * Whether a site start's home keeps its form (AGL-3899): a one-page site, a
150
+ * kind whose one action is that form (a landing or coming-soon page), or a
151
+ * brief that asks for it there. Every other start places the form on the
152
+ * page a visitor opens to get in touch, and its home closes with a call to
153
+ * action that links it (`aiSettlePlanHomeForms`).
154
+ */
155
+ export declare function aiSiteFormStaysHome(job: Partial<Pick<AiJob, 'inputs' | 'brief'>>, freePages?: number): boolean;
156
+ /** The home arc a site start's plan is told (AGL-3899), by its job's seed; `null` for a job with no id. */
157
+ export declare function aiSitePlanHomeArc(job: Partial<Pick<AiJob, '$id' | 'inputs'>>): AiSiteHomeArc | null;
133
158
  /**
134
159
  * The sentence a site plan's turn states where it may create datasets, with
135
160
  * how many it may (the live eval of 2026-10-10 planned four past a cap of
@@ -172,8 +197,12 @@ export declare function aiSiteThinHomeCheck(rule: {
172
197
  export declare const AI_SITE_STORE_SHOP_SENTENCE = "Plan a Shop page at /shop: a short title section, then a section named \"Product grid\" second, where the platform lists the store's real products with sort, filters, photos, prices and cart, then at most one or two short sections (care, shipping, gifting help). Never open it with a hero, and never plan the products themselves as cards of your own.";
173
198
  /** What a paid store's plan is told about the pages the platform adds beside it (AGL-3676). */
174
199
  export declare const AI_SITE_STORE_PAGES_SENTENCE = "The platform adds this store's account page (sign in, orders, saved items), its cart page, and its Shipping & returns, Privacy policy and Terms of sale pages, and links them in the header and footer; they do not count toward this plan's pages. Plan none of them: spend every page on what the brief is about.";
175
- /** What a store's home is composed of (AGL-3676): a storefront's parts, in a storefront's order. */
176
- export declare const AI_SITE_STORE_HOME_BANDS = "a hero that sells first, then \"Bestsellers\" (the platform lists the real products), \"Shop by collection\" with 3 or 4 items, the brand story, why buy here with 3 or 4 items, \"Customer reviews\" (the platform shows the store's real ones) and a \"Newsletter sign-up\" band last";
200
+ /**
201
+ * What a store's home is composed of (AGL-3676): a storefront's parts, the
202
+ * hero first and the sign-up last, between them in the order its arc gives
203
+ * (AGL-3899).
204
+ */
205
+ export declare const AI_SITE_STORE_HOME_BANDS = "a hero that sells first and a \"Newsletter sign-up\" band last, and between them \"Bestsellers\" (the platform lists the real products), \"Shop by collection\" with 3 or 4 items, the brand story, why buy here with 3 or 4 items and \"Customer reviews\" (the platform shows the store's real ones)";
177
206
  /**
178
207
  * A store plan held to a storefront (AGL-3676, `ai-layout-storefront.ts`):
179
208
  * on a paid start its home gains the parts a shopper expects that the plan
@@ -379,6 +408,12 @@ export interface AiJobPlanStepDeps {
379
408
  * their own, and `null` turns it off.
380
409
  */
381
410
  readSiteEvents?: typeof readAiSiteEventPlanning | null;
411
+ /**
412
+ * How a site start counts the records of each dataset its site sees
413
+ * (AGL-3912), which decides whether its plan may reuse one; specs hand in
414
+ * their own.
415
+ */
416
+ countDatasetRecords?: AiDatasetRecordCounter;
382
417
  }
383
418
  /** What a job's site context is read with (AGL-3661). */
384
419
  export type AiSiteContextReader = (input: {
@@ -15,6 +15,8 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
15
15
  * See the License for the specific language governing permissions and
16
16
  * limitations under the License.
17
17
  */ import { aiSiteKindOfInputs } from "../model/ai-site-kinds.js";
18
+ import { aiSiteHomeArc, aiSiteHomeArcSentence } from "../model/ai-site-home-arcs.js";
19
+ import { aiSiteSeed } from "../model/ai-site-look.js";
18
20
  import { createHash } from "node:crypto";
19
21
  import { AI_BUILD_PLAN_TOOL, aiBuildPlanToolFor, aiPlanSlugKey, isAiPlanNewRef } from "../model/ai-build-plan.js";
20
22
  import { AI_BUILD_PRODUCT_OP, aiBuildStorePagesFor } from "../model/ai-build-store-pages.js";
@@ -23,7 +25,7 @@ import { AI_TEMPLATE_SUBJECT_DEFINITIONS, aiTemplateShownTokens } from "../model
23
25
  import { AI_JOB_CREATE_KINDS, AI_JOB_CREATE_NOUNS } from "../model/ai-job-creations.js";
24
26
  import { AI_PAGE_CREATE_KINDS, aiPagePlanShapeRefusal } from "../model/ai-page-job.js";
25
27
  import { AI_PLAN_COMPILED_REPEATS_SENTENCE, AI_PLAN_NO_FORM_SENTENCE, aiPlanCanPlaceForm, aiPlanCapabilitiesForJob, aiPlanCapabilityLines } from "../model/ai-plan-capabilities.js";
26
- import { aiSitePlanDatasetCapability } from "./ai-job-site-datasets.js";
28
+ import { aiSettleSiteForeignDatasets, aiSiteInventoryWithRecordCounts, aiSitePlanDatasetCapability } from "./ai-job-site-datasets.js";
27
29
  import { AI_FREE_SITE_WORST_CASE_CREDITS, AI_SITE_CREATE_KINDS, AI_SITE_FREE_PAGES, AI_SITE_FREE_POSTS, AI_SITE_PLAN_MAX_TOKENS, AI_SITE_HOME_BANDS, aiFreeSiteSectionsWithin, aiSiteFullPlanSentence, aiSiteHomeMinSections, aiSiteNameSentence, aiSitePlanIsHome, aiSitePlanShapeRefusal, aiSiteBlogStandInViolations, aiSiteThinHomeViolations, AI_SITE_GALLERY_SENTENCE, aiSiteEmptyGalleryViolations, AI_SITE_DATASETS_MAX, AI_SITE_GALLERY_MIN_ITEMS } from "../model/ai-site-job.js";
28
30
  import { aiSiteContentPart } from "./ai-job-site-content.js";
29
31
  import { aiPlanHasCustomerWords, aiSettlePlanPlaceholderNames, aiSettlePlanSaidSections } from "../model/ai-plan-said-sections.js";
@@ -35,7 +37,7 @@ import { aiPlanFailureCopy, aiPlanRetryRefusal } from "../model/ai-job-failure-c
35
37
  import { FREE_AI_TASTE_CREDITS_PER_MONTH } from "../plan-entitlements.js";
36
38
  import { aiDoctrineSystemBlocks, runValidatedGeneration } from "../runtime/ai-doctrine.js";
37
39
  import { AI_STEP_TIERS, aiCatalogEntry, aiDefaultModelFor } from "../providers/catalog.js";
38
- import { AI_FREE_PAGE_WORST_CASE_CREDITS, aiPlanEmbedBriefViolations } from "../runtime/ai-doctrine-validators.js";
40
+ import { AI_FREE_PAGE_WORST_CASE_CREDITS, aiPlanEmbedBriefViolations, aiSettlePlanDatasets, aiSettlePlanHomeForms } from "../runtime/ai-doctrine-validators.js";
39
41
  import { assistCreditsFromUsd } from "../usage/assist-credits.js";
40
42
  import { ASSIST_RETURNED_USD_FIELD, assistSpendAfterReturnsUsd } from "../usage/assist-credit-returns.js";
41
43
  import { freeAccountUsageRef, freeAssistAccount } from "../usage/assist-free-taste.js";
@@ -53,7 +55,7 @@ import { readAiPlanCapabilities } from "./ai-job-drafts.js";
53
55
  import { AI_JOB_BRIEF_MAX_CHARS } from "./ai-job-text-step.js";
54
56
  import { aiJobStepBudget } from "./ai-job-budget.js";
55
57
  import { aiDoctrineReview, aiUnspentOutcome } from "./ai-job-generation.js";
56
- import { aiPlanWithDraftIds } from "./ai-job-draft-ids.js";
58
+ import { aiOriginJobId, aiPlanWithDraftIds } from "./ai-job-draft-ids.js";
57
59
  import { AI_JOBS_COLLECTION, registerAiJobPlanStep } from "./ai-jobs.js";
58
60
  /**
59
61
  * The plan step (AGL-2935): the first step of every job that builds site
@@ -197,17 +199,25 @@ import { AI_JOBS_COLLECTION, registerAiJobPlanStep } from "./ai-jobs.js";
197
199
  }
198
200
  const cap = capabilities == null ? void 0 : capabilities.freeSitePages;
199
201
  const home = aiSitePlanHomeRule(inventory, capabilities);
202
+ // The form lives on the page a visitor opens to get in touch (AGL-3899),
203
+ // and the home's call to action links it; a one-page site keeps it home.
204
+ const formOnHome = aiSiteFormStaysHome(job, cap);
200
205
  if (cap !== undefined) {
201
- lines.push(`This is a Free workspace: plan at most ${cap} ${cap === 1 ? 'page' : 'pages'} — the home page at / and the one page the brief most needs, such as services, booking or contact — and ${home.across} sections across them.${aiPlanCanPlaceForm(inventory, capabilities) ? ' Put the contact form on one of them.' : ''}`);
206
+ const where = formOnHome ? ' Put the contact form on the home page.' : ' Put the contact form on the page that is not the home page.';
207
+ lines.push(`This is a Free workspace: plan at most ${cap} ${cap === 1 ? 'page' : 'pages'} — the home page at / and the one page the brief most needs, such as services, booking or contact — and ${home.across} sections across them.${aiPlanCanPlaceForm(inventory, capabilities) ? where : ''}`);
202
208
  }
203
209
  // A new site is a full website (AGL-3660): its home is planned as one, not
204
210
  // left to a ceiling the outline is free to stay far under.
205
211
  const plansHome = !pages.some((page)=>!page.replaceable && aiSitePlanIsHome(page));
206
212
  if (home.min > 0 && plansHome) {
207
213
  var _aiSiteKindOfInputs1;
208
- // A store's home is a storefront (AGL-3676): its own parts, in its own order.
214
+ // A store's home is a storefront (AGL-3676): its own parts.
209
215
  const bands = ((_aiSiteKindOfInputs1 = aiSiteKindOfInputs(job.inputs)) == null ? void 0 : _aiSiteKindOfInputs1.id) === 'store' ? AI_SITE_STORE_HOME_BANDS : AI_SITE_HOME_BANDS;
210
216
  lines.push(`The home page at / reads as a full website: at least ${home.min} sections — ${bands}.`);
217
+ // And its own arc, chosen by the job's seed (AGL-3899): two identical
218
+ // briefs are two different homes.
219
+ const arc = aiSitePlanHomeArc(job);
220
+ if (arc) lines.push(aiSiteHomeArcSentence(arc));
211
221
  }
212
222
  // The sections are a budget to use (AGL-3660): a prod start of 2026-10-08
213
223
  // read "at most 8" as leave to plan a home of two.
@@ -242,7 +252,10 @@ import { AI_JOBS_COLLECTION, registerAiJobPlanStep } from "./ai-jobs.js";
242
252
  // A section that asks for something places the form (AGL-3660): a live
243
253
  // portfolio start planned a commissions inquiry beside the form it made
244
254
  // for its Contact page, without placing it.
245
- if (aiPlanCanPlaceForm(inventory, capabilities)) lines.push(AI_SITE_FORM_SENTENCE);
255
+ if (aiPlanCanPlaceForm(inventory, capabilities)) lines.push(aiSiteFormSentence(formOnHome));
256
+ // As many items as the content has (AGL-3899): 47 of 55 groups in the
257
+ // newest production homes showed exactly three.
258
+ lines.push(AI_SITE_ITEMS_SENTENCE);
246
259
  // Structured content is kept as a dataset where one may be made (AGL-3616).
247
260
  if (capabilities == null ? void 0 : capabilities.create.dataset.allowed) {
248
261
  var _capabilities_datasetsMax, _capabilities_create_dataset_left;
@@ -282,9 +295,58 @@ import { AI_JOBS_COLLECTION, registerAiJobPlanStep } from "./ai-jobs.js";
282
295
  */ export const AI_SITE_NO_CUSTOMER_WORDS_SENTENCE = 'This site has no reviews or testimonials yet, and the build never writes them: plan no testimonials, reviews or client feedback section, and plan what working together is like instead. Never name a section a placeholder.';
283
296
  /**
284
297
  * The sentence a site plan's turn states about a section that collects
285
- * answers (AGL-3660): it places the saved form by reference, and one form
286
- * may be placed on more than one page.
287
- */ export const AI_SITE_FORM_SENTENCE = "A section that asks visitors for something (contact, inquiry, commissions, booking, sign-up) places the form: put new:<the form's name> or the site's form id in its uses. One form may be placed on several pages.";
298
+ * answers (AGL-3660): it places the saved form by reference — on the page a
299
+ * visitor opens to get in touch, never on the home page as well (AGL-3899).
300
+ * Told "one form may be placed on several pages", 17 of the 22 newest
301
+ * production homes carried the contact form, 16 of them in their last band,
302
+ * so every site ended the same way.
303
+ */ export const AI_SITE_FORM_SENTENCE = "A section that asks visitors for something (contact, inquiry, commissions, booking, sign-up) places the form: put new:<the form's name> or the site's form id in its uses.";
304
+ /** Where the form goes, said after {@link AI_SITE_FORM_SENTENCE} on a site whose home does not keep it. */ export const AI_SITE_FORM_PAGE_SENTENCE = "Place it on the page a visitor opens to get in touch (such as Contact, Book, Visit or Enroll), and nowhere on the home page: the home's call to action is a button to that page.";
305
+ /** What a site plan is told about its form: where it goes, as {@link aiSiteFormStaysHome} decides. */ export function aiSiteFormSentence(onHome) {
306
+ return onHome ? AI_SITE_FORM_SENTENCE : `${AI_SITE_FORM_SENTENCE} ${AI_SITE_FORM_PAGE_SENTENCE}`;
307
+ }
308
+ /**
309
+ * What a site plan is told about repeated items (AGL-3899): as many as the
310
+ * content has. Told nothing, the newest production plans gave 45 of 62 home
311
+ * groups three items, and the pages drew 47 of 55 groups with three.
312
+ */ export const AI_SITE_ITEMS_SENTENCE = 'Give each section that repeats items as many as its content has, from 2 to 6 (two offers, four steps, five questions, six pieces of work): vary the counts from section to section, never 3 everywhere.';
313
+ /** The kinds whose one action is the form on the home page: a landing page and a coming-soon page. */ const AI_SITE_FORM_HOME_KINDS = new Set([
314
+ 'landing',
315
+ 'coming-soon'
316
+ ]);
317
+ /**
318
+ * A brief that asks for the form on the home page itself, in one sentence:
319
+ * "a sign-up form on the home page", "the homepage should have a contact form".
320
+ */ const HOME_FORM_ASKED = /\b(?:home ?page|front page|main page)\b[^.!?\n]{0,60}\bform\b|\bform\b[^.!?\n]{0,60}\b(?:home ?page|front page|main page)\b/i;
321
+ /**
322
+ * Whether a site start's home keeps its form (AGL-3899): a one-page site, a
323
+ * kind whose one action is that form (a landing or coming-soon page), or a
324
+ * brief that asks for it there. Every other start places the form on the
325
+ * page a visitor opens to get in touch, and its home closes with a call to
326
+ * action that links it (`aiSettlePlanHomeForms`).
327
+ */ export function aiSiteFormStaysHome(job, freePages) {
328
+ var _job_brief;
329
+ var _job_inputs, _aiSiteKindOfInputs;
330
+ const asked = Number((_job_inputs = job.inputs) == null ? void 0 : _job_inputs['pages']);
331
+ // A Free start's pages are its capped band (`freeSitePages`), else the pages asked for.
332
+ const pages = freePages != null ? freePages : Number.isInteger(asked) && asked > 0 ? asked : null;
333
+ if (pages === 1) return true;
334
+ const kind = (_aiSiteKindOfInputs = aiSiteKindOfInputs(job.inputs)) == null ? void 0 : _aiSiteKindOfInputs.id;
335
+ if (kind && AI_SITE_FORM_HOME_KINDS.has(kind)) return true;
336
+ return HOME_FORM_ASKED.test((_job_brief = job.brief) != null ? _job_brief : '');
337
+ }
338
+ /** The home arc a site start's plan is told (AGL-3899), by its job's seed; `null` for a job with no id. */ export function aiSitePlanHomeArc(job) {
339
+ var _ref, _job_inputs;
340
+ var _aiSiteKindOfInputs;
341
+ if (!job.$id) return null;
342
+ return aiSiteHomeArc({
343
+ kind: (_ref = (_aiSiteKindOfInputs = aiSiteKindOfInputs(job.inputs)) == null ? void 0 : _aiSiteKindOfInputs.id) != null ? _ref : null,
344
+ seed: aiSiteSeed(aiOriginJobId({
345
+ $id: job.$id,
346
+ inputs: (_job_inputs = job.inputs) != null ? _job_inputs : {}
347
+ }))
348
+ });
349
+ }
288
350
  /**
289
351
  * The sentence a site plan's turn states where it may create datasets, with
290
352
  * how many it may (the live eval of 2026-10-10 planned four past a cap of
@@ -348,7 +410,11 @@ import { AI_JOBS_COLLECTION, registerAiJobPlanStep } from "./ai-jobs.js";
348
410
  }
349
411
  /** What a store's plan is told about its Shop page (AGL-3676). */ export const AI_SITE_STORE_SHOP_SENTENCE = 'Plan a Shop page at /shop: a short title section, then a section named "Product grid" second, where the platform lists the store\'s real products with sort, filters, photos, prices and cart, then at most one or two short sections (care, shipping, gifting help). Never open it with a hero, and never plan the products themselves as cards of your own.';
350
412
  /** What a paid store's plan is told about the pages the platform adds beside it (AGL-3676). */ export const AI_SITE_STORE_PAGES_SENTENCE = "The platform adds this store's account page (sign in, orders, saved items), its cart page, and its Shipping & returns, Privacy policy and Terms of sale pages, and links them in the header and footer; they do not count toward this plan's pages. Plan none of them: spend every page on what the brief is about.";
351
- /** What a store's home is composed of (AGL-3676): a storefront's parts, in a storefront's order. */ export const AI_SITE_STORE_HOME_BANDS = 'a hero that sells first, then "Bestsellers" (the platform lists the real products), "Shop by collection" with 3 or 4 items, the brand story, why buy here with 3 or 4 items, "Customer reviews" (the platform shows the store\'s real ones) and a "Newsletter sign-up" band last';
413
+ /**
414
+ * What a store's home is composed of (AGL-3676): a storefront's parts, the
415
+ * hero first and the sign-up last, between them in the order its arc gives
416
+ * (AGL-3899).
417
+ */ export const AI_SITE_STORE_HOME_BANDS = 'a hero that sells first and a "Newsletter sign-up" band last, and between them "Bestsellers" (the platform lists the real products), "Shop by collection" with 3 or 4 items, the brand story, why buy here with 3 or 4 items and "Customer reviews" (the platform shows the store\'s real ones)';
352
418
  /**
353
419
  * A store plan held to a storefront (AGL-3676, `ai-layout-storefront.ts`):
354
420
  * on a paid start its home gains the parts a shopper expects that the plan
@@ -806,10 +872,17 @@ export function createAiJobPlanStep(deps = {}) {
806
872
  var _AI_JOB_PLAN_SCOPES_job_kind, _ref, _job_inputs, _ref1;
807
873
  var _aiSiteKindOfInputs, _aiSiteKindOfInputs1;
808
874
  const org = orgDocument != null ? orgDocument : null;
809
- const [inventory, workspace, siteContext] = await Promise.all([
810
- job.hostId ? readInventory(job.orgId, job.hostId, {
875
+ // A site start counts each dataset's records (AGL-3912): it reuses only
876
+ // one whose records are its own to show.
877
+ const readPlanInventory = async ()=>{
878
+ if (!job.hostId) return null;
879
+ const read = await readInventory(job.orgId, job.hostId, {
811
880
  firestore
812
- }) : Promise.resolve(null),
881
+ });
882
+ return job.kind === 'site' ? aiSiteInventoryWithRecordCounts(read, job.orgId, deps.countDatasetRecords) : read;
883
+ };
884
+ const [inventory, workspace, siteContext] = await Promise.all([
885
+ readPlanInventory(),
813
886
  readCapabilities({
814
887
  job,
815
888
  org,
@@ -960,9 +1033,11 @@ export function createAiJobPlanStep(deps = {}) {
960
1033
  if (reused) {
961
1034
  var _ref2;
962
1035
  // The reused plan's draft ids name the other job's drafts; this job's are its own.
963
- const plan = aiPlanWithStorePages(job.kind, ops, inventory, aiPlanWithItemCredits(itemOps, aiPlanWithDraftIds(job.kind, _extends({}, reused.plan, {
1036
+ // Settled again against what the datasets hold now (AGL-3912).
1037
+ const kept = site ? aiSettlePlanDatasets(aiSettleSiteForeignDatasets(reused.plan, inventory, capabilities), capabilities) : reused.plan;
1038
+ const plan = aiPlanWithStorePages(job.kind, ops, inventory, aiPlanWithItemCredits(itemOps, aiPlanWithDraftIds(job.kind, _extends({}, kept, {
964
1039
  status: 'proposed',
965
- labels: aiPlanLabels(reused.plan, inventory),
1040
+ labels: aiPlanLabels(kept, inventory),
966
1041
  proposedAt: now,
967
1042
  confirmedAt: null,
968
1043
  confirmedBy: null,
@@ -1012,7 +1087,8 @@ export function createAiJobPlanStep(deps = {}) {
1012
1087
  // A build's events are listed on a page (AGL-3755): on the one it makes
1013
1088
  // about them, else on an Events page of its own where there is room.
1014
1089
  // No section is named a placeholder, and a site start plans no section
1015
- // of customer words it has none of (AGL-3887).
1090
+ // of customer words it has none of (AGL-3887). A site start's plan
1091
+ // makes again each dataset it reuses that another site filled (AGL-3912).
1016
1092
  settle: (answered)=>{
1017
1093
  var _ref;
1018
1094
  return aiPlanSaidSettle(job, inventory, site ? aiSitePlanHomeRule(inventory, capabilities).min : 0)(ops ? aiSettleBuildEventPage(answered, {
@@ -1020,8 +1096,13 @@ export function createAiJobPlanStep(deps = {}) {
1020
1096
  siteSlugs: ((_ref = inventory == null ? void 0 : inventory.screens) != null ? _ref : []).map((screen)=>screen.slug),
1021
1097
  pageLimit: aiBuildPageLimit(freeTaste),
1022
1098
  siteLayout: aiSiteLayoutOf(inventory)
1023
- }) : answered);
1024
- },
1099
+ }) : site ? aiSettleSiteForeignDatasets(answered, inventory, capabilities) : answered);
1100
+ }
1101
+ }, site ? {
1102
+ settleForms: (placed)=>aiSettlePlanHomeForms(placed, inventory, {
1103
+ keep: aiSiteFormStaysHome(job, capabilities == null ? void 0 : capabilities.freeSitePages)
1104
+ })
1105
+ } : {}, {
1025
1106
  extend: aiSiteWithEventItemsCheck(planViolations(scope, job.brief, freeTaste, ops, site ? aiSitePlanHomeRule(inventory, capabilities) : null, site && aiSiteContentPart((_job_inputs = job.inputs) != null ? _job_inputs : null, freeTaste) === 'posts', site, site && ((_aiSiteKindOfInputs = aiSiteKindOfInputs(job.inputs)) == null ? void 0 : _aiSiteKindOfInputs.id) === 'store'), site ? siteEvents : undefined, ops)
1026
1107
  }));
1027
1108
  const spent = _extends({