@aglyn/plugins-ai 1.0.0-beta.225 → 1.0.0-beta.227
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/README.md +2 -1
- package/package.json +17 -17
- package/src/lib/billing/ai-month-meter.js +5 -3
- package/src/lib/billing/ai-month-meter.js.map +1 -1
- package/src/lib/billing/ai-overage-ledger.js +3 -1
- package/src/lib/billing/ai-overage-ledger.js.map +1 -1
- package/src/lib/components/ai-brief-dialog.component.d.ts +6 -2
- package/src/lib/components/ai-brief-dialog.component.js +35 -17
- package/src/lib/components/ai-brief-dialog.component.js.map +1 -1
- package/src/lib/components/ai-describe-automation.component.js +17 -2
- package/src/lib/components/ai-describe-automation.component.js.map +1 -1
- package/src/lib/components/ai-insight-dialog.component.js +24 -2
- package/src/lib/components/ai-insight-dialog.component.js.map +1 -1
- package/src/lib/components/ai-job-events.d.ts +14 -0
- package/src/lib/components/ai-job-events.js +76 -0
- package/src/lib/components/ai-job-events.js.map +1 -0
- package/src/lib/components/ai-job-follow.component.d.ts +49 -0
- package/src/lib/components/ai-job-follow.component.js +210 -0
- package/src/lib/components/ai-job-follow.component.js.map +1 -0
- package/src/lib/components/ai-job-links.d.ts +42 -0
- package/src/lib/components/ai-job-links.js +140 -0
- package/src/lib/components/ai-job-links.js.map +1 -0
- package/src/lib/components/ai-job-plan.component.d.ts +13 -6
- package/src/lib/components/ai-job-plan.component.js +29 -3
- package/src/lib/components/ai-job-plan.component.js.map +1 -1
- package/src/lib/components/ai-job-requests.d.ts +24 -0
- package/src/lib/components/ai-job-requests.js +76 -0
- package/src/lib/components/ai-job-requests.js.map +1 -0
- package/src/lib/components/ai-jobs-indicator.component.d.ts +25 -0
- package/src/lib/components/ai-jobs-indicator.component.js +80 -0
- package/src/lib/components/ai-jobs-indicator.component.js.map +1 -0
- package/src/lib/components/ai-jobs-store.d.ts +81 -0
- package/src/lib/components/ai-jobs-store.js +218 -0
- package/src/lib/components/ai-jobs-store.js.map +1 -0
- package/src/lib/components/ai-page-brief-dialog.component.d.ts +3 -3
- package/src/lib/components/ai-page-brief-dialog.component.js.map +1 -1
- package/src/lib/components/ai-site-build-page.component.d.ts +23 -0
- package/src/lib/components/ai-site-build-page.component.js +377 -0
- package/src/lib/components/ai-site-build-page.component.js.map +1 -0
- package/src/lib/components/ai-site-start-card.component.d.ts +1 -1
- package/src/lib/components/ai-site-start-card.component.js +522 -130
- package/src/lib/components/ai-site-start-card.component.js.map +1 -1
- package/src/lib/components/ai-site-starter-fallback.component.d.ts +24 -0
- package/src/lib/components/ai-site-starter-fallback.component.js +112 -0
- package/src/lib/components/ai-site-starter-fallback.component.js.map +1 -0
- package/src/lib/components/assist-jobs-drawer.component.d.ts +18 -20
- package/src/lib/components/assist-jobs-drawer.component.js +152 -172
- package/src/lib/components/assist-jobs-drawer.component.js.map +1 -1
- package/src/lib/components/assist-panel.component.js +62 -6
- package/src/lib/components/assist-panel.component.js.map +1 -1
- package/src/lib/components/staff-ai-credit-return.component.d.ts +55 -0
- package/src/lib/components/staff-ai-credit-return.component.js +346 -0
- package/src/lib/components/staff-ai-credit-return.component.js.map +1 -0
- package/src/lib/components/staff-org-ai-card.component.js +364 -279
- package/src/lib/components/staff-org-ai-card.component.js.map +1 -1
- package/src/lib/components/staff-user-ai-usage-card.component.d.ts +4 -0
- package/src/lib/components/staff-user-ai-usage-card.component.js +83 -28
- package/src/lib/components/staff-user-ai-usage-card.component.js.map +1 -1
- package/src/lib/components/use-ai-job-run.js +5 -18
- package/src/lib/components/use-ai-job-run.js.map +1 -1
- package/src/lib/jobs/ai-job-auto-confirm.d.ts +52 -0
- package/src/lib/jobs/ai-job-auto-confirm.js +62 -0
- package/src/lib/jobs/ai-job-auto-confirm.js.map +1 -0
- package/src/lib/jobs/ai-job-plan-step.d.ts +33 -2
- package/src/lib/jobs/ai-job-plan-step.js +161 -17
- package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
- package/src/lib/jobs/ai-job-site-step.d.ts +7 -0
- package/src/lib/jobs/ai-job-site-step.js +34 -4
- package/src/lib/jobs/ai-job-site-step.js.map +1 -1
- package/src/lib/jobs/ai-job-text-step.d.ts +7 -0
- package/src/lib/jobs/ai-job-text-step.js.map +1 -1
- package/src/lib/jobs/ai-jobs-notify.d.ts +37 -0
- package/src/lib/jobs/ai-jobs-notify.js +51 -0
- package/src/lib/jobs/ai-jobs-notify.js.map +1 -0
- package/src/lib/jobs/ai-jobs.d.ts +31 -4
- package/src/lib/jobs/ai-jobs.js +196 -32
- package/src/lib/jobs/ai-jobs.js.map +1 -1
- package/src/lib/model/ai-job-activity.d.ts +112 -0
- package/src/lib/model/ai-job-activity.js +144 -0
- package/src/lib/model/ai-job-activity.js.map +1 -0
- package/src/lib/model/ai-job-failure-copy.d.ts +69 -0
- package/src/lib/model/ai-job-failure-copy.js +78 -0
- package/src/lib/model/ai-job-failure-copy.js.map +1 -0
- package/src/lib/model/ai-job-notice.d.ts +61 -0
- package/src/lib/model/ai-job-notice.js +112 -0
- package/src/lib/model/ai-job-notice.js.map +1 -0
- package/src/lib/model/ai-jobs.types.d.ts +26 -0
- package/src/lib/model/ai-jobs.types.js +1 -0
- package/src/lib/model/ai-jobs.types.js.map +1 -1
- package/src/lib/model/ai-plan-capabilities.d.ts +7 -0
- package/src/lib/model/ai-plan-capabilities.js.map +1 -1
- package/src/lib/model/ai-site-build-progress.d.ts +45 -0
- package/src/lib/model/ai-site-build-progress.js +76 -0
- package/src/lib/model/ai-site-build-progress.js.map +1 -0
- package/src/lib/model/ai-site-inventory.d.ts +9 -0
- package/src/lib/model/ai-site-inventory.js +7 -2
- package/src/lib/model/ai-site-inventory.js.map +1 -1
- package/src/lib/model/ai-site-job.d.ts +93 -2
- package/src/lib/model/ai-site-job.js +88 -7
- package/src/lib/model/ai-site-job.js.map +1 -1
- package/src/lib/model/ai-site-start.d.ts +3 -1
- package/src/lib/model/ai-site-start.js +6 -4
- package/src/lib/model/ai-site-start.js.map +1 -1
- package/src/lib/model/ai-template-subjects.d.ts +4 -3
- package/src/lib/model/ai-template-subjects.js +5 -4
- package/src/lib/model/ai-template-subjects.js.map +1 -1
- package/src/lib/plan-entitlements.d.ts +3 -3
- package/src/lib/plan-entitlements.js +3 -3
- package/src/lib/plan-entitlements.js.map +1 -1
- package/src/lib/plugin.js +28 -0
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/runtime/ai-abuse-guards.d.ts +9 -6
- package/src/lib/runtime/ai-abuse-guards.js +10 -7
- package/src/lib/runtime/ai-abuse-guards.js.map +1 -1
- package/src/lib/runtime/ai-doctrine-validators.js +31 -4
- package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
- package/src/lib/runtime/ai-gate.d.ts +3 -2
- package/src/lib/runtime/ai-gate.js +3 -2
- package/src/lib/runtime/ai-gate.js.map +1 -1
- package/src/lib/runtime/ai-palette.generated.js +12 -4
- package/src/lib/runtime/ai-palette.generated.js.map +1 -1
- package/src/lib/runtime/site-inventory.d.ts +16 -1
- package/src/lib/runtime/site-inventory.js +43 -3
- package/src/lib/runtime/site-inventory.js.map +1 -1
- package/src/lib/server/ai-admin-credits.d.ts +21 -0
- package/src/lib/server/ai-admin-credits.js +213 -0
- package/src/lib/server/ai-admin-credits.js.map +1 -0
- package/src/lib/server/ai-admin-org.js +12 -5
- package/src/lib/server/ai-admin-org.js.map +1 -1
- package/src/lib/server/ai-admin-signals.js +3 -1
- package/src/lib/server/ai-admin-signals.js.map +1 -1
- package/src/lib/server/ai-admin-user.js +12 -1
- package/src/lib/server/ai-admin-user.js.map +1 -1
- package/src/lib/server/ai-allotments.js +3 -2
- package/src/lib/server/ai-allotments.js.map +1 -1
- package/src/lib/server/ai-jobs-route.js +20 -2
- package/src/lib/server/ai-jobs-route.js.map +1 -1
- package/src/lib/server/assist-chat.js +3 -2
- package/src/lib/server/assist-chat.js.map +1 -1
- package/src/lib/server/billing-credits.js +4 -3
- package/src/lib/server/billing-credits.js.map +1 -1
- package/src/lib/server/billing-overage.js +3 -2
- package/src/lib/server/billing-overage.js.map +1 -1
- package/src/lib/server.js +11 -0
- package/src/lib/server.js.map +1 -1
- package/src/lib/usage/assist-credit-returns-write.d.ts +75 -0
- package/src/lib/usage/assist-credit-returns-write.js +150 -0
- package/src/lib/usage/assist-credit-returns-write.js.map +1 -0
- package/src/lib/usage/assist-credit-returns.d.ts +68 -0
- package/src/lib/usage/assist-credit-returns.js +79 -0
- package/src/lib/usage/assist-credit-returns.js.map +1 -0
- package/src/lib/usage/assist-free-taste.js +6 -3
- package/src/lib/usage/assist-free-taste.js.map +1 -1
- package/src/lib/usage/assist-job-refund.d.ts +100 -0
- package/src/lib/usage/assist-job-refund.js +109 -0
- package/src/lib/usage/assist-job-refund.js.map +1 -0
- package/src/lib/usage/assist-usage.js +5 -2
- package/src/lib/usage/assist-usage.js.map +1 -1
- package/src/lib/usage/staff-org-ai.d.ts +31 -1
- package/src/lib/usage/staff-org-ai.js +19 -2
- package/src/lib/usage/staff-org-ai.js.map +1 -1
|
@@ -44,6 +44,84 @@ export declare const AI_SITE_PAGES: {
|
|
|
44
44
|
readonly min: 4;
|
|
45
45
|
readonly max: 8;
|
|
46
46
|
};
|
|
47
|
+
/**
|
|
48
|
+
* How many pages a Free workspace's scaffold builds (AGL-3594): its home page
|
|
49
|
+
* and the one page the brief most needs. A product decision, not a figure the
|
|
50
|
+
* wall derives — the wall's proof (`jobs/ai-job-free-site.spec.ts`) shows a
|
|
51
|
+
* plan and a build of this many pages fit the Free taste with room for one
|
|
52
|
+
* retried section.
|
|
53
|
+
*/
|
|
54
|
+
export declare const AI_SITE_FREE_PAGES: {
|
|
55
|
+
readonly min: 1;
|
|
56
|
+
readonly max: 2;
|
|
57
|
+
};
|
|
58
|
+
/** The pages a scaffold builds for a workspace on the Free taste or on a paid plan. */
|
|
59
|
+
export declare function aiSitePagesBand(freeTaste: boolean | null | undefined): {
|
|
60
|
+
readonly min: number;
|
|
61
|
+
readonly max: number;
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* The job input a guided site start sets so its plan is confirmed for it
|
|
65
|
+
* (AGL-3594); `jobs/ai-job-auto-confirm.ts` says where it is honored.
|
|
66
|
+
*/
|
|
67
|
+
export declare const AI_JOB_AUTO_CONFIRM_INPUT = "autoConfirm";
|
|
68
|
+
/** What a Free site start's page selector says under it (AGL-3594). */
|
|
69
|
+
export declare const AI_SITE_FREE_PAGES_NOTE = "Paid plans can generate more pages.";
|
|
70
|
+
/**
|
|
71
|
+
* The plan's answer ceiling for a scaffold (AGL-3594), Free and paid. A plan
|
|
72
|
+
* is an outline — each page's title, address, search listing and sections by
|
|
73
|
+
* name — and the build steps write everything else, so a scaffold plans with
|
|
74
|
+
* no thinking and at a ceiling sized to the outline of its largest band with
|
|
75
|
+
* room to spare: about 300 tokens a page. The 13,491-token plan a Free site
|
|
76
|
+
* spent 227 of its 300 credits on was its thinking, at the routing table's
|
|
77
|
+
* 8,000 a call.
|
|
78
|
+
*/
|
|
79
|
+
export declare const AI_SITE_PLAN_MAX_TOKENS: {
|
|
80
|
+
readonly free: 2000;
|
|
81
|
+
readonly paid: 4000;
|
|
82
|
+
};
|
|
83
|
+
/** What each exchange of a Free site job comes to at its worst, in credits. */
|
|
84
|
+
export interface AiFreeSiteWorstCase {
|
|
85
|
+
/** The site plan: its answer and its one re-ask, each at its ceiling. */
|
|
86
|
+
plan: number;
|
|
87
|
+
/** The one layout a Free plan includes, built first on a site with none. */
|
|
88
|
+
layout: number;
|
|
89
|
+
/** A page's first section pass, which writes the cached prefix. */
|
|
90
|
+
firstSection: number;
|
|
91
|
+
/** Each later section pass. */
|
|
92
|
+
laterSection: number;
|
|
93
|
+
/** A page's listing. */
|
|
94
|
+
listing: number;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* The Free site start's wall at its worst (AGL-3594): the site plan as
|
|
98
|
+
* `jobs/ai-job-free-site.spec.ts` derives it from the plan request as it
|
|
99
|
+
* stands, on the model a Free site plans with, every answer at its ceiling;
|
|
100
|
+
* the build's exchanges are the Free page's (`AI_FREE_PAGE_WORST_CASE_CREDITS`),
|
|
101
|
+
* which the same spec holds these to. The spec fails when a figure it derives
|
|
102
|
+
* moves and this does not.
|
|
103
|
+
*/
|
|
104
|
+
export declare const AI_FREE_SITE_WORST_CASE_CREDITS: Readonly<AiFreeSiteWorstCase>;
|
|
105
|
+
/**
|
|
106
|
+
* The most sections a Free site's plan fits in the Free taste at its worst,
|
|
107
|
+
* across all its pages: the plan, the layouts it builds first, each page's
|
|
108
|
+
* listing and first pass, and ROOM FOR ONE RETRIED SECTION, then as many later
|
|
109
|
+
* passes as the rest pays for; none when that is already past the wall. Every
|
|
110
|
+
* page's first pass is counted as a cache write, which errs dear.
|
|
111
|
+
*/
|
|
112
|
+
export declare function aiFreeSiteSectionsWithin(creations: {
|
|
113
|
+
layouts: number;
|
|
114
|
+
pages: number;
|
|
115
|
+
}, taste: number, credits?: Readonly<AiFreeSiteWorstCase>): number;
|
|
116
|
+
/** Sections a Free site's page is assumed to hold before its plan names them. */
|
|
117
|
+
export declare const AI_FREE_SITE_NOMINAL_SECTIONS = 3;
|
|
118
|
+
/**
|
|
119
|
+
* About what a Free site start of this many pages costs at its worst, in
|
|
120
|
+
* credits, for the dialog that asks for one: the site plan, each page's
|
|
121
|
+
* listing and its sections at the nominal count, from the figures the wall is
|
|
122
|
+
* proven with rather than the nominal credits a paid estimate counts.
|
|
123
|
+
*/
|
|
124
|
+
export declare function aiFreeSiteCreditEstimate(pages: number, credits?: Readonly<AiFreeSiteWorstCase>): number;
|
|
47
125
|
/**
|
|
48
126
|
* The most sections one scaffolded page may hold. The plan model allows more
|
|
49
127
|
* for a page job, which builds one page; a scaffold builds eight, and each
|
|
@@ -162,6 +240,15 @@ export declare const AI_SITE_EMAIL_TYPE = "reply";
|
|
|
162
240
|
export declare function aiSiteSubmissions(inputs: Readonly<Record<string, unknown>> | null | undefined): AiSiteSubmissions | null;
|
|
163
241
|
/** A scaffold's inputs, or the sentence the door refuses them with. */
|
|
164
242
|
export declare function parseAiSiteJobInputs(inputs: Readonly<Record<string, unknown>> | null | undefined): AiSiteJobInputs | string;
|
|
243
|
+
/**
|
|
244
|
+
* Why a workspace cannot ask for this many pages (AGL-3594), in a sentence a
|
|
245
|
+
* member reads; `null` when it can. A Free workspace builds one or two, and a
|
|
246
|
+
* paid one four to eight, as it always has. Refused rather than clamped: the
|
|
247
|
+
* guided start offers only the band, so a request outside it was not made
|
|
248
|
+
* there, and a job quietly building fewer pages than it was asked for is a
|
|
249
|
+
* worse answer than a sentence saying why.
|
|
250
|
+
*/
|
|
251
|
+
export declare function aiSitePagesRefusal(pages: number, freeTaste: boolean | null | undefined): string | null;
|
|
165
252
|
/**
|
|
166
253
|
* What the site itself is, as a step that is NOT the scaffold reads it off a
|
|
167
254
|
* job's inputs (AGL-2918).
|
|
@@ -205,12 +292,16 @@ export declare function aiSitePlanPrerequisites(plan: AiBuildPlan): Array<{
|
|
|
205
292
|
* with a re-ask (AGL-3030), and the doors hold a confirmed plan to it again
|
|
206
293
|
* through `aiSitePlanRefusal`.
|
|
207
294
|
*/
|
|
208
|
-
export declare function aiSitePlanShapeRefusal(plan: AiBuildPlan
|
|
295
|
+
export declare function aiSitePlanShapeRefusal(plan: AiBuildPlan, options?: {
|
|
296
|
+
freeTaste?: boolean;
|
|
297
|
+
}): string | null;
|
|
209
298
|
/**
|
|
210
299
|
* Why a scaffold cannot build this plan, in a sentence a member reads when
|
|
211
300
|
* confirming it; `null` when it can.
|
|
212
301
|
*/
|
|
213
|
-
export declare function aiSitePlanRefusal(plan: AiBuildPlan
|
|
302
|
+
export declare function aiSitePlanRefusal(plan: AiBuildPlan, options?: {
|
|
303
|
+
freeTaste?: boolean;
|
|
304
|
+
}): string | null;
|
|
214
305
|
/** What the estimate counts a plan in: whether a welcome email follows, and the creations the job builds. */
|
|
215
306
|
export interface AiPlanPassOptions {
|
|
216
307
|
welcomeEmail?: boolean;
|
|
@@ -43,6 +43,72 @@ import { AI_PAGE_CREATE_KINDS } from "./ai-page-job.js";
|
|
|
43
43
|
min: 4,
|
|
44
44
|
max: 8
|
|
45
45
|
};
|
|
46
|
+
/**
|
|
47
|
+
* How many pages a Free workspace's scaffold builds (AGL-3594): its home page
|
|
48
|
+
* and the one page the brief most needs. A product decision, not a figure the
|
|
49
|
+
* wall derives — the wall's proof (`jobs/ai-job-free-site.spec.ts`) shows a
|
|
50
|
+
* plan and a build of this many pages fit the Free taste with room for one
|
|
51
|
+
* retried section.
|
|
52
|
+
*/ export const AI_SITE_FREE_PAGES = {
|
|
53
|
+
min: 1,
|
|
54
|
+
max: 2
|
|
55
|
+
};
|
|
56
|
+
/** The pages a scaffold builds for a workspace on the Free taste or on a paid plan. */ export function aiSitePagesBand(freeTaste) {
|
|
57
|
+
return freeTaste ? AI_SITE_FREE_PAGES : AI_SITE_PAGES;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The job input a guided site start sets so its plan is confirmed for it
|
|
61
|
+
* (AGL-3594); `jobs/ai-job-auto-confirm.ts` says where it is honored.
|
|
62
|
+
*/ export const AI_JOB_AUTO_CONFIRM_INPUT = 'autoConfirm';
|
|
63
|
+
/** What a Free site start's page selector says under it (AGL-3594). */ export const AI_SITE_FREE_PAGES_NOTE = 'Paid plans can generate more pages.';
|
|
64
|
+
/**
|
|
65
|
+
* The plan's answer ceiling for a scaffold (AGL-3594), Free and paid. A plan
|
|
66
|
+
* is an outline — each page's title, address, search listing and sections by
|
|
67
|
+
* name — and the build steps write everything else, so a scaffold plans with
|
|
68
|
+
* no thinking and at a ceiling sized to the outline of its largest band with
|
|
69
|
+
* room to spare: about 300 tokens a page. The 13,491-token plan a Free site
|
|
70
|
+
* spent 227 of its 300 credits on was its thinking, at the routing table's
|
|
71
|
+
* 8,000 a call.
|
|
72
|
+
*/ export const AI_SITE_PLAN_MAX_TOKENS = {
|
|
73
|
+
free: 2000,
|
|
74
|
+
paid: 4000
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* The Free site start's wall at its worst (AGL-3594): the site plan as
|
|
78
|
+
* `jobs/ai-job-free-site.spec.ts` derives it from the plan request as it
|
|
79
|
+
* stands, on the model a Free site plans with, every answer at its ceiling;
|
|
80
|
+
* the build's exchanges are the Free page's (`AI_FREE_PAGE_WORST_CASE_CREDITS`),
|
|
81
|
+
* which the same spec holds these to. The spec fails when a figure it derives
|
|
82
|
+
* moves and this does not.
|
|
83
|
+
*/ export const AI_FREE_SITE_WORST_CASE_CREDITS = {
|
|
84
|
+
plan: 35,
|
|
85
|
+
layout: 64,
|
|
86
|
+
firstSection: 44,
|
|
87
|
+
laterSection: 20,
|
|
88
|
+
listing: 3
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* The most sections a Free site's plan fits in the Free taste at its worst,
|
|
92
|
+
* across all its pages: the plan, the layouts it builds first, each page's
|
|
93
|
+
* listing and first pass, and ROOM FOR ONE RETRIED SECTION, then as many later
|
|
94
|
+
* passes as the rest pays for; none when that is already past the wall. Every
|
|
95
|
+
* page's first pass is counted as a cache write, which errs dear.
|
|
96
|
+
*/ export function aiFreeSiteSectionsWithin(creations, taste, credits = AI_FREE_SITE_WORST_CASE_CREDITS) {
|
|
97
|
+
const pages = Math.max(1, Math.floor(creations.pages));
|
|
98
|
+
const before = credits.plan + creations.layouts * credits.layout + pages * (credits.listing + credits.firstSection) + credits.firstSection;
|
|
99
|
+
if (before > taste) return 0;
|
|
100
|
+
return pages + Math.floor((taste - before) / credits.laterSection);
|
|
101
|
+
}
|
|
102
|
+
/** Sections a Free site's page is assumed to hold before its plan names them. */ export const AI_FREE_SITE_NOMINAL_SECTIONS = 3;
|
|
103
|
+
/**
|
|
104
|
+
* About what a Free site start of this many pages costs at its worst, in
|
|
105
|
+
* credits, for the dialog that asks for one: the site plan, each page's
|
|
106
|
+
* listing and its sections at the nominal count, from the figures the wall is
|
|
107
|
+
* proven with rather than the nominal credits a paid estimate counts.
|
|
108
|
+
*/ export function aiFreeSiteCreditEstimate(pages, credits = AI_FREE_SITE_WORST_CASE_CREDITS) {
|
|
109
|
+
const count = Math.max(1, Math.floor(pages));
|
|
110
|
+
return credits.plan + count * (credits.listing + credits.firstSection) + count * (AI_FREE_SITE_NOMINAL_SECTIONS - 1) * credits.laterSection;
|
|
111
|
+
}
|
|
46
112
|
/**
|
|
47
113
|
* The most sections one scaffolded page may hold. The plan model allows more
|
|
48
114
|
* for a page job, which builds one page; a scaffold builds eight, and each
|
|
@@ -162,8 +228,10 @@ const ID_CHARS = /^[A-Za-z0-9_-]{1,64}$/;
|
|
|
162
228
|
}
|
|
163
229
|
const rawPages = inputs == null ? void 0 : inputs['pages'];
|
|
164
230
|
const pages = typeof rawPages === 'number' ? rawPages : Number(rawPages != null ? rawPages : NaN);
|
|
165
|
-
|
|
166
|
-
|
|
231
|
+
// Read here across both bands; the door holds a workspace to its own
|
|
232
|
+
// (`aiSitePagesRefusal`), since only it knows the workspace's plan.
|
|
233
|
+
if (!Number.isInteger(pages) || pages < AI_SITE_FREE_PAGES.min || pages > AI_SITE_PAGES.max) {
|
|
234
|
+
return `pages must be a whole number from ${AI_SITE_FREE_PAGES.min} to ${AI_SITE_PAGES.max}`;
|
|
167
235
|
}
|
|
168
236
|
// Where submissions go is admitted only as one of the two the form step
|
|
169
237
|
// can actually bind; anything else is nobody having said, and the model
|
|
@@ -185,6 +253,18 @@ const ID_CHARS = /^[A-Za-z0-9_-]{1,64}$/;
|
|
|
185
253
|
batchId
|
|
186
254
|
};
|
|
187
255
|
}
|
|
256
|
+
/**
|
|
257
|
+
* Why a workspace cannot ask for this many pages (AGL-3594), in a sentence a
|
|
258
|
+
* member reads; `null` when it can. A Free workspace builds one or two, and a
|
|
259
|
+
* paid one four to eight, as it always has. Refused rather than clamped: the
|
|
260
|
+
* guided start offers only the band, so a request outside it was not made
|
|
261
|
+
* there, and a job quietly building fewer pages than it was asked for is a
|
|
262
|
+
* worse answer than a sentence saying why.
|
|
263
|
+
*/ export function aiSitePagesRefusal(pages, freeTaste) {
|
|
264
|
+
const band = aiSitePagesBand(freeTaste);
|
|
265
|
+
if (pages >= band.min && pages <= band.max) return null;
|
|
266
|
+
return freeTaste ? `A Free workspace's AI site start builds ${AI_SITE_FREE_PAGES.min} or ${AI_SITE_FREE_PAGES.max} pages. ${AI_SITE_FREE_PAGES_NOTE}` : `A site is planned with ${band.min} to ${band.max} pages.`;
|
|
267
|
+
}
|
|
188
268
|
/** Whether either half of {@link AiSiteWords} says anything. */ export function aiSiteWordsSaidAnything(words) {
|
|
189
269
|
return Boolean(words.about || words.audience);
|
|
190
270
|
}
|
|
@@ -237,10 +317,11 @@ const ID_CHARS = /^[A-Za-z0-9_-]{1,64}$/;
|
|
|
237
317
|
* `null` when the shape is one it builds. The plan step holds a plan to it
|
|
238
318
|
* with a re-ask (AGL-3030), and the doors hold a confirmed plan to it again
|
|
239
319
|
* through `aiSitePlanRefusal`.
|
|
240
|
-
*/ export function aiSitePlanShapeRefusal(plan) {
|
|
320
|
+
*/ export function aiSitePlanShapeRefusal(plan, options = {}) {
|
|
241
321
|
const { screens } = plan;
|
|
242
|
-
|
|
243
|
-
|
|
322
|
+
const band = aiSitePagesBand(options.freeTaste);
|
|
323
|
+
if (screens.length < band.min || screens.length > band.max) {
|
|
324
|
+
return `This plan builds ${screens.length} ${screens.length === 1 ? 'page' : 'pages'}, and ${options.freeTaste ? "a Free workspace's site scaffold" : 'a site scaffold'} builds ${band.min} to ${band.max}. Describe the site again.`;
|
|
244
325
|
}
|
|
245
326
|
const slugs = new Set();
|
|
246
327
|
for (const screen of screens){
|
|
@@ -264,8 +345,8 @@ const ID_CHARS = /^[A-Za-z0-9_-]{1,64}$/;
|
|
|
264
345
|
/**
|
|
265
346
|
* Why a scaffold cannot build this plan, in a sentence a member reads when
|
|
266
347
|
* confirming it; `null` when it can.
|
|
267
|
-
*/ export function aiSitePlanRefusal(plan) {
|
|
268
|
-
const shape = aiSitePlanShapeRefusal(plan);
|
|
348
|
+
*/ export function aiSitePlanRefusal(plan, options = {}) {
|
|
349
|
+
const shape = aiSitePlanShapeRefusal(plan, options);
|
|
269
350
|
if (shape) return shape;
|
|
270
351
|
const prerequisites = aiSitePlanPrerequisites(plan);
|
|
271
352
|
if (!prerequisites.length) return null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-site-job.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n AI_BUILD_PLAN_CREATION_NOUNS,\n aiPlanUndeclaredRefs,\n type AiBuildPlan,\n type AiBuildPlanCreateKind,\n} from './ai-build-plan'\nimport { AI_JOB_CREATE_KINDS } from './ai-job-creations'\nimport { AI_PAGE_CREATE_KINDS } from './ai-page-job'\n\n/**\n * What a site scaffold is (AGL-2911): the inputs a `site` job is admitted\n * with, the plans a scaffold can build, and what one is estimated to cost\n * before it starts.\n *\n * A scaffold is the one job kind that builds a whole site rather than one\n * record: a plan of four to eight screens, the layout they render inside,\n * the contact form they place, a palette suggestion, and — where the email\n * step is loaded — a welcome email. It builds each of those through the job\n * kind that already owns it, so this module describes what a scaffold may\n * ask for and refuses the rest at confirmation, before a credit is spent,\n * naming what to make first.\n *\n * ── The estimate is the guard rail ───────────────────────────────────────\n *\n * A scaffold is tens of model calls, where every other kind is a few, so the\n * member reads what it will cost before they confirm the plan that spends\n * it. The figure is the machine's own nominal credits per step multiplied by\n * the passes the plan implies — an estimate, and said to be one wherever it\n * is shown.\n *\n * This module imports nothing at runtime, so the doors, the step, the batch\n * card and the plan proposal read one vocabulary.\n */\n\n/** How many pages a scaffold builds: enough to be a site, few enough to read. */\nexport const AI_SITE_PAGES = { min: 4, max: 8 } as const\n\n/**\n * The most sections one scaffolded page may hold. The plan model allows more\n * for a page job, which builds one page; a scaffold builds eight, and each\n * section is a pass of its own.\n */\nexport const AI_SITE_MAX_SECTIONS = 8\n\n/**\n * Sections a page is assumed to hold before a plan names them, for the\n * estimate a member reads when they ask for a scaffold. A declared\n * assumption, not a measurement: the plan's own section counts replace it\n * the moment the plan exists.\n */\nexport const AI_SITE_NOMINAL_SECTIONS = 5\n\n/**\n * The credits one pass is held against: the machine's\n * `AI_JOB_STEP_RESERVE_CREDITS`, declared here so the estimate stays a pure\n * number the console computes without the Admin SDK. `ai-site-job.spec.ts`\n * holds the two equal.\n */\nexport const AI_SITE_PASS_CREDITS = 50\n\n/** The longest a scaffold's scalar inputs may run; they are names, not copy. */\nexport const AI_SITE_INPUT_MAX_CHARS = 120\n\n/** The sites one agency batch may generate for in one request. */\nexport const AI_SITE_BATCH_MAX = 25\n\n/**\n * The sites a plan must hold before the org Sites page offers the batch at\n * all: the Agency and Advanced bands, which the plan's `hostLimit` is what\n * distinguishes.\n */\nexport const AI_SITE_BATCH_MIN_HOST_LIMIT = 25\n\nexport interface AiSiteJobInputs {\n /** What the business is, in a few words: the brief's subject. */\n businessType: string\n /**\n * Who the site is for, in a few words (AGL-2918); empty when nobody said.\n * The plan step puts it in front of the model on a line of its own, as it\n * does every other scalar here, so it narrows the pages a plan proposes\n * without a prompt of its own.\n */\n audience: string\n /**\n * The starter site whose shape the person liked (AGL-2918), by its\n * `STARTER_TEMPLATES` id; empty when they picked none.\n *\n * A hint carried as the id rather than the starter, because an id is what\n * the guided start's list, this door and the plan's line all agree on\n * without any of them loading a catalog of node maps. Unrecognized ids are\n * admitted for the same reason an unrecognized business type is: it is a\n * few words in a brief, not a lookup.\n */\n starter: string\n /** How many pages the member asked for. */\n pages: number\n /** The per-site variables an agency batch varies; empty when none was given. */\n businessName: string\n city: string\n brand: string\n /** Whether the scaffold also drafts a welcome email. */\n welcomeEmail: boolean\n /**\n * Where the contact form's submissions go (AGL-2918); `null` where nobody\n * said, which is every job created before the question existed and every\n * door that does not ask it.\n *\n * The one setting a new site owner actually needs made, and the one thing\n * about a contact form a model cannot know: whether a submission is a note\n * to read or a lead to chase is a decision about how the business runs. So\n * an answer BINDS the form step rather than hinting to it, and `null`\n * leaves the model's own proposal standing, exactly as it stood before.\n */\n submissions: AiSiteSubmissions | null\n /** The batch this job was created under (AGL-2911); `null` for a single site. */\n batchId: string | null\n}\n\n/**\n * Where a site's contact form sends what a visitor writes.\n *\n * Two, not three. The form step's own vocabulary has a third — a mailing\n * list — which it can only answer with a note, because the stored routing has\n * no place for a list; offering it as a question would be asking somebody to\n * pick an outcome the platform then explains it cannot store.\n */\nexport const AI_SITE_SUBMISSIONS = ['inbox', 'lead'] as const\n\nexport type AiSiteSubmissions = (typeof AI_SITE_SUBMISSIONS)[number]\n\n/**\n * What each answer to \"where do submissions go\" is called, and what it means.\n *\n * Here rather than beside the guided start's own questions, because it is not\n * only the guided start that asks: the agency batch asks the same thing of a\n * whole run (AGL-2918), and two surfaces wording one decision two ways is two\n * decisions. `ai-site-job.spec.ts` holds the list to {@link AI_SITE_SUBMISSIONS}.\n */\nexport const AI_SITE_SUBMISSION_CHOICES: ReadonlyArray<{\n id: AiSiteSubmissions\n label: string\n blurb: string\n}> = [\n {\n id: 'inbox',\n label: 'The Inbox',\n blurb: 'Every message is one to read and reply to.',\n },\n {\n id: 'lead',\n label: 'The Inbox, and CRM as a lead',\n blurb: 'Every message with an email address is also somebody to follow up.',\n },\n]\n\n/**\n * Which kind of email a scaffold's email unit is (AGL-2918), carried on the\n * derived job as `inputs.emailType`. The email step names the kind on the one\n * line it puts above the brief, and a scaffold was naming none.\n *\n * A key of the email step's own `AI_EMAIL_TYPES`, held here because neither\n * step may import the other: a scaffold delegates by job kind, and a\n * deployment that scaffolds is not necessarily one that has loaded the email\n * step. `ai-job-email-step.spec.ts` holds this key to that catalog.\n *\n * `reply` and not `welcome`. The five kinds a member picks are campaigns\n * somebody sends to an audience, and this is an answer to one person who\n * wrote in through the site; the nearest, `welcome`, is a label about signing\n * up, and a kind that misdescribes the email is worse than an absent one\n * because it is the first thing the model is told.\n */\nexport const AI_SITE_EMAIL_TYPE = 'reply'\n\n/** Where a job's inputs say submissions go, or `null` where they do not say. */\nexport function aiSiteSubmissions(\n inputs: Readonly<Record<string, unknown>> | null | undefined,\n): AiSiteSubmissions | null {\n const value = inputs?.['submissions']\n return (AI_SITE_SUBMISSIONS as readonly unknown[]).includes(value)\n ? (value as AiSiteSubmissions)\n : null\n}\n\nconst ID_CHARS = /^[A-Za-z0-9_-]{1,64}$/\n\n/** A scaffold's inputs, or the sentence the door refuses them with. */\nexport function parseAiSiteJobInputs(\n inputs: Readonly<Record<string, unknown>> | null | undefined,\n): AiSiteJobInputs | string {\n const text = (key: string): string | null => {\n const value = inputs?.[key]\n if (value === undefined || value === null || value === '') return ''\n if (typeof value !== 'string') return null\n const trimmed = value.trim()\n return trimmed.length > AI_SITE_INPUT_MAX_CHARS ? null : trimmed\n }\n const businessType = text('businessType')\n if (businessType === null) {\n return `businessType must be text under ${AI_SITE_INPUT_MAX_CHARS} characters`\n }\n if (!businessType) return 'Say what kind of business the site is for'\n const businessName = text('businessName')\n const city = text('city')\n const brand = text('brand')\n const audience = text('audience')\n const starter = text('starter')\n for (const [key, value] of [\n ['businessName', businessName],\n ['city', city],\n ['brand', brand],\n ['audience', audience],\n ['starter', starter],\n ] as const) {\n if (value === null)\n return `${key} must be text under ${AI_SITE_INPUT_MAX_CHARS} characters`\n }\n const rawPages = inputs?.['pages']\n const pages =\n typeof rawPages === 'number' ? rawPages : Number(rawPages ?? NaN)\n if (\n !Number.isInteger(pages) ||\n pages < AI_SITE_PAGES.min ||\n pages > AI_SITE_PAGES.max\n ) {\n return `pages must be a whole number from ${AI_SITE_PAGES.min} to ${AI_SITE_PAGES.max}`\n }\n // Where submissions go is admitted only as one of the two the form step\n // can actually bind; anything else is nobody having said, and the model\n // proposes as it always did rather than the door refusing the whole job.\n const submissions = aiSiteSubmissions(inputs)\n const rawBatch = inputs?.['batchId']\n const batchId =\n rawBatch === undefined || rawBatch === null || rawBatch === ''\n ? null\n : typeof rawBatch === 'string' && ID_CHARS.test(rawBatch)\n ? rawBatch\n : undefined\n if (batchId === undefined) return 'batchId is not a batch id'\n return {\n businessType,\n audience: audience as string,\n starter: starter as string,\n pages,\n businessName: businessName as string,\n city: city as string,\n brand: brand as string,\n welcomeEmail: inputs?.['welcomeEmail'] !== false,\n submissions,\n batchId,\n }\n}\n\n/**\n * What the site itself is, as a step that is NOT the scaffold reads it off a\n * job's inputs (AGL-2918).\n *\n * A scaffold delegates unit by unit under a job derived from its own — a page\n * job, a form job — and the derived job carries the scaffold's inputs. So the\n * two answers that describe the site rather than one record of it are\n * readable by every delegated step, and a step that wants them does not have\n * to know it was delegated to.\n *\n * Lenient where {@link parseAiSiteJobInputs} is strict, because it reads the\n * inputs of jobs that are not scaffolds: a page job started from the Screens\n * page has no `businessType` and no `pages`, and that is not an error here —\n * it is a site nobody described, and the answer is two empty strings.\n */\nexport interface AiSiteWords {\n /** What kind of site it is; empty when the job's inputs do not say. */\n about: string\n /** Who it is for; empty when the job's inputs do not say. */\n audience: string\n}\n\n/** Whether either half of {@link AiSiteWords} says anything. */\nexport function aiSiteWordsSaidAnything(words: AiSiteWords): boolean {\n return Boolean(words.about || words.audience)\n}\n\n/** What a job's inputs say the site is, read defensively and trimmed to the input ceiling. */\nexport function aiSiteWords(\n inputs: Readonly<Record<string, unknown>> | null | undefined,\n): AiSiteWords {\n const read = (key: string): string => {\n const value = inputs?.[key]\n if (typeof value !== 'string') return ''\n return value.replace(/\\s+/g, ' ').trim().slice(0, AI_SITE_INPUT_MAX_CHARS).trim()\n }\n return { about: read('businessType'), audience: read('audience') }\n}\n\n/**\n * What a scaffold builds for itself: the layout its pages render inside, the\n * form they place, and the palette suggestion a member applies in the Theme\n * section. Anything else a plan asks to create is a job of its own.\n */\nexport const AI_SITE_CREATE_KINDS: readonly AiBuildPlanCreateKind[] = [\n 'layout',\n 'form',\n 'theme-change',\n]\n\n/** The creations a scaffold cannot build, in the order the plan lists them, each named once. */\nexport function aiSitePlanPrerequisites(\n plan: AiBuildPlan,\n): Array<{ kind: AiBuildPlanCreateKind; name: string }> {\n const seen = new Set<string>()\n const prerequisites: Array<{ kind: AiBuildPlanCreateKind; name: string }> = []\n const add = (kind: AiBuildPlanCreateKind, name: string) => {\n const key = `${kind}:${name.toLowerCase()}`\n if (seen.has(key)) return\n seen.add(key)\n prerequisites.push({ kind, name })\n }\n for (const entry of plan.create) {\n if (!AI_SITE_CREATE_KINDS.includes(entry.kind)) add(entry.kind, entry.name)\n }\n // A reference the create list does not carry is refused by the plan rules\n // before a plan is kept: rule 2 for a screen's layout, rule 7 for anything\n // else (`plan-creation-undeclared`, AGL-3040). A plan confirmed before that\n // rule existed can still carry one. It is named as a component: the\n // reference carries no kind, and the refusal must say where one is made.\n for (const { name } of aiPlanUndeclaredRefs(plan)) add('component', name)\n return prerequisites\n}\n\n/**\n * Why a scaffold cannot build a plan of this SHAPE — the page band, a page's\n * sections, its addresses, its navigation — in a sentence a member reads;\n * `null` when the shape is one it builds. The plan step holds a plan to it\n * with a re-ask (AGL-3030), and the doors hold a confirmed plan to it again\n * through `aiSitePlanRefusal`.\n */\nexport function aiSitePlanShapeRefusal(plan: AiBuildPlan): string | null {\n const { screens } = plan\n if (\n screens.length < AI_SITE_PAGES.min ||\n screens.length > AI_SITE_PAGES.max\n ) {\n return `This plan builds ${screens.length} ${screens.length === 1 ? 'page' : 'pages'}, and a site scaffold builds ${AI_SITE_PAGES.min} to ${AI_SITE_PAGES.max}. Describe the site again.`\n }\n const slugs = new Set<string>()\n for (const screen of screens) {\n if (!screen.sections.length) {\n return `The page “${screen.title}” has no sections to build. Describe the site again.`\n }\n if (screen.sections.length > AI_SITE_MAX_SECTIONS) {\n return `The page “${screen.title}” has ${screen.sections.length} sections, and a scaffolded page holds ${AI_SITE_MAX_SECTIONS}. Describe the site again.`\n }\n const slug = screen.slug.trim().toLowerCase()\n if (slugs.has(slug)) {\n return `Two pages in this plan share the address ${screen.slug}. Describe the site again.`\n }\n slugs.add(slug)\n }\n if (!screens.some((screen) => screen.nav)) {\n return 'No page in this plan is in the site navigation. Describe the site again.'\n }\n return null\n}\n\n/**\n * Why a scaffold cannot build this plan, in a sentence a member reads when\n * confirming it; `null` when it can.\n */\nexport function aiSitePlanRefusal(plan: AiBuildPlan): string | null {\n const shape = aiSitePlanShapeRefusal(plan)\n if (shape) return shape\n const prerequisites = aiSitePlanPrerequisites(plan)\n if (!prerequisites.length) return null\n const parts = prerequisites.map(\n ({ kind, name }) =>\n `the ${AI_BUILD_PLAN_CREATION_NOUNS[kind].noun} “${name}” ${AI_BUILD_PLAN_CREATION_NOUNS[kind].where}`,\n )\n const listed =\n parts.length === 1\n ? parts[0]\n : `${parts.slice(0, -1).join(', ')} and ${parts[parts.length - 1]}`\n return `This site needs what the workspace does not have yet. Create ${listed}, then describe the site again.`\n}\n\n/** What the estimate counts a plan in: whether a welcome email follows, and the creations the job builds. */\nexport interface AiPlanPassOptions {\n welcomeEmail?: boolean\n /** The creation kinds the job builds itself; a scaffold's when absent. */\n creates?: readonly AiBuildPlanCreateKind[]\n}\n\n/**\n * The passes a plan implies: one per section of every screen, one more per\n * screen for its search listing and its draft, and one for each thing the\n * plan creates that the job builds. What the estimate is counted in.\n */\nexport function aiPlanPasses(plan: AiBuildPlan, options: AiPlanPassOptions = {}): number {\n const screens = plan.screens.reduce(\n (total, screen) => total + screen.sections.length + 1,\n 0,\n )\n const creates = options.creates ?? AI_SITE_CREATE_KINDS\n const creations = plan.create.filter((entry) => creates.includes(entry.kind)).length\n return screens + creations + (options.welcomeEmail ? 1 : 0)\n}\n\n/**\n * About what a plan costs to build, in credits. An ESTIMATE: the nominal\n * credits the machine holds per step, times the passes the plan implies.\n * What each pass actually costs is its model's tokens, recorded on the job\n * as it runs.\n */\nexport function aiPlanCreditEstimate(plan: AiBuildPlan, options: AiPlanPassOptions = {}): number {\n return aiPlanPasses(plan, options) * AI_SITE_PASS_CREDITS\n}\n\n/**\n * About what a job of this kind costs to build from its plan (AGL-3031): a\n * page job counts the layout, forms and components it builds before its\n * page; a template, layout, component, form or email job counts its own\n * record and every creation it builds before it (AGL-3143 §15); and a\n * scaffold counts what it builds. The figure the proposal shows beside\n * Confirm, and the hold a confirmation takes.\n */\nexport function aiJobPlanCreditEstimate(kind: string, plan: AiBuildPlan): number {\n const creates =\n kind === 'page' ? AI_PAGE_CREATE_KINDS : AI_JOB_CREATE_KINDS[kind as keyof typeof AI_JOB_CREATE_KINDS]\n return aiPlanCreditEstimate(plan, creates ? { creates } : {})\n}\n\n/**\n * About what a scaffold of this many pages costs, before a plan names its\n * sections: the same arithmetic on the nominal section count, plus the plan\n * step that proposes it. Shown where a member asks for a scaffold.\n */\nexport function aiSiteCreditEstimate(\n pages: number,\n options: { welcomeEmail?: boolean } = {},\n): number {\n const screens = Math.max(0, Math.floor(pages))\n const passes =\n 1 +\n screens * (AI_SITE_NOMINAL_SECTIONS + 1) +\n 2 +\n (options.welcomeEmail ? 1 : 0)\n return passes * AI_SITE_PASS_CREDITS\n}\n"],"names":["AI_BUILD_PLAN_CREATION_NOUNS","aiPlanUndeclaredRefs","AI_JOB_CREATE_KINDS","AI_PAGE_CREATE_KINDS","AI_SITE_PAGES","min","max","AI_SITE_MAX_SECTIONS","AI_SITE_NOMINAL_SECTIONS","AI_SITE_PASS_CREDITS","AI_SITE_INPUT_MAX_CHARS","AI_SITE_BATCH_MAX","AI_SITE_BATCH_MIN_HOST_LIMIT","AI_SITE_SUBMISSIONS","AI_SITE_SUBMISSION_CHOICES","id","label","blurb","AI_SITE_EMAIL_TYPE","aiSiteSubmissions","inputs","value","includes","ID_CHARS","parseAiSiteJobInputs","text","key","undefined","trimmed","trim","length","businessType","businessName","city","brand","audience","starter","rawPages","pages","Number","NaN","isInteger","submissions","rawBatch","batchId","test","welcomeEmail","aiSiteWordsSaidAnything","words","Boolean","about","aiSiteWords","read","replace","slice","AI_SITE_CREATE_KINDS","aiSitePlanPrerequisites","plan","seen","Set","prerequisites","add","kind","name","toLowerCase","has","push","entry","create","aiSitePlanShapeRefusal","screens","slugs","screen","sections","title","slug","some","nav","aiSitePlanRefusal","shape","parts","map","noun","where","listed","join","aiPlanPasses","options","reduce","total","creates","creations","filter","aiPlanCreditEstimate","aiJobPlanCreditEstimate","aiSiteCreditEstimate","Math","floor","passes"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-site-job.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n AI_BUILD_PLAN_CREATION_NOUNS,\n aiPlanUndeclaredRefs,\n type AiBuildPlan,\n type AiBuildPlanCreateKind,\n} from './ai-build-plan'\nimport { AI_JOB_CREATE_KINDS } from './ai-job-creations'\nimport { AI_PAGE_CREATE_KINDS } from './ai-page-job'\n\n/**\n * What a site scaffold is (AGL-2911): the inputs a `site` job is admitted\n * with, the plans a scaffold can build, and what one is estimated to cost\n * before it starts.\n *\n * A scaffold is the one job kind that builds a whole site rather than one\n * record: a plan of four to eight screens, the layout they render inside,\n * the contact form they place, a palette suggestion, and — where the email\n * step is loaded — a welcome email. It builds each of those through the job\n * kind that already owns it, so this module describes what a scaffold may\n * ask for and refuses the rest at confirmation, before a credit is spent,\n * naming what to make first.\n *\n * ── The estimate is the guard rail ───────────────────────────────────────\n *\n * A scaffold is tens of model calls, where every other kind is a few, so the\n * member reads what it will cost before they confirm the plan that spends\n * it. The figure is the machine's own nominal credits per step multiplied by\n * the passes the plan implies — an estimate, and said to be one wherever it\n * is shown.\n *\n * This module imports nothing at runtime, so the doors, the step, the batch\n * card and the plan proposal read one vocabulary.\n */\n\n/** How many pages a scaffold builds: enough to be a site, few enough to read. */\nexport const AI_SITE_PAGES = { min: 4, max: 8 } as const\n\n/**\n * How many pages a Free workspace's scaffold builds (AGL-3594): its home page\n * and the one page the brief most needs. A product decision, not a figure the\n * wall derives — the wall's proof (`jobs/ai-job-free-site.spec.ts`) shows a\n * plan and a build of this many pages fit the Free taste with room for one\n * retried section.\n */\nexport const AI_SITE_FREE_PAGES = { min: 1, max: 2 } as const\n\n/** The pages a scaffold builds for a workspace on the Free taste or on a paid plan. */\nexport function aiSitePagesBand(\n freeTaste: boolean | null | undefined,\n): { readonly min: number; readonly max: number } {\n return freeTaste ? AI_SITE_FREE_PAGES : AI_SITE_PAGES\n}\n\n/**\n * The job input a guided site start sets so its plan is confirmed for it\n * (AGL-3594); `jobs/ai-job-auto-confirm.ts` says where it is honored.\n */\nexport const AI_JOB_AUTO_CONFIRM_INPUT = 'autoConfirm'\n\n/** What a Free site start's page selector says under it (AGL-3594). */\nexport const AI_SITE_FREE_PAGES_NOTE = 'Paid plans can generate more pages.'\n\n/**\n * The plan's answer ceiling for a scaffold (AGL-3594), Free and paid. A plan\n * is an outline — each page's title, address, search listing and sections by\n * name — and the build steps write everything else, so a scaffold plans with\n * no thinking and at a ceiling sized to the outline of its largest band with\n * room to spare: about 300 tokens a page. The 13,491-token plan a Free site\n * spent 227 of its 300 credits on was its thinking, at the routing table's\n * 8,000 a call.\n */\nexport const AI_SITE_PLAN_MAX_TOKENS = { free: 2_000, paid: 4_000 } as const\n\n/** What each exchange of a Free site job comes to at its worst, in credits. */\nexport interface AiFreeSiteWorstCase {\n /** The site plan: its answer and its one re-ask, each at its ceiling. */\n plan: number\n /** The one layout a Free plan includes, built first on a site with none. */\n layout: number\n /** A page's first section pass, which writes the cached prefix. */\n firstSection: number\n /** Each later section pass. */\n laterSection: number\n /** A page's listing. */\n listing: number\n}\n\n/**\n * The Free site start's wall at its worst (AGL-3594): the site plan as\n * `jobs/ai-job-free-site.spec.ts` derives it from the plan request as it\n * stands, on the model a Free site plans with, every answer at its ceiling;\n * the build's exchanges are the Free page's (`AI_FREE_PAGE_WORST_CASE_CREDITS`),\n * which the same spec holds these to. The spec fails when a figure it derives\n * moves and this does not.\n */\nexport const AI_FREE_SITE_WORST_CASE_CREDITS: Readonly<AiFreeSiteWorstCase> = {\n plan: 35,\n layout: 64,\n firstSection: 44,\n laterSection: 20,\n listing: 3,\n}\n\n/**\n * The most sections a Free site's plan fits in the Free taste at its worst,\n * across all its pages: the plan, the layouts it builds first, each page's\n * listing and first pass, and ROOM FOR ONE RETRIED SECTION, then as many later\n * passes as the rest pays for; none when that is already past the wall. Every\n * page's first pass is counted as a cache write, which errs dear.\n */\nexport function aiFreeSiteSectionsWithin(\n creations: { layouts: number; pages: number },\n taste: number,\n credits: Readonly<AiFreeSiteWorstCase> = AI_FREE_SITE_WORST_CASE_CREDITS,\n): number {\n const pages = Math.max(1, Math.floor(creations.pages))\n const before =\n credits.plan +\n creations.layouts * credits.layout +\n pages * (credits.listing + credits.firstSection) +\n credits.firstSection\n if (before > taste) return 0\n return pages + Math.floor((taste - before) / credits.laterSection)\n}\n\n/** Sections a Free site's page is assumed to hold before its plan names them. */\nexport const AI_FREE_SITE_NOMINAL_SECTIONS = 3\n\n/**\n * About what a Free site start of this many pages costs at its worst, in\n * credits, for the dialog that asks for one: the site plan, each page's\n * listing and its sections at the nominal count, from the figures the wall is\n * proven with rather than the nominal credits a paid estimate counts.\n */\nexport function aiFreeSiteCreditEstimate(\n pages: number,\n credits: Readonly<AiFreeSiteWorstCase> = AI_FREE_SITE_WORST_CASE_CREDITS,\n): number {\n const count = Math.max(1, Math.floor(pages))\n return (\n credits.plan +\n count * (credits.listing + credits.firstSection) +\n count * (AI_FREE_SITE_NOMINAL_SECTIONS - 1) * credits.laterSection\n )\n}\n\n/**\n * The most sections one scaffolded page may hold. The plan model allows more\n * for a page job, which builds one page; a scaffold builds eight, and each\n * section is a pass of its own.\n */\nexport const AI_SITE_MAX_SECTIONS = 8\n\n/**\n * Sections a page is assumed to hold before a plan names them, for the\n * estimate a member reads when they ask for a scaffold. A declared\n * assumption, not a measurement: the plan's own section counts replace it\n * the moment the plan exists.\n */\nexport const AI_SITE_NOMINAL_SECTIONS = 5\n\n/**\n * The credits one pass is held against: the machine's\n * `AI_JOB_STEP_RESERVE_CREDITS`, declared here so the estimate stays a pure\n * number the console computes without the Admin SDK. `ai-site-job.spec.ts`\n * holds the two equal.\n */\nexport const AI_SITE_PASS_CREDITS = 50\n\n/** The longest a scaffold's scalar inputs may run; they are names, not copy. */\nexport const AI_SITE_INPUT_MAX_CHARS = 120\n\n/** The sites one agency batch may generate for in one request. */\nexport const AI_SITE_BATCH_MAX = 25\n\n/**\n * The sites a plan must hold before the org Sites page offers the batch at\n * all: the Agency and Advanced bands, which the plan's `hostLimit` is what\n * distinguishes.\n */\nexport const AI_SITE_BATCH_MIN_HOST_LIMIT = 25\n\nexport interface AiSiteJobInputs {\n /** What the business is, in a few words: the brief's subject. */\n businessType: string\n /**\n * Who the site is for, in a few words (AGL-2918); empty when nobody said.\n * The plan step puts it in front of the model on a line of its own, as it\n * does every other scalar here, so it narrows the pages a plan proposes\n * without a prompt of its own.\n */\n audience: string\n /**\n * The starter site whose shape the person liked (AGL-2918), by its\n * `STARTER_TEMPLATES` id; empty when they picked none.\n *\n * A hint carried as the id rather than the starter, because an id is what\n * the guided start's list, this door and the plan's line all agree on\n * without any of them loading a catalog of node maps. Unrecognized ids are\n * admitted for the same reason an unrecognized business type is: it is a\n * few words in a brief, not a lookup.\n */\n starter: string\n /** How many pages the member asked for. */\n pages: number\n /** The per-site variables an agency batch varies; empty when none was given. */\n businessName: string\n city: string\n brand: string\n /** Whether the scaffold also drafts a welcome email. */\n welcomeEmail: boolean\n /**\n * Where the contact form's submissions go (AGL-2918); `null` where nobody\n * said, which is every job created before the question existed and every\n * door that does not ask it.\n *\n * The one setting a new site owner actually needs made, and the one thing\n * about a contact form a model cannot know: whether a submission is a note\n * to read or a lead to chase is a decision about how the business runs. So\n * an answer BINDS the form step rather than hinting to it, and `null`\n * leaves the model's own proposal standing, exactly as it stood before.\n */\n submissions: AiSiteSubmissions | null\n /** The batch this job was created under (AGL-2911); `null` for a single site. */\n batchId: string | null\n}\n\n/**\n * Where a site's contact form sends what a visitor writes.\n *\n * Two, not three. The form step's own vocabulary has a third — a mailing\n * list — which it can only answer with a note, because the stored routing has\n * no place for a list; offering it as a question would be asking somebody to\n * pick an outcome the platform then explains it cannot store.\n */\nexport const AI_SITE_SUBMISSIONS = ['inbox', 'lead'] as const\n\nexport type AiSiteSubmissions = (typeof AI_SITE_SUBMISSIONS)[number]\n\n/**\n * What each answer to \"where do submissions go\" is called, and what it means.\n *\n * Here rather than beside the guided start's own questions, because it is not\n * only the guided start that asks: the agency batch asks the same thing of a\n * whole run (AGL-2918), and two surfaces wording one decision two ways is two\n * decisions. `ai-site-job.spec.ts` holds the list to {@link AI_SITE_SUBMISSIONS}.\n */\nexport const AI_SITE_SUBMISSION_CHOICES: ReadonlyArray<{\n id: AiSiteSubmissions\n label: string\n blurb: string\n}> = [\n {\n id: 'inbox',\n label: 'The Inbox',\n blurb: 'Every message is one to read and reply to.',\n },\n {\n id: 'lead',\n label: 'The Inbox, and CRM as a lead',\n blurb: 'Every message with an email address is also somebody to follow up.',\n },\n]\n\n/**\n * Which kind of email a scaffold's email unit is (AGL-2918), carried on the\n * derived job as `inputs.emailType`. The email step names the kind on the one\n * line it puts above the brief, and a scaffold was naming none.\n *\n * A key of the email step's own `AI_EMAIL_TYPES`, held here because neither\n * step may import the other: a scaffold delegates by job kind, and a\n * deployment that scaffolds is not necessarily one that has loaded the email\n * step. `ai-job-email-step.spec.ts` holds this key to that catalog.\n *\n * `reply` and not `welcome`. The five kinds a member picks are campaigns\n * somebody sends to an audience, and this is an answer to one person who\n * wrote in through the site; the nearest, `welcome`, is a label about signing\n * up, and a kind that misdescribes the email is worse than an absent one\n * because it is the first thing the model is told.\n */\nexport const AI_SITE_EMAIL_TYPE = 'reply'\n\n/** Where a job's inputs say submissions go, or `null` where they do not say. */\nexport function aiSiteSubmissions(\n inputs: Readonly<Record<string, unknown>> | null | undefined,\n): AiSiteSubmissions | null {\n const value = inputs?.['submissions']\n return (AI_SITE_SUBMISSIONS as readonly unknown[]).includes(value)\n ? (value as AiSiteSubmissions)\n : null\n}\n\nconst ID_CHARS = /^[A-Za-z0-9_-]{1,64}$/\n\n/** A scaffold's inputs, or the sentence the door refuses them with. */\nexport function parseAiSiteJobInputs(\n inputs: Readonly<Record<string, unknown>> | null | undefined,\n): AiSiteJobInputs | string {\n const text = (key: string): string | null => {\n const value = inputs?.[key]\n if (value === undefined || value === null || value === '') return ''\n if (typeof value !== 'string') return null\n const trimmed = value.trim()\n return trimmed.length > AI_SITE_INPUT_MAX_CHARS ? null : trimmed\n }\n const businessType = text('businessType')\n if (businessType === null) {\n return `businessType must be text under ${AI_SITE_INPUT_MAX_CHARS} characters`\n }\n if (!businessType) return 'Say what kind of business the site is for'\n const businessName = text('businessName')\n const city = text('city')\n const brand = text('brand')\n const audience = text('audience')\n const starter = text('starter')\n for (const [key, value] of [\n ['businessName', businessName],\n ['city', city],\n ['brand', brand],\n ['audience', audience],\n ['starter', starter],\n ] as const) {\n if (value === null)\n return `${key} must be text under ${AI_SITE_INPUT_MAX_CHARS} characters`\n }\n const rawPages = inputs?.['pages']\n const pages =\n typeof rawPages === 'number' ? rawPages : Number(rawPages ?? NaN)\n // Read here across both bands; the door holds a workspace to its own\n // (`aiSitePagesRefusal`), since only it knows the workspace's plan.\n if (\n !Number.isInteger(pages) ||\n pages < AI_SITE_FREE_PAGES.min ||\n pages > AI_SITE_PAGES.max\n ) {\n return `pages must be a whole number from ${AI_SITE_FREE_PAGES.min} to ${AI_SITE_PAGES.max}`\n }\n // Where submissions go is admitted only as one of the two the form step\n // can actually bind; anything else is nobody having said, and the model\n // proposes as it always did rather than the door refusing the whole job.\n const submissions = aiSiteSubmissions(inputs)\n const rawBatch = inputs?.['batchId']\n const batchId =\n rawBatch === undefined || rawBatch === null || rawBatch === ''\n ? null\n : typeof rawBatch === 'string' && ID_CHARS.test(rawBatch)\n ? rawBatch\n : undefined\n if (batchId === undefined) return 'batchId is not a batch id'\n return {\n businessType,\n audience: audience as string,\n starter: starter as string,\n pages,\n businessName: businessName as string,\n city: city as string,\n brand: brand as string,\n welcomeEmail: inputs?.['welcomeEmail'] !== false,\n submissions,\n batchId,\n }\n}\n\n/**\n * Why a workspace cannot ask for this many pages (AGL-3594), in a sentence a\n * member reads; `null` when it can. A Free workspace builds one or two, and a\n * paid one four to eight, as it always has. Refused rather than clamped: the\n * guided start offers only the band, so a request outside it was not made\n * there, and a job quietly building fewer pages than it was asked for is a\n * worse answer than a sentence saying why.\n */\nexport function aiSitePagesRefusal(pages: number, freeTaste: boolean | null | undefined): string | null {\n const band = aiSitePagesBand(freeTaste)\n if (pages >= band.min && pages <= band.max) return null\n return freeTaste\n ? `A Free workspace's AI site start builds ${AI_SITE_FREE_PAGES.min} or ${AI_SITE_FREE_PAGES.max} pages. ${AI_SITE_FREE_PAGES_NOTE}`\n : `A site is planned with ${band.min} to ${band.max} pages.`\n}\n\n/**\n * What the site itself is, as a step that is NOT the scaffold reads it off a\n * job's inputs (AGL-2918).\n *\n * A scaffold delegates unit by unit under a job derived from its own — a page\n * job, a form job — and the derived job carries the scaffold's inputs. So the\n * two answers that describe the site rather than one record of it are\n * readable by every delegated step, and a step that wants them does not have\n * to know it was delegated to.\n *\n * Lenient where {@link parseAiSiteJobInputs} is strict, because it reads the\n * inputs of jobs that are not scaffolds: a page job started from the Screens\n * page has no `businessType` and no `pages`, and that is not an error here —\n * it is a site nobody described, and the answer is two empty strings.\n */\nexport interface AiSiteWords {\n /** What kind of site it is; empty when the job's inputs do not say. */\n about: string\n /** Who it is for; empty when the job's inputs do not say. */\n audience: string\n}\n\n/** Whether either half of {@link AiSiteWords} says anything. */\nexport function aiSiteWordsSaidAnything(words: AiSiteWords): boolean {\n return Boolean(words.about || words.audience)\n}\n\n/** What a job's inputs say the site is, read defensively and trimmed to the input ceiling. */\nexport function aiSiteWords(\n inputs: Readonly<Record<string, unknown>> | null | undefined,\n): AiSiteWords {\n const read = (key: string): string => {\n const value = inputs?.[key]\n if (typeof value !== 'string') return ''\n return value.replace(/\\s+/g, ' ').trim().slice(0, AI_SITE_INPUT_MAX_CHARS).trim()\n }\n return { about: read('businessType'), audience: read('audience') }\n}\n\n/**\n * What a scaffold builds for itself: the layout its pages render inside, the\n * form they place, and the palette suggestion a member applies in the Theme\n * section. Anything else a plan asks to create is a job of its own.\n */\nexport const AI_SITE_CREATE_KINDS: readonly AiBuildPlanCreateKind[] = [\n 'layout',\n 'form',\n 'theme-change',\n]\n\n/** The creations a scaffold cannot build, in the order the plan lists them, each named once. */\nexport function aiSitePlanPrerequisites(\n plan: AiBuildPlan,\n): Array<{ kind: AiBuildPlanCreateKind; name: string }> {\n const seen = new Set<string>()\n const prerequisites: Array<{ kind: AiBuildPlanCreateKind; name: string }> = []\n const add = (kind: AiBuildPlanCreateKind, name: string) => {\n const key = `${kind}:${name.toLowerCase()}`\n if (seen.has(key)) return\n seen.add(key)\n prerequisites.push({ kind, name })\n }\n for (const entry of plan.create) {\n if (!AI_SITE_CREATE_KINDS.includes(entry.kind)) add(entry.kind, entry.name)\n }\n // A reference the create list does not carry is refused by the plan rules\n // before a plan is kept: rule 2 for a screen's layout, rule 7 for anything\n // else (`plan-creation-undeclared`, AGL-3040). A plan confirmed before that\n // rule existed can still carry one. It is named as a component: the\n // reference carries no kind, and the refusal must say where one is made.\n for (const { name } of aiPlanUndeclaredRefs(plan)) add('component', name)\n return prerequisites\n}\n\n/**\n * Why a scaffold cannot build a plan of this SHAPE — the page band, a page's\n * sections, its addresses, its navigation — in a sentence a member reads;\n * `null` when the shape is one it builds. The plan step holds a plan to it\n * with a re-ask (AGL-3030), and the doors hold a confirmed plan to it again\n * through `aiSitePlanRefusal`.\n */\nexport function aiSitePlanShapeRefusal(\n plan: AiBuildPlan,\n options: { freeTaste?: boolean } = {},\n): string | null {\n const { screens } = plan\n const band = aiSitePagesBand(options.freeTaste)\n if (screens.length < band.min || screens.length > band.max) {\n return `This plan builds ${screens.length} ${screens.length === 1 ? 'page' : 'pages'}, and ${\n options.freeTaste ? \"a Free workspace's site scaffold\" : 'a site scaffold'\n } builds ${band.min} to ${band.max}. Describe the site again.`\n }\n const slugs = new Set<string>()\n for (const screen of screens) {\n if (!screen.sections.length) {\n return `The page “${screen.title}” has no sections to build. Describe the site again.`\n }\n if (screen.sections.length > AI_SITE_MAX_SECTIONS) {\n return `The page “${screen.title}” has ${screen.sections.length} sections, and a scaffolded page holds ${AI_SITE_MAX_SECTIONS}. Describe the site again.`\n }\n const slug = screen.slug.trim().toLowerCase()\n if (slugs.has(slug)) {\n return `Two pages in this plan share the address ${screen.slug}. Describe the site again.`\n }\n slugs.add(slug)\n }\n if (!screens.some((screen) => screen.nav)) {\n return 'No page in this plan is in the site navigation. Describe the site again.'\n }\n return null\n}\n\n/**\n * Why a scaffold cannot build this plan, in a sentence a member reads when\n * confirming it; `null` when it can.\n */\nexport function aiSitePlanRefusal(\n plan: AiBuildPlan,\n options: { freeTaste?: boolean } = {},\n): string | null {\n const shape = aiSitePlanShapeRefusal(plan, options)\n if (shape) return shape\n const prerequisites = aiSitePlanPrerequisites(plan)\n if (!prerequisites.length) return null\n const parts = prerequisites.map(\n ({ kind, name }) =>\n `the ${AI_BUILD_PLAN_CREATION_NOUNS[kind].noun} “${name}” ${AI_BUILD_PLAN_CREATION_NOUNS[kind].where}`,\n )\n const listed =\n parts.length === 1\n ? parts[0]\n : `${parts.slice(0, -1).join(', ')} and ${parts[parts.length - 1]}`\n return `This site needs what the workspace does not have yet. Create ${listed}, then describe the site again.`\n}\n\n/** What the estimate counts a plan in: whether a welcome email follows, and the creations the job builds. */\nexport interface AiPlanPassOptions {\n welcomeEmail?: boolean\n /** The creation kinds the job builds itself; a scaffold's when absent. */\n creates?: readonly AiBuildPlanCreateKind[]\n}\n\n/**\n * The passes a plan implies: one per section of every screen, one more per\n * screen for its search listing and its draft, and one for each thing the\n * plan creates that the job builds. What the estimate is counted in.\n */\nexport function aiPlanPasses(plan: AiBuildPlan, options: AiPlanPassOptions = {}): number {\n const screens = plan.screens.reduce(\n (total, screen) => total + screen.sections.length + 1,\n 0,\n )\n const creates = options.creates ?? AI_SITE_CREATE_KINDS\n const creations = plan.create.filter((entry) => creates.includes(entry.kind)).length\n return screens + creations + (options.welcomeEmail ? 1 : 0)\n}\n\n/**\n * About what a plan costs to build, in credits. An ESTIMATE: the nominal\n * credits the machine holds per step, times the passes the plan implies.\n * What each pass actually costs is its model's tokens, recorded on the job\n * as it runs.\n */\nexport function aiPlanCreditEstimate(plan: AiBuildPlan, options: AiPlanPassOptions = {}): number {\n return aiPlanPasses(plan, options) * AI_SITE_PASS_CREDITS\n}\n\n/**\n * About what a job of this kind costs to build from its plan (AGL-3031): a\n * page job counts the layout, forms and components it builds before its\n * page; a template, layout, component, form or email job counts its own\n * record and every creation it builds before it (AGL-3143 §15); and a\n * scaffold counts what it builds. The figure the proposal shows beside\n * Confirm, and the hold a confirmation takes.\n */\nexport function aiJobPlanCreditEstimate(kind: string, plan: AiBuildPlan): number {\n const creates =\n kind === 'page' ? AI_PAGE_CREATE_KINDS : AI_JOB_CREATE_KINDS[kind as keyof typeof AI_JOB_CREATE_KINDS]\n return aiPlanCreditEstimate(plan, creates ? { creates } : {})\n}\n\n/**\n * About what a scaffold of this many pages costs, before a plan names its\n * sections: the same arithmetic on the nominal section count, plus the plan\n * step that proposes it. Shown where a member asks for a scaffold.\n */\nexport function aiSiteCreditEstimate(\n pages: number,\n options: { welcomeEmail?: boolean } = {},\n): number {\n const screens = Math.max(0, Math.floor(pages))\n const passes =\n 1 +\n screens * (AI_SITE_NOMINAL_SECTIONS + 1) +\n 2 +\n (options.welcomeEmail ? 1 : 0)\n return passes * AI_SITE_PASS_CREDITS\n}\n"],"names":["AI_BUILD_PLAN_CREATION_NOUNS","aiPlanUndeclaredRefs","AI_JOB_CREATE_KINDS","AI_PAGE_CREATE_KINDS","AI_SITE_PAGES","min","max","AI_SITE_FREE_PAGES","aiSitePagesBand","freeTaste","AI_JOB_AUTO_CONFIRM_INPUT","AI_SITE_FREE_PAGES_NOTE","AI_SITE_PLAN_MAX_TOKENS","free","paid","AI_FREE_SITE_WORST_CASE_CREDITS","plan","layout","firstSection","laterSection","listing","aiFreeSiteSectionsWithin","creations","taste","credits","pages","Math","floor","before","layouts","AI_FREE_SITE_NOMINAL_SECTIONS","aiFreeSiteCreditEstimate","count","AI_SITE_MAX_SECTIONS","AI_SITE_NOMINAL_SECTIONS","AI_SITE_PASS_CREDITS","AI_SITE_INPUT_MAX_CHARS","AI_SITE_BATCH_MAX","AI_SITE_BATCH_MIN_HOST_LIMIT","AI_SITE_SUBMISSIONS","AI_SITE_SUBMISSION_CHOICES","id","label","blurb","AI_SITE_EMAIL_TYPE","aiSiteSubmissions","inputs","value","includes","ID_CHARS","parseAiSiteJobInputs","text","key","undefined","trimmed","trim","length","businessType","businessName","city","brand","audience","starter","rawPages","Number","NaN","isInteger","submissions","rawBatch","batchId","test","welcomeEmail","aiSitePagesRefusal","band","aiSiteWordsSaidAnything","words","Boolean","about","aiSiteWords","read","replace","slice","AI_SITE_CREATE_KINDS","aiSitePlanPrerequisites","seen","Set","prerequisites","add","kind","name","toLowerCase","has","push","entry","create","aiSitePlanShapeRefusal","options","screens","slugs","screen","sections","title","slug","some","nav","aiSitePlanRefusal","shape","parts","map","noun","where","listed","join","aiPlanPasses","reduce","total","creates","filter","aiPlanCreditEstimate","aiJobPlanCreditEstimate","aiSiteCreditEstimate","passes"],"mappings":"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"}
|
|
@@ -102,7 +102,9 @@ export declare function aiSiteStartExample(answers: AiSiteStartAnswers): AiSiteS
|
|
|
102
102
|
* when they can. The door's own parser is what actually admits the job — this
|
|
103
103
|
* is what the card says before it asks for one.
|
|
104
104
|
*/
|
|
105
|
-
export declare function aiSiteStartRefusal(answers: AiSiteStartAnswers
|
|
105
|
+
export declare function aiSiteStartRefusal(answers: AiSiteStartAnswers, options?: {
|
|
106
|
+
freeTaste?: boolean;
|
|
107
|
+
}): string | null;
|
|
106
108
|
/**
|
|
107
109
|
* The answers as the brief the `site` job is created with: the plan step reads
|
|
108
110
|
* it as the person's own words, so it says what they said and asks for nothing
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
|
-
*/ import { AI_SITE_INPUT_MAX_CHARS,
|
|
16
|
+
*/ import { AI_SITE_INPUT_MAX_CHARS, AI_SITE_SUBMISSIONS, aiSitePagesBand } from "./ai-site-job.js";
|
|
17
17
|
/**
|
|
18
18
|
* The examples the start offers, in the order they are shown. The platform's
|
|
19
19
|
* own starter sites: a person who has no words for what they want has seen
|
|
@@ -87,7 +87,9 @@
|
|
|
87
87
|
* Why these answers cannot start a site, in a sentence a person reads; `null`
|
|
88
88
|
* when they can. The door's own parser is what actually admits the job — this
|
|
89
89
|
* is what the card says before it asks for one.
|
|
90
|
-
*/ export function aiSiteStartRefusal(answers) {
|
|
90
|
+
*/ export function aiSiteStartRefusal(answers, options = {}) {
|
|
91
|
+
// The workspace's own band (AGL-3594): one or two pages on the Free taste.
|
|
92
|
+
const band = aiSitePagesBand(options.freeTaste);
|
|
91
93
|
const siteType = answers.siteType.trim();
|
|
92
94
|
if (!siteType) return 'Say what kind of site this is.';
|
|
93
95
|
if (siteType.length > AI_SITE_INPUT_MAX_CHARS) {
|
|
@@ -96,8 +98,8 @@
|
|
|
96
98
|
if (answers.audience.trim().length > AI_SITE_INPUT_MAX_CHARS) {
|
|
97
99
|
return `Keep who the site is for under ${AI_SITE_INPUT_MAX_CHARS} characters.`;
|
|
98
100
|
}
|
|
99
|
-
if (!Number.isInteger(answers.pages) || answers.pages <
|
|
100
|
-
return `A site is planned with ${
|
|
101
|
+
if (!Number.isInteger(answers.pages) || answers.pages < band.min || answers.pages > band.max) {
|
|
102
|
+
return `A site is planned with ${band.min} to ${band.max} pages.`;
|
|
101
103
|
}
|
|
102
104
|
if (answers.example !== null && !aiSiteStartExample(answers)) {
|
|
103
105
|
return 'Pick one of the examples, or none of them.';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-site-start.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n AI_SITE_INPUT_MAX_CHARS,\n AI_SITE_PAGES,\n AI_SITE_SUBMISSIONS,\n type AiSiteJobInputs,\n type AiSiteSubmissions,\n} from './ai-site-job'\n\n/**\n * The guided start (AGL-2918): the questions a person answers on a site they\n * have just made, and the site job those answers become.\n *\n * A scaffold already exists — the `site` job plans four to eight pages, a\n * layout, a contact form, a palette and a welcome email, and builds every one\n * of them as a draft. What it had no way in through was a brief: somebody had\n * to know what to write, on a site where nothing has happened yet. So this\n * module is the QUESTIONS, and nothing else: what is asked, what an answer may\n * be, and how a set of answers becomes the brief and the scalar inputs the\n * `site` door already takes.\n *\n * ── Answering is never the way out ───────────────────────────────────────\n *\n * There is no answer here that means \"not this\". The way past the questions\n * is the `hostFirstRun` zone's own `startBlank`, which is drawn beside them\n * from the first one and creates nothing at all — see the zone in\n * `CONSOLE_WIDGET_SLOTS`. Putting the exit inside the question set would make\n * leaving a thing you answer your way to, and it is not.\n *\n * ── Every answer but the first is optional ───────────────────────────────\n *\n * Only what a site is FOR has no default worth guessing. The audience narrows\n * the copy, the example steers the shape, the page count and the welcome\n * email are defaults a person may change — so a person who types six words\n * and confirms gets a planned site, and the rest of the questions are there\n * for the person who wants to answer them.\n *\n * Pure: shapes, vocabularies and two string builders. No React, no network,\n * and no import of the starter catalog — {@link AI_SITE_START_EXAMPLES} names\n * the starters rather than reading them, so the console does not carry a\n * catalog of node maps to draw five cards. `ai-site-start.spec.ts` holds the\n * two lists to each other.\n */\n\n/** A worked example a person picks to steer what the site should look like. */\nexport interface AiSiteStartExample {\n /** A `STARTER_TEMPLATES` id, which is a persisted identifier. */\n id: string\n /** The starter as its card names it. */\n label: string\n /** The starter's category, which is how the examples group. */\n category: string\n /** What the example is, in one line a person reads before picking it. */\n blurb: string\n}\n\n/**\n * The examples the start offers, in the order they are shown. The platform's\n * own starter sites: a person who has no words for what they want has seen\n * these five shapes before, and picking one says more than a sentence would.\n */\nexport const AI_SITE_START_EXAMPLES: readonly AiSiteStartExample[] = [\n {\n id: 'business',\n label: 'Business',\n category: 'Business',\n blurb: 'A home page led by what you do, an about page and a contact page',\n },\n {\n id: 'landing',\n label: 'Landing Page',\n category: 'Marketing',\n blurb: 'One page that leads to one action, with the reasons to take it',\n },\n {\n id: 'portfolio',\n label: 'Portfolio',\n category: 'Personal',\n blurb: 'An introduction, a grid of work and a way to get in touch',\n },\n {\n id: 'physical-shop',\n label: 'Shop (physical products)',\n category: 'Commerce',\n blurb: 'Featured products, a shop to browse, and a cart',\n },\n {\n id: 'digital-shop',\n label: 'Shop (digital products)',\n category: 'Commerce',\n blurb: 'Downloads up front, a shop to browse, and a cart',\n },\n]\n\n/**\n * Kinds of site offered beside the first question's box, so answering it can\n * be a tap. They fill the box rather than replacing it: what somebody is\n * building is theirs to say, and a list of eight is a list somebody is not on.\n */\nexport const AI_SITE_START_TYPES: readonly string[] = [\n 'a local service business',\n 'a restaurant or cafe',\n 'a professional practice',\n 'a trades business',\n 'a shop',\n 'a nonprofit',\n 'a personal portfolio',\n 'an event',\n]\n\n/** What a person answered; the shape the card holds and the brief is built from. */\nexport interface AiSiteStartAnswers {\n /** What kind of site this is. The one answer with no default. */\n siteType: string\n /** Who the site is for; empty when they did not say. */\n audience: string\n /** The example they liked, by {@link AiSiteStartExample.id}; `null` for none. */\n example: string | null\n /** How many pages to plan. */\n pages: number\n /** Where the contact form's submissions go. */\n submissions: AiSiteSubmissions\n /** Whether the scaffold also drafts a welcome email. */\n welcomeEmail: boolean\n}\n\n/**\n * What the questions start on. Five pages because it is the middle of the\n * band the scaffold builds, a welcome email because the scaffold drafts one\n * unless it is told not to, and the Inbox because it is what every submission\n * does anyway — filing a lead as well is the addition, and an addition is the\n * thing a person should have to choose.\n */\nexport const AI_SITE_START_ANSWERS: AiSiteStartAnswers = {\n siteType: '',\n audience: '',\n example: null,\n pages: 5,\n submissions: 'inbox',\n welcomeEmail: true,\n}\n\n/** The example an answer names, or `null` when it names none this list knows. */\nexport function aiSiteStartExample(\n answers: AiSiteStartAnswers,\n): AiSiteStartExample | null {\n if (!answers.example) return null\n return AI_SITE_START_EXAMPLES.find((entry) => entry.id === answers.example) ?? null\n}\n\n/**\n * Why these answers cannot start a site, in a sentence a person reads; `null`\n * when they can. The door's own parser is what actually admits the job — this\n * is what the card says before it asks for one.\n */\nexport function aiSiteStartRefusal(answers: AiSiteStartAnswers): string | null {\n const siteType = answers.siteType.trim()\n if (!siteType) return 'Say what kind of site this is.'\n if (siteType.length > AI_SITE_INPUT_MAX_CHARS) {\n return `Keep what kind of site this is under ${AI_SITE_INPUT_MAX_CHARS} characters.`\n }\n if (answers.audience.trim().length > AI_SITE_INPUT_MAX_CHARS) {\n return `Keep who the site is for under ${AI_SITE_INPUT_MAX_CHARS} characters.`\n }\n if (\n !Number.isInteger(answers.pages) ||\n answers.pages < AI_SITE_PAGES.min ||\n answers.pages > AI_SITE_PAGES.max\n ) {\n return `A site is planned with ${AI_SITE_PAGES.min} to ${AI_SITE_PAGES.max} pages.`\n }\n if (answers.example !== null && !aiSiteStartExample(answers)) {\n return 'Pick one of the examples, or none of them.'\n }\n if (!(AI_SITE_SUBMISSIONS as readonly string[]).includes(answers.submissions)) {\n return 'Say where the contact form’s submissions go.'\n }\n return null\n}\n\n/**\n * The answers as the brief the `site` job is created with: the plan step reads\n * it as the person's own words, so it says what they said and asks for nothing\n * they did not.\n */\nexport function aiSiteStartBrief(answers: AiSiteStartAnswers): string {\n const siteType = answers.siteType.trim()\n const audience = answers.audience.trim()\n const example = aiSiteStartExample(answers)\n const sentences = [\n `A ${answers.pages}-page website for ${siteType}.`,\n ]\n if (audience) sentences.push(`It is for ${audience}.`)\n if (example) {\n sentences.push(\n `Follow the shape of the ${example.label} starter: ${example.blurb.toLowerCase()}.`,\n )\n }\n sentences.push(\n 'Plan a page for each thing a visitor comes to do, and a contact form on the page that asks to be contacted.',\n )\n if (audience) {\n // Said of the FORM as well as of the copy: who fills a form in is what\n // decides which fields it asks for, and a brief that named the audience\n // once, in a sentence about the pages, left the form to guess.\n sentences.push(`Ask the contact form for what ${audience} would be asked for.`)\n }\n return sentences.join(' ')\n}\n\n/**\n * The answers as the `site` job's scalar inputs. Read by the door's\n * {@link parseAiSiteJobInputs}, and put in front of the model one per line by\n * the plan step, which is how the audience and the example reach the plan\n * without a prompt of their own.\n *\n * The per-site variables an agency batch fills — the business name, the city,\n * the brand — are left empty: this is one person's own site, and its name is\n * on the site already.\n */\nexport function aiSiteStartInputs(\n answers: AiSiteStartAnswers,\n): Omit<AiSiteJobInputs, 'batchId'> {\n const example = aiSiteStartExample(answers)\n return {\n businessType: answers.siteType.trim(),\n audience: answers.audience.trim(),\n starter: example?.id ?? '',\n pages: answers.pages,\n businessName: '',\n city: '',\n brand: '',\n submissions: answers.submissions,\n welcomeEmail: answers.welcomeEmail,\n }\n}\n"],"names":["AI_SITE_INPUT_MAX_CHARS","AI_SITE_PAGES","AI_SITE_SUBMISSIONS","AI_SITE_START_EXAMPLES","id","label","category","blurb","AI_SITE_START_TYPES","AI_SITE_START_ANSWERS","siteType","audience","example","pages","submissions","welcomeEmail","aiSiteStartExample","answers","find","entry","aiSiteStartRefusal","trim","length","Number","isInteger","min","max","includes","aiSiteStartBrief","sentences","push","toLowerCase","join","aiSiteStartInputs","businessType","starter","businessName","city","brand"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-site-start.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n AI_SITE_INPUT_MAX_CHARS,\n AI_SITE_SUBMISSIONS,\n aiSitePagesBand,\n type AiSiteJobInputs,\n type AiSiteSubmissions,\n} from './ai-site-job'\n\n/**\n * The guided start (AGL-2918): the questions a person answers on a site they\n * have just made, and the site job those answers become.\n *\n * A scaffold already exists — the `site` job plans four to eight pages, a\n * layout, a contact form, a palette and a welcome email, and builds every one\n * of them as a draft. What it had no way in through was a brief: somebody had\n * to know what to write, on a site where nothing has happened yet. So this\n * module is the QUESTIONS, and nothing else: what is asked, what an answer may\n * be, and how a set of answers becomes the brief and the scalar inputs the\n * `site` door already takes.\n *\n * ── Answering is never the way out ───────────────────────────────────────\n *\n * There is no answer here that means \"not this\". The way past the questions\n * is the `hostFirstRun` zone's own `startBlank`, which is drawn beside them\n * from the first one and creates nothing at all — see the zone in\n * `CONSOLE_WIDGET_SLOTS`. Putting the exit inside the question set would make\n * leaving a thing you answer your way to, and it is not.\n *\n * ── Every answer but the first is optional ───────────────────────────────\n *\n * Only what a site is FOR has no default worth guessing. The audience narrows\n * the copy, the example steers the shape, the page count and the welcome\n * email are defaults a person may change — so a person who types six words\n * and confirms gets a planned site, and the rest of the questions are there\n * for the person who wants to answer them.\n *\n * Pure: shapes, vocabularies and two string builders. No React, no network,\n * and no import of the starter catalog — {@link AI_SITE_START_EXAMPLES} names\n * the starters rather than reading them, so the console does not carry a\n * catalog of node maps to draw five cards. `ai-site-start.spec.ts` holds the\n * two lists to each other.\n */\n\n/** A worked example a person picks to steer what the site should look like. */\nexport interface AiSiteStartExample {\n /** A `STARTER_TEMPLATES` id, which is a persisted identifier. */\n id: string\n /** The starter as its card names it. */\n label: string\n /** The starter's category, which is how the examples group. */\n category: string\n /** What the example is, in one line a person reads before picking it. */\n blurb: string\n}\n\n/**\n * The examples the start offers, in the order they are shown. The platform's\n * own starter sites: a person who has no words for what they want has seen\n * these five shapes before, and picking one says more than a sentence would.\n */\nexport const AI_SITE_START_EXAMPLES: readonly AiSiteStartExample[] = [\n {\n id: 'business',\n label: 'Business',\n category: 'Business',\n blurb: 'A home page led by what you do, an about page and a contact page',\n },\n {\n id: 'landing',\n label: 'Landing Page',\n category: 'Marketing',\n blurb: 'One page that leads to one action, with the reasons to take it',\n },\n {\n id: 'portfolio',\n label: 'Portfolio',\n category: 'Personal',\n blurb: 'An introduction, a grid of work and a way to get in touch',\n },\n {\n id: 'physical-shop',\n label: 'Shop (physical products)',\n category: 'Commerce',\n blurb: 'Featured products, a shop to browse, and a cart',\n },\n {\n id: 'digital-shop',\n label: 'Shop (digital products)',\n category: 'Commerce',\n blurb: 'Downloads up front, a shop to browse, and a cart',\n },\n]\n\n/**\n * Kinds of site offered beside the first question's box, so answering it can\n * be a tap. They fill the box rather than replacing it: what somebody is\n * building is theirs to say, and a list of eight is a list somebody is not on.\n */\nexport const AI_SITE_START_TYPES: readonly string[] = [\n 'a local service business',\n 'a restaurant or cafe',\n 'a professional practice',\n 'a trades business',\n 'a shop',\n 'a nonprofit',\n 'a personal portfolio',\n 'an event',\n]\n\n/** What a person answered; the shape the card holds and the brief is built from. */\nexport interface AiSiteStartAnswers {\n /** What kind of site this is. The one answer with no default. */\n siteType: string\n /** Who the site is for; empty when they did not say. */\n audience: string\n /** The example they liked, by {@link AiSiteStartExample.id}; `null` for none. */\n example: string | null\n /** How many pages to plan. */\n pages: number\n /** Where the contact form's submissions go. */\n submissions: AiSiteSubmissions\n /** Whether the scaffold also drafts a welcome email. */\n welcomeEmail: boolean\n}\n\n/**\n * What the questions start on. Five pages because it is the middle of the\n * band the scaffold builds, a welcome email because the scaffold drafts one\n * unless it is told not to, and the Inbox because it is what every submission\n * does anyway — filing a lead as well is the addition, and an addition is the\n * thing a person should have to choose.\n */\nexport const AI_SITE_START_ANSWERS: AiSiteStartAnswers = {\n siteType: '',\n audience: '',\n example: null,\n pages: 5,\n submissions: 'inbox',\n welcomeEmail: true,\n}\n\n/** The example an answer names, or `null` when it names none this list knows. */\nexport function aiSiteStartExample(\n answers: AiSiteStartAnswers,\n): AiSiteStartExample | null {\n if (!answers.example) return null\n return AI_SITE_START_EXAMPLES.find((entry) => entry.id === answers.example) ?? null\n}\n\n/**\n * Why these answers cannot start a site, in a sentence a person reads; `null`\n * when they can. The door's own parser is what actually admits the job — this\n * is what the card says before it asks for one.\n */\nexport function aiSiteStartRefusal(\n answers: AiSiteStartAnswers,\n options: { freeTaste?: boolean } = {},\n): string | null {\n // The workspace's own band (AGL-3594): one or two pages on the Free taste.\n const band = aiSitePagesBand(options.freeTaste)\n const siteType = answers.siteType.trim()\n if (!siteType) return 'Say what kind of site this is.'\n if (siteType.length > AI_SITE_INPUT_MAX_CHARS) {\n return `Keep what kind of site this is under ${AI_SITE_INPUT_MAX_CHARS} characters.`\n }\n if (answers.audience.trim().length > AI_SITE_INPUT_MAX_CHARS) {\n return `Keep who the site is for under ${AI_SITE_INPUT_MAX_CHARS} characters.`\n }\n if (!Number.isInteger(answers.pages) || answers.pages < band.min || answers.pages > band.max) {\n return `A site is planned with ${band.min} to ${band.max} pages.`\n }\n if (answers.example !== null && !aiSiteStartExample(answers)) {\n return 'Pick one of the examples, or none of them.'\n }\n if (!(AI_SITE_SUBMISSIONS as readonly string[]).includes(answers.submissions)) {\n return 'Say where the contact form’s submissions go.'\n }\n return null\n}\n\n/**\n * The answers as the brief the `site` job is created with: the plan step reads\n * it as the person's own words, so it says what they said and asks for nothing\n * they did not.\n */\nexport function aiSiteStartBrief(answers: AiSiteStartAnswers): string {\n const siteType = answers.siteType.trim()\n const audience = answers.audience.trim()\n const example = aiSiteStartExample(answers)\n const sentences = [\n `A ${answers.pages}-page website for ${siteType}.`,\n ]\n if (audience) sentences.push(`It is for ${audience}.`)\n if (example) {\n sentences.push(\n `Follow the shape of the ${example.label} starter: ${example.blurb.toLowerCase()}.`,\n )\n }\n sentences.push(\n 'Plan a page for each thing a visitor comes to do, and a contact form on the page that asks to be contacted.',\n )\n if (audience) {\n // Said of the FORM as well as of the copy: who fills a form in is what\n // decides which fields it asks for, and a brief that named the audience\n // once, in a sentence about the pages, left the form to guess.\n sentences.push(`Ask the contact form for what ${audience} would be asked for.`)\n }\n return sentences.join(' ')\n}\n\n/**\n * The answers as the `site` job's scalar inputs. Read by the door's\n * {@link parseAiSiteJobInputs}, and put in front of the model one per line by\n * the plan step, which is how the audience and the example reach the plan\n * without a prompt of their own.\n *\n * The per-site variables an agency batch fills — the business name, the city,\n * the brand — are left empty: this is one person's own site, and its name is\n * on the site already.\n */\nexport function aiSiteStartInputs(\n answers: AiSiteStartAnswers,\n): Omit<AiSiteJobInputs, 'batchId'> {\n const example = aiSiteStartExample(answers)\n return {\n businessType: answers.siteType.trim(),\n audience: answers.audience.trim(),\n starter: example?.id ?? '',\n pages: answers.pages,\n businessName: '',\n city: '',\n brand: '',\n submissions: answers.submissions,\n welcomeEmail: answers.welcomeEmail,\n }\n}\n"],"names":["AI_SITE_INPUT_MAX_CHARS","AI_SITE_SUBMISSIONS","aiSitePagesBand","AI_SITE_START_EXAMPLES","id","label","category","blurb","AI_SITE_START_TYPES","AI_SITE_START_ANSWERS","siteType","audience","example","pages","submissions","welcomeEmail","aiSiteStartExample","answers","find","entry","aiSiteStartRefusal","options","band","freeTaste","trim","length","Number","isInteger","min","max","includes","aiSiteStartBrief","sentences","push","toLowerCase","join","aiSiteStartInputs","businessType","starter","businessName","city","brand"],"mappings":"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"}
|
|
@@ -75,9 +75,10 @@ export declare function aiTemplateAddressTokens(definition: AiTemplateSubjectDef
|
|
|
75
75
|
/**
|
|
76
76
|
* Whether a token is filled for a machine and never read on the page: a URL
|
|
77
77
|
* segment (`{{entry.slug}}`, `{{collection.slug}}`, `{{entry.collectionSlug}}`)
|
|
78
|
-
*
|
|
79
|
-
* (`{{entry.publishedAt}}`)
|
|
80
|
-
*
|
|
78
|
+
* the ISO timestamp a Video element's publication date takes
|
|
79
|
+
* (`{{entry.publishedAt}}`), or the bare seconds its duration takes
|
|
80
|
+
* (`{{entry.coverVideoDuration}}`, AGL-3584). A block may be handed one whole;
|
|
81
|
+
* a reader shown one sees hyphens and digits (AGL-3143 §16).
|
|
81
82
|
*/
|
|
82
83
|
export declare function isAiUnreadToken(token: string): boolean;
|
|
83
84
|
/** The tokens of a subject its page shows: every one a reader reads or follows, which is all but the unread. */
|
|
@@ -114,13 +114,14 @@ export const AI_TEMPLATE_SUBJECT_DEFINITIONS = {
|
|
|
114
114
|
/**
|
|
115
115
|
* Whether a token is filled for a machine and never read on the page: a URL
|
|
116
116
|
* segment (`{{entry.slug}}`, `{{collection.slug}}`, `{{entry.collectionSlug}}`)
|
|
117
|
-
*
|
|
118
|
-
* (`{{entry.publishedAt}}`)
|
|
119
|
-
*
|
|
117
|
+
* the ISO timestamp a Video element's publication date takes
|
|
118
|
+
* (`{{entry.publishedAt}}`), or the bare seconds its duration takes
|
|
119
|
+
* (`{{entry.coverVideoDuration}}`, AGL-3584). A block may be handed one whole;
|
|
120
|
+
* a reader shown one sees hyphens and digits (AGL-3143 §16).
|
|
120
121
|
*/ export function isAiUnreadToken(token) {
|
|
121
122
|
var _token_replace_split_pop;
|
|
122
123
|
const field = (_token_replace_split_pop = token.replace(/[{}\s]/g, '').split('.').pop()) != null ? _token_replace_split_pop : '';
|
|
123
|
-
return /^(?:slug|publishedAt)$|Slug$/.test(field);
|
|
124
|
+
return /^(?:slug|publishedAt)$|Slug$|Duration$/.test(field);
|
|
124
125
|
}
|
|
125
126
|
/** The tokens of a subject its page shows: every one a reader reads or follows, which is all but the unread. */ export function aiTemplateShownTokens(definition) {
|
|
126
127
|
return definition.tokens.map((entry)=>entry.token).filter((token)=>!isAiUnreadToken(token));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-template-subjects.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n AUTHOR_TOKEN_CATALOG,\n COLLECTION_TOKEN_CATALOG,\n ENTRY_TOKEN_CATALOG,\n type BindingTokenCatalogEntry,\n} from '@aglyn/aglyn/app-utils/binding-token-catalog'\n\n/**\n * What a generated page template renders for (AGL-2909): one page drawn once\n * per record the site already keeps.\n *\n * - `entry` — a content collection's entry page: a blog post, an event, a\n * case study. Picked as the collection's entry template screen.\n * - `product` — the store's product page. Picked as the product template in\n * Store settings.\n * - `author` — an author's page. Picked as the author page screen.\n *\n * Everything that differs per record is a binding token, and the tokens a\n * template may bind are the ones the platform fills on that page. For an\n * entry and an author those are the besigner's own insert-picker catalogs, so\n * what a model may bind and what an author can pick are one list; a product's\n * are the commerce product page's, which no catalog names yet (below).\n *\n * Model only: shapes, the vocabularies and the inputs reader, no runtime.\n */\n\nexport type AiTemplateSubject = 'entry' | 'product' | 'author'\n\nexport const AI_TEMPLATE_SUBJECTS: readonly AiTemplateSubject[] = ['entry', 'product', 'author']\n\n/**\n * The tokens the commerce product page fills. The commerce plugin's page\n * resolver is their definition of record; a spec reads its source and holds\n * this list to it in both directions, because a plugin never imports another\n * plugin's internals.\n */\nexport const AI_PRODUCT_TEMPLATE_TOKENS: readonly BindingTokenCatalogEntry[] = [\n { token: '{{product.name}}', label: 'Name', description: 'The product’s name.' },\n {\n token: '{{product.description}}',\n label: 'Description',\n description: 'The product’s description.',\n },\n {\n token: '{{product.price}}',\n label: 'Price',\n description: 'The price, or “From” the lowest price when variants differ.',\n },\n { token: '{{product.image}}', label: 'Image', description: 'The product’s first image.' },\n { token: '{{product.slug}}', label: 'Slug', description: 'The product’s URL segment.' },\n]\n\n/** The pager tokens, which resolve on an author's page over their own archive. */\nconst PAGINATION_TOKENS = COLLECTION_TOKEN_CATALOG.filter((entry) =>\n entry.token.startsWith('{{pagination.'),\n)\n\nexport interface AiTemplateSubjectDefinition {\n subject: AiTemplateSubject\n /** One rendered page's record, as a sentence names it. */\n noun: string\n /** The token the page's one h1 binds, so every record shows its own title. */\n titleToken: string\n /** Every token the page may bind. */\n tokens: readonly BindingTokenCatalogEntry[]\n /**\n * Palette blocks that fill themselves from this subject on its page, and\n * render nothing on another subject's. Inside a Collection Entries card each\n * fills from that card's entry instead, so a card is exempt.\n */\n blocks: readonly string[]\n /** The template's name when the plan names none. */\n defaultName: string\n /** Where a member puts the page to work, for the proposal and the docs. */\n appliedIn: string\n}\n\nexport const AI_TEMPLATE_SUBJECT_DEFINITIONS: Readonly<\n Record<AiTemplateSubject, AiTemplateSubjectDefinition>\n> = {\n entry: {\n subject: 'entry',\n noun: 'a collection entry',\n titleToken: '{{entry.title}}',\n tokens: [...ENTRY_TOKEN_CATALOG, ...COLLECTION_TOKEN_CATALOG],\n blocks: ['collectionEntryMeta', 'collectionEntryBody', 'collectionEntryAuthor', 'collectionRelated'],\n defaultName: 'Entry page template',\n appliedIn: 'the collection’s entry template page, under Template pages in Content',\n },\n product: {\n subject: 'product',\n noun: 'a product',\n titleToken: '{{product.name}}',\n tokens: AI_PRODUCT_TEMPLATE_TOKENS,\n blocks: [],\n defaultName: 'Product page template',\n appliedIn: 'the product page template in Store settings',\n },\n author: {\n subject: 'author',\n noun: 'an author',\n titleToken: '{{author.name}}',\n tokens: [...AUTHOR_TOKEN_CATALOG, ...PAGINATION_TOKENS],\n blocks: ['contentAuthorProfile'],\n defaultName: 'Author page template',\n appliedIn: 'the author page on the Authors tab',\n },\n}\n\n/** Every `{{…}}` token a string holds, spelled without inner whitespace. */\nexport function aiBindingTokensIn(value: string): string[] {\n return [...value.matchAll(/\\{\\{\\s*([^{}\\s]+)\\s*\\}\\}/g)].map((match) => `{{${match[1]}}}`)\n}\n\n/**\n * Whether a token may fill a link or media prop whole: its field is an\n * address or a picture (`{{entry.url}}`, `{{entry.coverImage}}`,\n * `{{author.image}}`), never copy.\n */\nexport function isAiAddressToken(token: string): boolean {\n const field = token.replace(/^\\{\\{|\\}\\}$/g, '').split('.').pop() ?? ''\n return /^(?:url|image)$|(?:Url|Image|Video)$/.test(field)\n}\n\n/** The tokens of a subject a link or media prop may hold. */\nexport function aiTemplateAddressTokens(definition: AiTemplateSubjectDefinition): string[] {\n return definition.tokens.map((entry) => entry.token).filter(isAiAddressToken)\n}\n\n/**\n * Whether a token is filled for a machine and never read on the page: a URL\n * segment (`{{entry.slug}}`, `{{collection.slug}}`, `{{entry.collectionSlug}}`)\n * or the ISO timestamp a Video element's publication date takes\n * (`{{entry.publishedAt}}`). A block may be handed one whole; a reader shown\n * one sees hyphens and digits (AGL-3143 §16).\n */\nexport function isAiUnreadToken(token: string): boolean {\n const field = token.replace(/[{}\\s]/g, '').split('.').pop() ?? ''\n return /^(?:slug|publishedAt)$|Slug$/.test(field)\n}\n\n/** The tokens of a subject its page shows: every one a reader reads or follows, which is all but the unread. */\nexport function aiTemplateShownTokens(definition: AiTemplateSubjectDefinition): string[] {\n return definition.tokens.map((entry) => entry.token).filter((token) => !isAiUnreadToken(token))\n}\n\n/** What a template job reads from its inputs. */\nexport interface AiTemplateJobInputs {\n subject: AiTemplateSubject\n /** The content collection an entry template is for; `null` for a product or author page. */\n collectionId: string | null\n}\n\nconst RECORD_ID = /^[A-Za-z0-9_-]{1,100}$/\n\n/** The inputs of a template job, or a sentence naming what is missing. */\nexport function parseAiTemplateJobInputs(\n inputs: Readonly<Record<string, unknown>> | null | undefined,\n): AiTemplateJobInputs | string {\n const subject = String(inputs?.['subject'] ?? '')\n if (!(AI_TEMPLATE_SUBJECTS as readonly string[]).includes(subject)) {\n return `Name what the template is for: inputs.subject is one of ${AI_TEMPLATE_SUBJECTS.join(', ')}`\n }\n if (subject !== 'entry') return { subject: subject as AiTemplateSubject, collectionId: null }\n const raw = inputs?.['collectionId']\n const collectionId = typeof raw === 'string' ? raw.trim() : ''\n if (!RECORD_ID.test(collectionId)) {\n return 'An entry page template names its content collection in inputs.collectionId'\n }\n return { subject: 'entry', collectionId }\n}\n"],"names":["AUTHOR_TOKEN_CATALOG","COLLECTION_TOKEN_CATALOG","ENTRY_TOKEN_CATALOG","AI_TEMPLATE_SUBJECTS","AI_PRODUCT_TEMPLATE_TOKENS","token","label","description","PAGINATION_TOKENS","filter","entry","startsWith","AI_TEMPLATE_SUBJECT_DEFINITIONS","subject","noun","titleToken","tokens","blocks","defaultName","appliedIn","product","author","aiBindingTokensIn","value","matchAll","map","match","isAiAddressToken","field","replace","split","pop","test","aiTemplateAddressTokens","definition","isAiUnreadToken","aiTemplateShownTokens","RECORD_ID","parseAiTemplateJobInputs","inputs","String","includes","join","collectionId","raw","trim"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-template-subjects.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n AUTHOR_TOKEN_CATALOG,\n COLLECTION_TOKEN_CATALOG,\n ENTRY_TOKEN_CATALOG,\n type BindingTokenCatalogEntry,\n} from '@aglyn/aglyn/app-utils/binding-token-catalog'\n\n/**\n * What a generated page template renders for (AGL-2909): one page drawn once\n * per record the site already keeps.\n *\n * - `entry` — a content collection's entry page: a blog post, an event, a\n * case study. Picked as the collection's entry template screen.\n * - `product` — the store's product page. Picked as the product template in\n * Store settings.\n * - `author` — an author's page. Picked as the author page screen.\n *\n * Everything that differs per record is a binding token, and the tokens a\n * template may bind are the ones the platform fills on that page. For an\n * entry and an author those are the besigner's own insert-picker catalogs, so\n * what a model may bind and what an author can pick are one list; a product's\n * are the commerce product page's, which no catalog names yet (below).\n *\n * Model only: shapes, the vocabularies and the inputs reader, no runtime.\n */\n\nexport type AiTemplateSubject = 'entry' | 'product' | 'author'\n\nexport const AI_TEMPLATE_SUBJECTS: readonly AiTemplateSubject[] = ['entry', 'product', 'author']\n\n/**\n * The tokens the commerce product page fills. The commerce plugin's page\n * resolver is their definition of record; a spec reads its source and holds\n * this list to it in both directions, because a plugin never imports another\n * plugin's internals.\n */\nexport const AI_PRODUCT_TEMPLATE_TOKENS: readonly BindingTokenCatalogEntry[] = [\n { token: '{{product.name}}', label: 'Name', description: 'The product’s name.' },\n {\n token: '{{product.description}}',\n label: 'Description',\n description: 'The product’s description.',\n },\n {\n token: '{{product.price}}',\n label: 'Price',\n description: 'The price, or “From” the lowest price when variants differ.',\n },\n { token: '{{product.image}}', label: 'Image', description: 'The product’s first image.' },\n { token: '{{product.slug}}', label: 'Slug', description: 'The product’s URL segment.' },\n]\n\n/** The pager tokens, which resolve on an author's page over their own archive. */\nconst PAGINATION_TOKENS = COLLECTION_TOKEN_CATALOG.filter((entry) =>\n entry.token.startsWith('{{pagination.'),\n)\n\nexport interface AiTemplateSubjectDefinition {\n subject: AiTemplateSubject\n /** One rendered page's record, as a sentence names it. */\n noun: string\n /** The token the page's one h1 binds, so every record shows its own title. */\n titleToken: string\n /** Every token the page may bind. */\n tokens: readonly BindingTokenCatalogEntry[]\n /**\n * Palette blocks that fill themselves from this subject on its page, and\n * render nothing on another subject's. Inside a Collection Entries card each\n * fills from that card's entry instead, so a card is exempt.\n */\n blocks: readonly string[]\n /** The template's name when the plan names none. */\n defaultName: string\n /** Where a member puts the page to work, for the proposal and the docs. */\n appliedIn: string\n}\n\nexport const AI_TEMPLATE_SUBJECT_DEFINITIONS: Readonly<\n Record<AiTemplateSubject, AiTemplateSubjectDefinition>\n> = {\n entry: {\n subject: 'entry',\n noun: 'a collection entry',\n titleToken: '{{entry.title}}',\n tokens: [...ENTRY_TOKEN_CATALOG, ...COLLECTION_TOKEN_CATALOG],\n blocks: ['collectionEntryMeta', 'collectionEntryBody', 'collectionEntryAuthor', 'collectionRelated'],\n defaultName: 'Entry page template',\n appliedIn: 'the collection’s entry template page, under Template pages in Content',\n },\n product: {\n subject: 'product',\n noun: 'a product',\n titleToken: '{{product.name}}',\n tokens: AI_PRODUCT_TEMPLATE_TOKENS,\n blocks: [],\n defaultName: 'Product page template',\n appliedIn: 'the product page template in Store settings',\n },\n author: {\n subject: 'author',\n noun: 'an author',\n titleToken: '{{author.name}}',\n tokens: [...AUTHOR_TOKEN_CATALOG, ...PAGINATION_TOKENS],\n blocks: ['contentAuthorProfile'],\n defaultName: 'Author page template',\n appliedIn: 'the author page on the Authors tab',\n },\n}\n\n/** Every `{{…}}` token a string holds, spelled without inner whitespace. */\nexport function aiBindingTokensIn(value: string): string[] {\n return [...value.matchAll(/\\{\\{\\s*([^{}\\s]+)\\s*\\}\\}/g)].map((match) => `{{${match[1]}}}`)\n}\n\n/**\n * Whether a token may fill a link or media prop whole: its field is an\n * address or a picture (`{{entry.url}}`, `{{entry.coverImage}}`,\n * `{{author.image}}`), never copy.\n */\nexport function isAiAddressToken(token: string): boolean {\n const field = token.replace(/^\\{\\{|\\}\\}$/g, '').split('.').pop() ?? ''\n return /^(?:url|image)$|(?:Url|Image|Video)$/.test(field)\n}\n\n/** The tokens of a subject a link or media prop may hold. */\nexport function aiTemplateAddressTokens(definition: AiTemplateSubjectDefinition): string[] {\n return definition.tokens.map((entry) => entry.token).filter(isAiAddressToken)\n}\n\n/**\n * Whether a token is filled for a machine and never read on the page: a URL\n * segment (`{{entry.slug}}`, `{{collection.slug}}`, `{{entry.collectionSlug}}`)\n * the ISO timestamp a Video element's publication date takes\n * (`{{entry.publishedAt}}`), or the bare seconds its duration takes\n * (`{{entry.coverVideoDuration}}`, AGL-3584). A block may be handed one whole;\n * a reader shown one sees hyphens and digits (AGL-3143 §16).\n */\nexport function isAiUnreadToken(token: string): boolean {\n const field = token.replace(/[{}\\s]/g, '').split('.').pop() ?? ''\n return /^(?:slug|publishedAt)$|Slug$|Duration$/.test(field)\n}\n\n/** The tokens of a subject its page shows: every one a reader reads or follows, which is all but the unread. */\nexport function aiTemplateShownTokens(definition: AiTemplateSubjectDefinition): string[] {\n return definition.tokens.map((entry) => entry.token).filter((token) => !isAiUnreadToken(token))\n}\n\n/** What a template job reads from its inputs. */\nexport interface AiTemplateJobInputs {\n subject: AiTemplateSubject\n /** The content collection an entry template is for; `null` for a product or author page. */\n collectionId: string | null\n}\n\nconst RECORD_ID = /^[A-Za-z0-9_-]{1,100}$/\n\n/** The inputs of a template job, or a sentence naming what is missing. */\nexport function parseAiTemplateJobInputs(\n inputs: Readonly<Record<string, unknown>> | null | undefined,\n): AiTemplateJobInputs | string {\n const subject = String(inputs?.['subject'] ?? '')\n if (!(AI_TEMPLATE_SUBJECTS as readonly string[]).includes(subject)) {\n return `Name what the template is for: inputs.subject is one of ${AI_TEMPLATE_SUBJECTS.join(', ')}`\n }\n if (subject !== 'entry') return { subject: subject as AiTemplateSubject, collectionId: null }\n const raw = inputs?.['collectionId']\n const collectionId = typeof raw === 'string' ? raw.trim() : ''\n if (!RECORD_ID.test(collectionId)) {\n return 'An entry page template names its content collection in inputs.collectionId'\n }\n return { subject: 'entry', collectionId }\n}\n"],"names":["AUTHOR_TOKEN_CATALOG","COLLECTION_TOKEN_CATALOG","ENTRY_TOKEN_CATALOG","AI_TEMPLATE_SUBJECTS","AI_PRODUCT_TEMPLATE_TOKENS","token","label","description","PAGINATION_TOKENS","filter","entry","startsWith","AI_TEMPLATE_SUBJECT_DEFINITIONS","subject","noun","titleToken","tokens","blocks","defaultName","appliedIn","product","author","aiBindingTokensIn","value","matchAll","map","match","isAiAddressToken","field","replace","split","pop","test","aiTemplateAddressTokens","definition","isAiUnreadToken","aiTemplateShownTokens","RECORD_ID","parseAiTemplateJobInputs","inputs","String","includes","join","collectionId","raw","trim"],"mappings":"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"}
|