@aglyn/plugins-ai 1.0.0-beta.292 → 1.0.0-beta.296
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +17 -17
- package/src/lib/components/ai-job-links.d.ts +15 -2
- package/src/lib/components/ai-job-links.js +24 -3
- package/src/lib/components/ai-job-links.js.map +1 -1
- package/src/lib/components/ai-job-requests.d.ts +9 -0
- package/src/lib/components/ai-job-requests.js +34 -0
- package/src/lib/components/ai-job-requests.js.map +1 -1
- package/src/lib/components/ai-site-build-page.component.js +28 -2
- package/src/lib/components/ai-site-build-page.component.js.map +1 -1
- package/src/lib/components/ai-site-import-job.component.js +5 -4
- package/src/lib/components/ai-site-import-job.component.js.map +1 -1
- package/src/lib/components/ai-site-import-start.component.js +3 -3
- package/src/lib/components/ai-site-import-start.component.js.map +1 -1
- package/src/lib/components/ai-site-start-card.component.d.ts +1 -1
- package/src/lib/components/ai-site-start-card.component.js +129 -29
- package/src/lib/components/ai-site-start-card.component.js.map +1 -1
- package/src/lib/components/ai-site-start-steps.component.d.ts +15 -3
- package/src/lib/components/ai-site-start-steps.component.js +42 -25
- package/src/lib/components/ai-site-start-steps.component.js.map +1 -1
- package/src/lib/components/ai-site-style-previews.d.ts +17 -0
- package/src/lib/components/ai-site-style-previews.js +26 -95
- package/src/lib/components/ai-site-style-previews.js.map +1 -1
- package/src/lib/components/assist-jobs-drawer.component.d.ts +7 -1
- package/src/lib/components/assist-jobs-drawer.component.js +20 -6
- package/src/lib/components/assist-jobs-drawer.component.js.map +1 -1
- package/src/lib/components/assist-page-change-card.component.d.ts +44 -0
- package/src/lib/components/assist-page-change-card.component.js +166 -0
- package/src/lib/components/assist-page-change-card.component.js.map +1 -0
- package/src/lib/components/assist-panel.component.d.ts +105 -1
- package/src/lib/components/assist-panel.component.js +224 -29
- package/src/lib/components/assist-panel.component.js.map +1 -1
- package/src/lib/declarations.server.js +22 -0
- package/src/lib/declarations.server.js.map +1 -1
- package/src/lib/jobs/ai-job-edit-step.d.ts +62 -16
- package/src/lib/jobs/ai-job-edit-step.js +172 -61
- package/src/lib/jobs/ai-job-edit-step.js.map +1 -1
- package/src/lib/jobs/ai-job-import-step.d.ts +21 -2
- package/src/lib/jobs/ai-job-import-step.js +179 -49
- package/src/lib/jobs/ai-job-import-step.js.map +1 -1
- package/src/lib/jobs/ai-job-layout-language.d.ts +27 -3
- package/src/lib/jobs/ai-job-layout-language.js +156 -113
- package/src/lib/jobs/ai-job-layout-language.js.map +1 -1
- package/src/lib/jobs/ai-job-layout-step.js +56 -17
- package/src/lib/jobs/ai-job-layout-step.js.map +1 -1
- package/src/lib/jobs/ai-job-navigation-step.js +4 -2
- package/src/lib/jobs/ai-job-navigation-step.js.map +1 -1
- package/src/lib/jobs/ai-job-page-language.d.ts +25 -15
- package/src/lib/jobs/ai-job-page-language.js +120 -66
- package/src/lib/jobs/ai-job-page-language.js.map +1 -1
- package/src/lib/jobs/ai-job-page-step.js +27 -7
- package/src/lib/jobs/ai-job-page-step.js.map +1 -1
- package/src/lib/jobs/ai-job-site-datasets.d.ts +12 -4
- package/src/lib/jobs/ai-job-site-datasets.js +36 -16
- package/src/lib/jobs/ai-job-site-datasets.js.map +1 -1
- package/src/lib/jobs/ai-jobs.js +6 -2
- package/src/lib/jobs/ai-jobs.js.map +1 -1
- package/src/lib/jobs/ai-layout-stock-photos.d.ts +50 -1
- package/src/lib/jobs/ai-layout-stock-photos.js +586 -35
- package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
- package/src/lib/jobs/ai-site-import-compile.d.ts +147 -0
- package/src/lib/jobs/ai-site-import-compile.js +1143 -0
- package/src/lib/jobs/ai-site-import-compile.js.map +1 -0
- package/src/lib/jobs/ai-site-import-document.d.ts +9 -36
- package/src/lib/jobs/ai-site-import-document.js +23 -489
- package/src/lib/jobs/ai-site-import-document.js.map +1 -1
- package/src/lib/jobs/ai-site-import-look.d.ts +54 -7
- package/src/lib/jobs/ai-site-import-look.js +163 -52
- package/src/lib/jobs/ai-site-import-look.js.map +1 -1
- package/src/lib/jobs/ai-site-import-publish.d.ts +7 -1
- package/src/lib/jobs/ai-site-import-publish.js +19 -5
- package/src/lib/jobs/ai-site-import-publish.js.map +1 -1
- package/src/lib/jobs/ai-site-late-photos.js +4 -2
- package/src/lib/jobs/ai-site-late-photos.js.map +1 -1
- package/src/lib/jobs/ai-site-publish.d.ts +10 -1
- package/src/lib/jobs/ai-site-publish.js +8 -3
- package/src/lib/jobs/ai-site-publish.js.map +1 -1
- package/src/lib/jobs/fixtures/ai-stock-photo-agl4092-1029-recordings.d.ts +23 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-agl4092-1029-recordings.js +677 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-agl4092-1029-recordings.js.map +1 -0
- package/src/lib/layout-language/ai-layout-compiler.d.ts +26 -0
- package/src/lib/layout-language/ai-layout-compiler.js +677 -33
- package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
- package/src/lib/layout-language/ai-layout-frame-hollows.js +5 -1
- package/src/lib/layout-language/ai-layout-frame-hollows.js.map +1 -1
- package/src/lib/layout-language/ai-layout-frame-source.d.ts +196 -0
- package/src/lib/layout-language/ai-layout-frame-source.js +726 -0
- package/src/lib/layout-language/ai-layout-frame-source.js.map +1 -0
- package/src/lib/layout-language/ai-layout-frame.d.ts +22 -0
- package/src/lib/layout-language/ai-layout-frame.js +121 -29
- package/src/lib/layout-language/ai-layout-frame.js.map +1 -1
- package/src/lib/layout-language/ai-layout-language.d.ts +103 -37
- package/src/lib/layout-language/ai-layout-language.js +85 -120
- package/src/lib/layout-language/ai-layout-language.js.map +1 -1
- package/src/lib/model/ai-credit-estimate.d.ts +11 -7
- package/src/lib/model/ai-credit-estimate.js +11 -7
- package/src/lib/model/ai-credit-estimate.js.map +1 -1
- package/src/lib/model/ai-edit-job.d.ts +33 -16
- package/src/lib/model/ai-edit-job.js +50 -34
- package/src/lib/model/ai-edit-job.js.map +1 -1
- package/src/lib/model/ai-job-activity.d.ts +6 -11
- package/src/lib/model/ai-job-activity.js +8 -15
- package/src/lib/model/ai-job-activity.js.map +1 -1
- package/src/lib/model/ai-jobs.types.d.ts +6 -0
- package/src/lib/model/ai-jobs.types.js.map +1 -1
- package/src/lib/model/ai-site-import.d.ts +307 -13
- package/src/lib/model/ai-site-import.js +227 -19
- package/src/lib/model/ai-site-import.js.map +1 -1
- package/src/lib/model/ai-site-inventory.d.ts +6 -0
- package/src/lib/model/ai-site-inventory.js.map +1 -1
- package/src/lib/model/ai-site-look.d.ts +65 -1
- package/src/lib/model/ai-site-look.js +141 -23
- package/src/lib/model/ai-site-look.js.map +1 -1
- package/src/lib/model/ai-site-photos.d.ts +2 -0
- package/src/lib/model/ai-site-photos.js.map +1 -1
- package/src/lib/model/ai-site-start-steps.d.ts +4 -3
- package/src/lib/model/ai-site-start-steps.js +11 -3
- package/src/lib/model/ai-site-start-steps.js.map +1 -1
- package/src/lib/model/ai-site-start.d.ts +23 -2
- package/src/lib/model/ai-site-start.js +25 -3
- package/src/lib/model/ai-site-start.js.map +1 -1
- package/src/lib/model/assist-edit.d.ts +18 -1
- package/src/lib/model/assist-edit.js +7 -0
- package/src/lib/model/assist-edit.js.map +1 -1
- package/src/lib/model/assist-page-change.d.ts +86 -0
- package/src/lib/model/assist-page-change.js +86 -0
- package/src/lib/model/assist-page-change.js.map +1 -0
- package/src/lib/providers/routing.d.ts +11 -0
- package/src/lib/providers/routing.js +23 -0
- package/src/lib/providers/routing.js.map +1 -1
- package/src/lib/runtime/ai-doctrine-validators.d.ts +4 -2
- package/src/lib/runtime/ai-doctrine-validators.js +7 -2
- package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
- package/src/lib/runtime/ai-eval.js +3 -1
- package/src/lib/runtime/ai-eval.js.map +1 -1
- package/src/lib/runtime/ai-icon-library.d.ts +4 -0
- package/src/lib/runtime/ai-icon-library.js +4 -0
- package/src/lib/runtime/ai-icon-library.js.map +1 -1
- package/src/lib/runtime/ai-node-tree.d.ts +5 -0
- package/src/lib/runtime/ai-node-tree.js +23 -4
- package/src/lib/runtime/ai-node-tree.js.map +1 -1
- package/src/lib/runtime/ai-palette.d.ts +2 -0
- package/src/lib/runtime/ai-palette.generated.js +19 -3
- package/src/lib/runtime/ai-palette.generated.js.map +1 -1
- package/src/lib/runtime/ai-palette.js +2 -1
- package/src/lib/runtime/ai-palette.js.map +1 -1
- package/src/lib/runtime/site-inventory.d.ts +8 -1
- package/src/lib/runtime/site-inventory.js +18 -0
- package/src/lib/runtime/site-inventory.js.map +1 -1
- package/src/lib/server/assist-build.js +2 -0
- package/src/lib/server/assist-build.js.map +1 -1
- package/src/lib/server/assist-chat.js +51 -2
- package/src/lib/server/assist-chat.js.map +1 -1
- package/src/lib/server/assist-edit.d.ts +54 -2
- package/src/lib/server/assist-edit.js +219 -30
- package/src/lib/server/assist-edit.js.map +1 -1
- package/src/lib/server/assist-page-change.d.ts +62 -0
- package/src/lib/server/assist-page-change.js +140 -0
- package/src/lib/server/assist-page-change.js.map +1 -0
- package/src/lib/site-import/fixtures/site-layout-recordings.d.ts +50 -0
- package/src/lib/site-import/fixtures/site-layout-recordings.js +86 -0
- package/src/lib/site-import/fixtures/site-layout-recordings.js.map +1 -0
- package/src/lib/site-import/site-import-crawl.d.ts +18 -1
- package/src/lib/site-import/site-import-crawl.js +70 -10
- package/src/lib/site-import/site-import-crawl.js.map +1 -1
- package/src/lib/site-import/site-import-css.d.ts +110 -0
- package/src/lib/site-import/site-import-css.js +826 -0
- package/src/lib/site-import/site-import-css.js.map +1 -0
- package/src/lib/site-import/site-import-html.d.ts +116 -1
- package/src/lib/site-import/site-import-html.js +538 -46
- package/src/lib/site-import/site-import-html.js.map +1 -1
- package/src/lib/site-import/site-import-layout.d.ts +79 -0
- package/src/lib/site-import/site-import-layout.js +1608 -0
- package/src/lib/site-import/site-import-layout.js.map +1 -0
- package/src/lib/site-import/site-import-nav.d.ts +40 -0
- package/src/lib/site-import/site-import-nav.js +437 -0
- package/src/lib/site-import/site-import-nav.js.map +1 -0
- package/src/lib/site-import/site-import-page.d.ts +8 -1
- package/src/lib/site-import/site-import-page.js +24 -2
- package/src/lib/site-import/site-import-page.js.map +1 -1
- package/src/lib/assets/site-styles/beauty.webp +0 -0
- package/src/lib/assets/site-styles/blog.webp +0 -0
- package/src/lib/assets/site-styles/business.webp +0 -0
- package/src/lib/assets/site-styles/coming-soon.webp +0 -0
- package/src/lib/assets/site-styles/education.webp +0 -0
- package/src/lib/assets/site-styles/events.webp +0 -0
- package/src/lib/assets/site-styles/fitness.webp +0 -0
- package/src/lib/assets/site-styles/landing.webp +0 -0
- package/src/lib/assets/site-styles/music.webp +0 -0
- package/src/lib/assets/site-styles/nonprofit.webp +0 -0
- package/src/lib/assets/site-styles/personal.webp +0 -0
- package/src/lib/assets/site-styles/photography.webp +0 -0
- package/src/lib/assets/site-styles/portfolio.webp +0 -0
- package/src/lib/assets/site-styles/professional.webp +0 -0
- package/src/lib/assets/site-styles/realestate.webp +0 -0
- package/src/lib/assets/site-styles/restaurant.webp +0 -0
- package/src/lib/assets/site-styles/store.webp +0 -0
- package/src/lib/assets/site-styles/studio.webp +0 -0
- package/src/lib/assets/site-styles/trades.webp +0 -0
- package/src/lib/assets/site-styles/wellness.webp +0 -0
- package/src/lib/assets/site-styles/yoga.webp +0 -0
- package/src/lib/site-import/site-import-review.d.ts +0 -40
- package/src/lib/site-import/site-import-review.js +0 -149
- package/src/lib/site-import/site-import-review.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-credit-estimate.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\n/**\n * WHAT A JOB REALLY COSTS, NEXT TO WHAT IT CAN COST (AGL-3722).\n *\n * Every job's cost used to be quoted at its CEILING: each pass at the\n * `AI_SITE_PASS_CREDITS` (50) a pass is reserved against. A Free Assist\n * build of two pages, a quote form and a layout was quoted 600 credits and\n * refused with 164 left, when builds like it measure about a fifth of that.\n *\n * So a job is now quoted as a RANGE: what it is likely to cost (the sum of\n * the measured medians of what it builds), what it costs nine times in ten\n * (the measured p90s), and its ceiling (the old figure, unchanged). The\n * meter still charges actual use, and the reservation each pass takes is\n * still the ceiling's 50 — only the words and the Free admission move:\n * a Free job is admitted outright when its p90 fits what is left, and asked\n * to be confirmed when it does not.\n *\n * This module is pure: the console, the doors and the specs read one table.\n */\n\n/** One measured cost: the median and the 90th percentile, in credits. */\nexport interface AiMeasuredCredits {\n median: number\n p90: number\n}\n\n/**\n * MEASURED IN PRODUCTION, credits per unit, over every `done` AI job since\n * 2026-10-01 (read 2026-10-09; a whole Free site job came to 148–160):\n *\n * page item 30 / 40 (a page of about `AI_MEASURED_PAGE_SECTIONS` sections, its listing included)\n * form 29 / 31\n * layout 23 / 24 (header and footer, one answer)\n * theme 4 / 8 (a site start's look)\n * plan step 21 / 70\n *\n * THE FAST TIER'S PART, REPRICED (AGL-3781). A site start's look, a Free\n * start's plan and every page's listing run on the fast tier, measured above\n * on Claude Haiku 4.5 at its cost. They run on Claude Haiku 5.5 at the\n * platform's markup now, so those parts are repriced from what production\n * sent, at the new rates and about 30% more tokens: over October 1–14 a look\n * 4 / 7 → 1 / 2, a Free start's plan 15 / 22 → 3 / 5, a listing 3 / 5 → 1 / 1\n * (2 less on every page), and a whole two-page Free site 148 → 121 at the\n * median. The other rows ran on the balanced tier and do not move.\n *\n * HOW TO RE-MEASURE: a read-only collection-group read of `aiJobs` in\n * production where `status == 'done'` and `createdAt` is in the window; group\n * each ledger row's (`items[]`) `creditsSpent` by its `op` (the site start's\n * look is the `theme` row), take the plan step's `steps[].creditsSpent` for\n * `plan`, and each job's `creditsSpent` for the whole-site band. Put each\n * group's median and 90th percentile here, and the dates in\n * `AI_MEASURED_AT`. `ai-credit-estimate.spec.ts` holds the table, the page\n * scaling and the measured whole-site band together, so a re-measure that\n * moves one moves the spec with it.\n */\nexport const AI_MEASURED_UNIT_CREDITS = {\n // 30 / 40 with its listing at Haiku 4.5's cost.\n page: { median: 28, p90: 38 },\n form: { median: 29, p90: 31 },\n layout: { median: 23, p90: 24 },\n // 4 / 8 on Haiku 4.5.\n theme: { median: 1, p90: 2 },\n // 21 / 70: the median is a Free start's plan, on the fast tier; the p90 a\n // plan on the balanced tier, which does not move.\n plan: { median: 4, p90: 70 },\n} as const satisfies Record<string, AiMeasuredCredits>\n\n/**\n * MEASURED IN PRODUCTION, credits per unit of a PAID site start, whose plan\n * and parts run on the paid routing (Sonnet) rather than a Free start's\n * (Haiku) the table above is mostly made of: every `done` paid site job\n * since 2026-10-01, read 2026-10-12 (6 jobs; products from the 4 stores; AGL-3739).\n *\n * plan step 81 / 86 (a Free start's plan: 20 / 42 on Haiku 4.5, 3 / 5 repriced)\n * page item 29 / 49 (about `AI_MEASURED_PAGE_SECTIONS` sections; 27 / 47 with its listing repriced)\n * form 32 / 39\n * layout 29 / 29\n * theme 4 / 4 (1 / 1 repriced)\n * email 49 / 51\n * products 64 / 66 (a store's first products, one answer)\n *\n * A 5-page store with its welcome email was quoted about 277 off the table\n * above: its plan at a Free start's 21 where paid plans run about 81, and its\n * first products not counted at all where they run about 64. Those stores\n * came to 413-448; this table quotes one at about 404.\n *\n * Re-measure as the table above says, keeping only jobs whose plan step's\n * `tokens.model` is the paid routing's.\n */\nexport const AI_MEASURED_PAID_SITE_CREDITS = {\n plan: { median: 81, p90: 86 },\n page: { median: 27, p90: 47 },\n form: { median: 32, p90: 39 },\n layout: { median: 29, p90: 29 },\n theme: { median: 1, p90: 1 },\n email: { median: 49, p90: 51 },\n products: { median: 64, p90: 66 },\n} as const satisfies Record<string, AiMeasuredCredits>\n\n/**\n * A page brought in from a person's own website (AGL-3976), priced as an AI\n * page: a paid site start's page as measured above, since an imported page is\n * one answer in the layout language and its listing, written inside one job\n * the way a site start writes its pages. Its site's header and footer, built\n * once in an import's first wave, is that table's layout.\n *\n * Re-measure from done `import` jobs' item ledgers once production has run\n * them: their `page` rows' `creditsSpent`, as the paid table is read.\n */\nexport const AI_MEASURED_IMPORT_CREDITS = {\n page: AI_MEASURED_PAID_SITE_CREDITS.page,\n frame: AI_MEASURED_PAID_SITE_CREDITS.layout,\n /**\n * A main page Aglyn AI arranges from its old page's own lines (AGL-4083):\n * one frame-sized structured answer, no prose, and its listing.\n * PROVISIONAL until the first live import measures it; it prices nothing\n * until the step marks a page `arranged`.\n */\n arranged: { median: 12, p90: 20 },\n /** A page copied as it is (AGL-4083): its listing pass alone. Provisional, as `arranged`. */\n copied: { median: 1, p90: 2 },\n} as const satisfies Record<string, AiMeasuredCredits>\n\n/**\n * When the table above was read, and from when its jobs ran. Both tables\n * were measured before AGL-3781 asked a paid site's plan at medium effort and\n * the layout language's pages and frames at low. A lower effort draws less,\n * so their plan, page and layout figures stand as the higher reading until\n * the next re-measure: a quote is never under what is drawn.\n */\nexport const AI_MEASURED_AT = { read: '2026-10-09', since: '2026-10-01' } as const\n\n/**\n * What a whole Free two-page site job measured at, in credits, in the same\n * window: 148–160 with its plan, look and listings at Haiku 4.5's cost, 27\n * less at the median with them repriced (AGL-3781).\n */\nexport const AI_MEASURED_FREE_SITE_CREDITS = { low: 121, high: 133 } as const\n\n/** The sections the measured page item is about: a page is scaled around it. */\nexport const AI_MEASURED_PAGE_SECTIONS = 5\n\n/**\n * What a page of this many sections is likely to cost: the measured page\n * scaled by its sections around the ~5-section median — about 6 credits a\n * section, 8 at the p90.\n */\nexport function aiMeasuredPageCredits(sections: number): AiMeasuredCredits {\n const count = Math.max(1, Math.floor(Number.isFinite(sections) ? sections : AI_MEASURED_PAGE_SECTIONS))\n const { median, p90 } = AI_MEASURED_UNIT_CREDITS.page\n return {\n median: Math.ceil((median * count) / AI_MEASURED_PAGE_SECTIONS),\n p90: Math.ceil((p90 * count) / AI_MEASURED_PAGE_SECTIONS),\n }\n}\n\n/**\n * What a page costs when it is written ON ITS OWN — a page an Assist build or\n * a page job writes, rather than one of a site start's pages (AGL-3722).\n *\n * The table's page item is a site start's: its pages are written one after\n * another inside one job, the later ones reading the prompt the first one\n * paid to write, so a 2-section second page measured 19–28. A page written on\n * its own pays that prompt itself and reads the site it lands on, and costs\n * what a site start's FIRST page costs: a 5-section home page measured 39–78\n * (median 40, p90 78), standalone page jobs' writing step 82–105 for a\n * 2-section page, and the Assist build pages of 2026-10-09 22–89 each. Quoting\n * a build's pages at the site start's scaled median told a Free workspace\n * \"about 48 credits\" for two 4-section pages (AGL-3722, 2026-10-10).\n *\n * So it is priced as a fixed cost — the prompt, the site, the listing — and a\n * cost per section: a 4-section page about 38 (p90 76), a 5-section 44 (p90 88).\n * The fixed part was 16 / 30 with its listing at Haiku 4.5's cost (AGL-3781).\n */\nexport const AI_MEASURED_STANDALONE_PAGE = {\n fixed: { median: 14, p90: 28 },\n perSection: { median: 6, p90: 12 },\n} as const satisfies Record<string, AiMeasuredCredits>\n\n/** What a page of this many sections written on its own is likely to cost. */\nexport function aiMeasuredStandalonePageCredits(sections: number): AiMeasuredCredits {\n const count = Math.max(1, Math.floor(Number.isFinite(sections) ? sections : AI_MEASURED_PAGE_SECTIONS))\n const { fixed, perSection } = AI_MEASURED_STANDALONE_PAGE\n return {\n median: fixed.median + perSection.median * count,\n p90: fixed.p90 + perSection.p90 * count,\n }\n}\n\n/**\n * What a creation costs when it is written ON ITS OWN (AGL-3722), where it\n * measured apart from a site start's: an Assist build's layout came to 24–28\n * (2026-10-09/10), against a site start's 23/24 written in its warm prompt.\n * A form measured the same either way (25–30 on its own, 29/31 in a site\n * start), so it keeps the table's figure.\n */\nexport const AI_MEASURED_STANDALONE_CREATION_CREDITS = {\n layout: { median: 26, p90: 30 },\n} as const satisfies Record<string, AiMeasuredCredits>\n\n/**\n * What one pass of a kind with no measurement of its own is likely to cost\n * (a component, an email design, a template, an item another runner\n * builds), written on its own: a component job's writing step measured\n * 52–65, a form job's 37, a layout job's 63, and the Assist build of\n * 2026-10-09 a welcome email at 46 and a products step at 60. Until\n * 2026-10-10 this was a site start's form (29/31), which is written inside a\n * site job's warm prompt and is the cheapest single answer there is.\n */\nexport const AI_MEASURED_PASS_CREDITS: AiMeasuredCredits = { median: 50, p90: 65 }\n\n/** A job's cost as it is quoted: likely, nine times in ten, and at its worst. */\nexport interface AiCreditRange {\n /** The sum of the measured medians: what the job is likely to spend. */\n likely: number\n /** The sum of the measured p90s: what a Free job is admitted on. */\n p90: number\n /** Every pass at its reserve: what it can spend at most. */\n ceiling: number\n}\n\nexport const AI_CREDIT_RANGE_ZERO: AiCreditRange = { likely: 0, p90: 0, ceiling: 0 }\n\n/** Two ranges added. */\nexport function aiCreditRangeAdd(a: AiCreditRange, b: AiCreditRange): AiCreditRange {\n return { likely: a.likely + b.likely, p90: a.p90 + b.p90, ceiling: a.ceiling + b.ceiling }\n}\n\n/** A measured cost as a range with its own ceiling, the likely and p90 never above it. */\nexport function aiCreditRangeOf(measured: AiMeasuredCredits, ceiling: number): AiCreditRange {\n const top = Math.max(0, Math.ceil(ceiling))\n return { likely: Math.min(measured.median, top), p90: Math.min(measured.p90, top), ceiling: top }\n}\n\n/** A range whose likely and p90 are each at least the last, and at most the ceiling. */\nexport function aiCreditRangeOrdered(range: AiCreditRange): AiCreditRange {\n const ceiling = Math.max(0, Math.ceil(range.ceiling))\n const likely = Math.min(Math.max(0, Math.ceil(range.likely)), ceiling)\n const p90 = Math.min(Math.max(likely, Math.ceil(range.p90)), ceiling)\n return { likely, p90, ceiling }\n}\n\nconst n = (value: number) => value.toLocaleString('en-US')\n\n/**\n * The range as every surface quotes it: \"About 106 credits (up to 650)\".\n * A job whose likely figure is its ceiling says one number.\n */\nexport function aiCreditRangeText(range: AiCreditRange): string {\n if (range.likely >= range.ceiling) return `About ${n(range.ceiling)} credits`\n return `About ${n(range.likely)} credits (up to ${n(range.ceiling)})`\n}\n\n/**\n * Whether a Free job of this range needs the person's go-ahead before it\n * starts (AGL-3722): its p90 is more than what is left. A job whose p90\n * fits starts with no prompt; one that does not starts only when confirmed,\n * builds as much as what is left pays for, and pauses with Resume.\n */\nexport function aiCreditsNeedConfirm(range: Pick<AiCreditRange, 'p90'>, left: number): boolean {\n return Number.isFinite(left) && range.p90 > Math.max(0, Math.floor(left))\n}\n\n/** The smaller first build a Free job that does not fit is offered (AGL-3722). */\nexport interface AiCreditsSmaller {\n /** \"Build the home page first\". */\n label: string\n likely: number\n p90: number\n ceiling: number\n}\n\n/**\n * What a Free job that does not fit what is left says before it starts, and\n * the choices it offers: build what fits, a smaller first build, or Upgrade.\n * The door that refuses an unconfirmed start answers with it (status 409,\n * `code: AI_CREDITS_CONFIRM_CODE`), and the plan card and the guided start\n * draw it from the same figures.\n */\nexport interface AiCreditsPrompt extends AiCreditRange {\n left: number\n resetsOn: string\n smaller: AiCreditsSmaller | null\n}\n\n/** The `code` a door answers an unconfirmed Free start with. */\nexport const AI_CREDITS_CONFIRM_CODE = 'credits-confirm'\n\n/** \"Build what fits\": the go-ahead for a Free job past what is left. */\nexport const AI_CREDITS_BUILD_WHAT_FITS = 'Build what fits'\n\n/** The smaller first build's button: \"Build the home page first (about 64 credits)\". */\nexport function aiCreditsSmallerText(smaller: Pick<AiCreditsSmaller, 'label' | 'likely'>): string {\n return `${smaller.label} (about ${n(smaller.likely)} credits)`\n}\n\n/** Which way on a prompt offers, in the order it offers them. */\nexport type AiCreditsChoiceKey = 'fits' | 'smaller' | 'upgrade'\n\n/** One way on, as its option button reads it: a short label and the figure under it (AGL-3722). */\nexport interface AiCreditsChoice {\n key: AiCreditsChoiceKey\n label: string\n detail: string\n /** The one choice drawn as the primary button. */\n recommended: boolean\n}\n\nconst credit = (count: number) => `${n(count)} ${count === 1 ? 'credit' : 'credits'}`\n\n/**\n * The prompt's ways on, as its three option buttons read them (Zach,\n * 2026-10-10: \"more appealing and consistent\"): build what fits, the smaller\n * first build when there is one, and Upgrade when it can be offered — each a\n * short sentence-case label with its figure under it. The smaller build is\n * the recommended one when its p90 fits what is left, since it then finishes;\n * otherwise building what fits is.\n */\nexport function aiCreditsChoices(\n prompt: Pick<AiCreditsPrompt, 'left' | 'smaller'>,\n offers: { smaller: boolean; upgrade: boolean },\n): AiCreditsChoice[] {\n const left = Math.max(0, Math.floor(prompt.left))\n const smaller = offers.smaller ? prompt.smaller : null\n const smallerFits = smaller !== null && smaller.p90 <= left\n const choices: AiCreditsChoice[] = [\n {\n key: 'fits',\n label: AI_CREDITS_BUILD_WHAT_FITS,\n detail: `Uses your ${credit(left)} left, then pauses`,\n recommended: !smallerFits,\n },\n ]\n if (smaller) {\n choices.push({\n key: 'smaller',\n label: smaller.label,\n detail: smallerFits ? `About ${credit(smaller.likely)}, fits what you have left` : `About ${credit(smaller.likely)}`,\n recommended: smallerFits,\n })\n }\n if (offers.upgrade) choices.push({ key: 'upgrade', label: 'Upgrade', detail: 'Get more credits', recommended: false })\n return choices\n}\n\n/** A reset day as a person reads it: \"November 1\". */\nfunction resetLabel(resetsOn: string): string {\n const at = new Date(`${resetsOn}T00:00:00.000Z`)\n return Number.isFinite(at.getTime())\n ? at.toLocaleDateString('en-US', { month: 'long', day: 'numeric', timeZone: 'UTC' })\n : 'the first of next month'\n}\n\n/**\n * The plain sentence (Zach, 2026-10-09): \"This build is about N credits (up\n * to M). You have L left, so it will build as much as it can and pause when\n * your credits run out. You can upgrade or resume when they renew.\"\n */\nexport function aiCreditsPromptText(prompt: AiCreditsPrompt, noun: 'build' | 'site' = 'build'): string {\n const size =\n prompt.likely >= prompt.ceiling\n ? `about ${n(prompt.ceiling)} credits`\n : `about ${n(prompt.likely)} credits (up to ${n(prompt.ceiling)})`\n const left = Math.max(0, Math.floor(prompt.left))\n return (\n `This ${noun} is ${size}. You have ${n(left)} left, so it will build as much as it can and pause ` +\n `when your credits run out. You can upgrade or resume when they renew on ${resetLabel(prompt.resetsOn)}.`\n )\n}\n\n/**\n * The record a confirmed Free start keeps (AGL-3722): who said go, when,\n * what was left then and the range they were shown.\n */\nexport interface AiJobCreditsConfirmation extends AiCreditRange {\n by: string\n at: Date | string\n left: number\n}\n\n/** The confirmation a door keeps on the job for the prompt the person confirmed, or `null` when there was none. */\nexport function aiCreditsConfirmation(\n prompt: AiCreditsPrompt | null,\n by: string,\n at: Date,\n): AiJobCreditsConfirmation | null {\n if (!prompt) return null\n return { by, at, left: prompt.left, likely: prompt.likely, p90: prompt.p90, ceiling: prompt.ceiling }\n}\n\n/** The prompt a range needs against what is left, or `null` when it fits. */\nexport function aiCreditsPromptFor(\n range: AiCreditRange,\n credits: { left: number; resetsOn: string } | null | undefined,\n smaller: AiCreditsSmaller | null = null,\n): AiCreditsPrompt | null {\n if (!credits || !aiCreditsNeedConfirm(range, credits.left)) return null\n return { ...range, left: Math.max(0, Math.floor(credits.left)), resetsOn: credits.resetsOn, smaller }\n}\n"],"names":["AI_MEASURED_UNIT_CREDITS","page","median","p90","form","layout","theme","plan","AI_MEASURED_PAID_SITE_CREDITS","email","products","AI_MEASURED_IMPORT_CREDITS","frame","arranged","copied","AI_MEASURED_AT","read","since","AI_MEASURED_FREE_SITE_CREDITS","low","high","AI_MEASURED_PAGE_SECTIONS","aiMeasuredPageCredits","sections","count","Math","max","floor","Number","isFinite","ceil","AI_MEASURED_STANDALONE_PAGE","fixed","perSection","aiMeasuredStandalonePageCredits","AI_MEASURED_STANDALONE_CREATION_CREDITS","AI_MEASURED_PASS_CREDITS","AI_CREDIT_RANGE_ZERO","likely","ceiling","aiCreditRangeAdd","a","b","aiCreditRangeOf","measured","top","min","aiCreditRangeOrdered","range","n","value","toLocaleString","aiCreditRangeText","aiCreditsNeedConfirm","left","AI_CREDITS_CONFIRM_CODE","AI_CREDITS_BUILD_WHAT_FITS","aiCreditsSmallerText","smaller","label","credit","aiCreditsChoices","prompt","offers","smallerFits","choices","key","detail","recommended","push","upgrade","resetLabel","resetsOn","at","Date","getTime","toLocaleDateString","month","day","timeZone","aiCreditsPromptText","noun","size","aiCreditsConfirmation","by","aiCreditsPromptFor","credits"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-credit-estimate.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\n/**\n * WHAT A JOB REALLY COSTS, NEXT TO WHAT IT CAN COST (AGL-3722).\n *\n * Every job's cost used to be quoted at its CEILING: each pass at the\n * `AI_SITE_PASS_CREDITS` (50) a pass is reserved against. A Free Assist\n * build of two pages, a quote form and a layout was quoted 600 credits and\n * refused with 164 left, when builds like it measure about a fifth of that.\n *\n * So a job is now quoted as a RANGE: what it is likely to cost (the sum of\n * the measured medians of what it builds), what it costs nine times in ten\n * (the measured p90s), and its ceiling (the old figure, unchanged). The\n * meter still charges actual use, and the reservation each pass takes is\n * still the ceiling's 50 — only the words and the Free admission move:\n * a Free job is admitted outright when its p90 fits what is left, and asked\n * to be confirmed when it does not.\n *\n * This module is pure: the console, the doors and the specs read one table.\n */\n\n/** One measured cost: the median and the 90th percentile, in credits. */\nexport interface AiMeasuredCredits {\n median: number\n p90: number\n}\n\n/**\n * MEASURED IN PRODUCTION, credits per unit, over every `done` AI job since\n * 2026-10-01 (read 2026-10-09; a whole Free site job came to 148–160):\n *\n * page item 30 / 40 (a page of about `AI_MEASURED_PAGE_SECTIONS` sections, its listing included)\n * form 29 / 31\n * layout 23 / 24 (header and footer, one answer)\n * theme 4 / 8 (a site start's look)\n * plan step 21 / 70\n *\n * THE FAST TIER'S PART, REPRICED (AGL-3781). A site start's look, a Free\n * start's plan and every page's listing run on the fast tier, measured above\n * on Claude Haiku 4.5 at its cost. They run on Claude Haiku 5.5 at the\n * platform's markup now, so those parts are repriced from what production\n * sent, at the new rates and about 30% more tokens: over October 1–14 a look\n * 4 / 7 → 1 / 2, a Free start's plan 15 / 22 → 3 / 5, a listing 3 / 5 → 1 / 1\n * (2 less on every page), and a whole two-page Free site 148 → 121 at the\n * median. The other rows ran on the balanced tier and do not move.\n *\n * HOW TO RE-MEASURE: a read-only collection-group read of `aiJobs` in\n * production where `status == 'done'` and `createdAt` is in the window; group\n * each ledger row's (`items[]`) `creditsSpent` by its `op` (the site start's\n * look is the `theme` row), take the plan step's `steps[].creditsSpent` for\n * `plan`, and each job's `creditsSpent` for the whole-site band. Put each\n * group's median and 90th percentile here, and the dates in\n * `AI_MEASURED_AT`. `ai-credit-estimate.spec.ts` holds the table, the page\n * scaling and the measured whole-site band together, so a re-measure that\n * moves one moves the spec with it.\n */\nexport const AI_MEASURED_UNIT_CREDITS = {\n // 30 / 40 with its listing at Haiku 4.5's cost.\n page: { median: 28, p90: 38 },\n form: { median: 29, p90: 31 },\n layout: { median: 23, p90: 24 },\n // 4 / 8 on Haiku 4.5.\n theme: { median: 1, p90: 2 },\n // 21 / 70: the median is a Free start's plan, on the fast tier; the p90 a\n // plan on the balanced tier, which does not move.\n plan: { median: 4, p90: 70 },\n} as const satisfies Record<string, AiMeasuredCredits>\n\n/**\n * MEASURED IN PRODUCTION, credits per unit of a PAID site start, whose plan\n * and parts run on the paid routing (Sonnet) rather than a Free start's\n * (Haiku) the table above is mostly made of: every `done` paid site job\n * since 2026-10-01, read 2026-10-12 (6 jobs; products from the 4 stores; AGL-3739).\n *\n * plan step 81 / 86 (a Free start's plan: 20 / 42 on Haiku 4.5, 3 / 5 repriced)\n * page item 29 / 49 (about `AI_MEASURED_PAGE_SECTIONS` sections; 27 / 47 with its listing repriced)\n * form 32 / 39\n * layout 29 / 29\n * theme 4 / 4 (1 / 1 repriced)\n * email 49 / 51\n * products 64 / 66 (a store's first products, one answer)\n *\n * A 5-page store with its welcome email was quoted about 277 off the table\n * above: its plan at a Free start's 21 where paid plans run about 81, and its\n * first products not counted at all where they run about 64. Those stores\n * came to 413-448; this table quotes one at about 404.\n *\n * Re-measure as the table above says, keeping only jobs whose plan step's\n * `tokens.model` is the paid routing's.\n */\nexport const AI_MEASURED_PAID_SITE_CREDITS = {\n plan: { median: 81, p90: 86 },\n page: { median: 27, p90: 47 },\n form: { median: 32, p90: 39 },\n layout: { median: 29, p90: 29 },\n theme: { median: 1, p90: 1 },\n email: { median: 49, p90: 51 },\n products: { median: 64, p90: 66 },\n} as const satisfies Record<string, AiMeasuredCredits>\n\n/**\n * A page brought in from a person's own website (AGL-3976), priced as an AI\n * page: a paid site start's page as measured above, since an imported page is\n * one answer in the layout language and its listing, written inside one job\n * the way a site start writes its pages. Its site's header and footer, built\n * once in an import's first wave, is that table's layout.\n *\n * Re-measure from done `import` jobs' item ledgers once production has run\n * them: their `page` rows' `creditsSpent`, as the paid table is read.\n */\nexport const AI_MEASURED_IMPORT_CREDITS = {\n page: AI_MEASURED_PAID_SITE_CREDITS.page,\n frame: AI_MEASURED_PAID_SITE_CREDITS.layout,\n /**\n * A main page whose design Aglyn AI answers (AGL-4107): one small answer on\n * the balanced tier over the page's read structure — about 1,000 to 1,300\n * tokens in, two thirds of them its cached instructions, and 500 to 900 out\n * with its thinking, $0.009–$0.017 at Sonnet 5.5's billed rates over the\n * recorded aglyn.com pages — and no listing, the source's own being kept.\n * The 10/29 run spent 36–58 a page on a smaller model for a line-by-line\n * answer twenty times the size. PROVISIONAL until the next live import\n * measures it.\n */\n arranged: { median: 16, p90: 30 },\n /** A page copied as it is (AGL-4083): no model, and a listing only where the source has none. Provisional, as `arranged`. */\n copied: { median: 1, p90: 2 },\n} as const satisfies Record<string, AiMeasuredCredits>\n\n/**\n * When the table above was read, and from when its jobs ran. Both tables\n * were measured before AGL-3781 asked a paid site's plan at medium effort and\n * the layout language's pages and frames at low. A lower effort draws less,\n * so their plan, page and layout figures stand as the higher reading until\n * the next re-measure: a quote is never under what is drawn.\n */\nexport const AI_MEASURED_AT = { read: '2026-10-09', since: '2026-10-01' } as const\n\n/**\n * What a whole Free two-page site job measured at, in credits, in the same\n * window: 148–160 with its plan, look and listings at Haiku 4.5's cost, 27\n * less at the median with them repriced (AGL-3781).\n */\nexport const AI_MEASURED_FREE_SITE_CREDITS = { low: 121, high: 133 } as const\n\n/** The sections the measured page item is about: a page is scaled around it. */\nexport const AI_MEASURED_PAGE_SECTIONS = 5\n\n/**\n * What a page of this many sections is likely to cost: the measured page\n * scaled by its sections around the ~5-section median — about 6 credits a\n * section, 8 at the p90.\n */\nexport function aiMeasuredPageCredits(sections: number): AiMeasuredCredits {\n const count = Math.max(1, Math.floor(Number.isFinite(sections) ? sections : AI_MEASURED_PAGE_SECTIONS))\n const { median, p90 } = AI_MEASURED_UNIT_CREDITS.page\n return {\n median: Math.ceil((median * count) / AI_MEASURED_PAGE_SECTIONS),\n p90: Math.ceil((p90 * count) / AI_MEASURED_PAGE_SECTIONS),\n }\n}\n\n/**\n * What a page costs when it is written ON ITS OWN — a page an Assist build or\n * a page job writes, rather than one of a site start's pages (AGL-3722).\n *\n * The table's page item is a site start's: its pages are written one after\n * another inside one job, the later ones reading the prompt the first one\n * paid to write, so a 2-section second page measured 19–28. A page written on\n * its own pays that prompt itself and reads the site it lands on, and costs\n * what a site start's FIRST page costs: a 5-section home page measured 39–78\n * (median 40, p90 78), standalone page jobs' writing step 82–105 for a\n * 2-section page, and the Assist build pages of 2026-10-09 22–89 each. Quoting\n * a build's pages at the site start's scaled median told a Free workspace\n * \"about 48 credits\" for two 4-section pages (AGL-3722, 2026-10-10).\n *\n * So it is priced as a fixed cost — the prompt, the site, the listing — and a\n * cost per section: a 4-section page about 38 (p90 76), a 5-section 44 (p90 88).\n * The fixed part was 16 / 30 with its listing at Haiku 4.5's cost (AGL-3781).\n */\nexport const AI_MEASURED_STANDALONE_PAGE = {\n fixed: { median: 14, p90: 28 },\n perSection: { median: 6, p90: 12 },\n} as const satisfies Record<string, AiMeasuredCredits>\n\n/** What a page of this many sections written on its own is likely to cost. */\nexport function aiMeasuredStandalonePageCredits(sections: number): AiMeasuredCredits {\n const count = Math.max(1, Math.floor(Number.isFinite(sections) ? sections : AI_MEASURED_PAGE_SECTIONS))\n const { fixed, perSection } = AI_MEASURED_STANDALONE_PAGE\n return {\n median: fixed.median + perSection.median * count,\n p90: fixed.p90 + perSection.p90 * count,\n }\n}\n\n/**\n * What a creation costs when it is written ON ITS OWN (AGL-3722), where it\n * measured apart from a site start's: an Assist build's layout came to 24–28\n * (2026-10-09/10), against a site start's 23/24 written in its warm prompt.\n * A form measured the same either way (25–30 on its own, 29/31 in a site\n * start), so it keeps the table's figure.\n */\nexport const AI_MEASURED_STANDALONE_CREATION_CREDITS = {\n layout: { median: 26, p90: 30 },\n} as const satisfies Record<string, AiMeasuredCredits>\n\n/**\n * What one pass of a kind with no measurement of its own is likely to cost\n * (a component, an email design, a template, an item another runner\n * builds), written on its own: a component job's writing step measured\n * 52–65, a form job's 37, a layout job's 63, and the Assist build of\n * 2026-10-09 a welcome email at 46 and a products step at 60. Until\n * 2026-10-10 this was a site start's form (29/31), which is written inside a\n * site job's warm prompt and is the cheapest single answer there is.\n */\nexport const AI_MEASURED_PASS_CREDITS: AiMeasuredCredits = { median: 50, p90: 65 }\n\n/** A job's cost as it is quoted: likely, nine times in ten, and at its worst. */\nexport interface AiCreditRange {\n /** The sum of the measured medians: what the job is likely to spend. */\n likely: number\n /** The sum of the measured p90s: what a Free job is admitted on. */\n p90: number\n /** Every pass at its reserve: what it can spend at most. */\n ceiling: number\n}\n\nexport const AI_CREDIT_RANGE_ZERO: AiCreditRange = { likely: 0, p90: 0, ceiling: 0 }\n\n/** Two ranges added. */\nexport function aiCreditRangeAdd(a: AiCreditRange, b: AiCreditRange): AiCreditRange {\n return { likely: a.likely + b.likely, p90: a.p90 + b.p90, ceiling: a.ceiling + b.ceiling }\n}\n\n/** A measured cost as a range with its own ceiling, the likely and p90 never above it. */\nexport function aiCreditRangeOf(measured: AiMeasuredCredits, ceiling: number): AiCreditRange {\n const top = Math.max(0, Math.ceil(ceiling))\n return { likely: Math.min(measured.median, top), p90: Math.min(measured.p90, top), ceiling: top }\n}\n\n/** A range whose likely and p90 are each at least the last, and at most the ceiling. */\nexport function aiCreditRangeOrdered(range: AiCreditRange): AiCreditRange {\n const ceiling = Math.max(0, Math.ceil(range.ceiling))\n const likely = Math.min(Math.max(0, Math.ceil(range.likely)), ceiling)\n const p90 = Math.min(Math.max(likely, Math.ceil(range.p90)), ceiling)\n return { likely, p90, ceiling }\n}\n\nconst n = (value: number) => value.toLocaleString('en-US')\n\n/**\n * The range as every surface quotes it: \"About 106 credits (up to 650)\".\n * A job whose likely figure is its ceiling says one number.\n */\nexport function aiCreditRangeText(range: AiCreditRange): string {\n if (range.likely >= range.ceiling) return `About ${n(range.ceiling)} credits`\n return `About ${n(range.likely)} credits (up to ${n(range.ceiling)})`\n}\n\n/**\n * Whether a Free job of this range needs the person's go-ahead before it\n * starts (AGL-3722): its p90 is more than what is left. A job whose p90\n * fits starts with no prompt; one that does not starts only when confirmed,\n * builds as much as what is left pays for, and pauses with Resume.\n */\nexport function aiCreditsNeedConfirm(range: Pick<AiCreditRange, 'p90'>, left: number): boolean {\n return Number.isFinite(left) && range.p90 > Math.max(0, Math.floor(left))\n}\n\n/** The smaller first build a Free job that does not fit is offered (AGL-3722). */\nexport interface AiCreditsSmaller {\n /** \"Build the home page first\". */\n label: string\n likely: number\n p90: number\n ceiling: number\n}\n\n/**\n * What a Free job that does not fit what is left says before it starts, and\n * the choices it offers: build what fits, a smaller first build, or Upgrade.\n * The door that refuses an unconfirmed start answers with it (status 409,\n * `code: AI_CREDITS_CONFIRM_CODE`), and the plan card and the guided start\n * draw it from the same figures.\n */\nexport interface AiCreditsPrompt extends AiCreditRange {\n left: number\n resetsOn: string\n smaller: AiCreditsSmaller | null\n}\n\n/** The `code` a door answers an unconfirmed Free start with. */\nexport const AI_CREDITS_CONFIRM_CODE = 'credits-confirm'\n\n/** \"Build what fits\": the go-ahead for a Free job past what is left. */\nexport const AI_CREDITS_BUILD_WHAT_FITS = 'Build what fits'\n\n/** The smaller first build's button: \"Build the home page first (about 64 credits)\". */\nexport function aiCreditsSmallerText(smaller: Pick<AiCreditsSmaller, 'label' | 'likely'>): string {\n return `${smaller.label} (about ${n(smaller.likely)} credits)`\n}\n\n/** Which way on a prompt offers, in the order it offers them. */\nexport type AiCreditsChoiceKey = 'fits' | 'smaller' | 'upgrade'\n\n/** One way on, as its option button reads it: a short label and the figure under it (AGL-3722). */\nexport interface AiCreditsChoice {\n key: AiCreditsChoiceKey\n label: string\n detail: string\n /** The one choice drawn as the primary button. */\n recommended: boolean\n}\n\nconst credit = (count: number) => `${n(count)} ${count === 1 ? 'credit' : 'credits'}`\n\n/**\n * The prompt's ways on, as its three option buttons read them (Zach,\n * 2026-10-10: \"more appealing and consistent\"): build what fits, the smaller\n * first build when there is one, and Upgrade when it can be offered — each a\n * short sentence-case label with its figure under it. The smaller build is\n * the recommended one when its p90 fits what is left, since it then finishes;\n * otherwise building what fits is.\n */\nexport function aiCreditsChoices(\n prompt: Pick<AiCreditsPrompt, 'left' | 'smaller'>,\n offers: { smaller: boolean; upgrade: boolean },\n): AiCreditsChoice[] {\n const left = Math.max(0, Math.floor(prompt.left))\n const smaller = offers.smaller ? prompt.smaller : null\n const smallerFits = smaller !== null && smaller.p90 <= left\n const choices: AiCreditsChoice[] = [\n {\n key: 'fits',\n label: AI_CREDITS_BUILD_WHAT_FITS,\n detail: `Uses your ${credit(left)} left, then pauses`,\n recommended: !smallerFits,\n },\n ]\n if (smaller) {\n choices.push({\n key: 'smaller',\n label: smaller.label,\n detail: smallerFits ? `About ${credit(smaller.likely)}, fits what you have left` : `About ${credit(smaller.likely)}`,\n recommended: smallerFits,\n })\n }\n if (offers.upgrade) choices.push({ key: 'upgrade', label: 'Upgrade', detail: 'Get more credits', recommended: false })\n return choices\n}\n\n/** A reset day as a person reads it: \"November 1\". */\nfunction resetLabel(resetsOn: string): string {\n const at = new Date(`${resetsOn}T00:00:00.000Z`)\n return Number.isFinite(at.getTime())\n ? at.toLocaleDateString('en-US', { month: 'long', day: 'numeric', timeZone: 'UTC' })\n : 'the first of next month'\n}\n\n/**\n * The plain sentence (Zach, 2026-10-09): \"This build is about N credits (up\n * to M). You have L left, so it will build as much as it can and pause when\n * your credits run out. You can upgrade or resume when they renew.\"\n */\nexport function aiCreditsPromptText(prompt: AiCreditsPrompt, noun: 'build' | 'site' = 'build'): string {\n const size =\n prompt.likely >= prompt.ceiling\n ? `about ${n(prompt.ceiling)} credits`\n : `about ${n(prompt.likely)} credits (up to ${n(prompt.ceiling)})`\n const left = Math.max(0, Math.floor(prompt.left))\n return (\n `This ${noun} is ${size}. You have ${n(left)} left, so it will build as much as it can and pause ` +\n `when your credits run out. You can upgrade or resume when they renew on ${resetLabel(prompt.resetsOn)}.`\n )\n}\n\n/**\n * The record a confirmed Free start keeps (AGL-3722): who said go, when,\n * what was left then and the range they were shown.\n */\nexport interface AiJobCreditsConfirmation extends AiCreditRange {\n by: string\n at: Date | string\n left: number\n}\n\n/** The confirmation a door keeps on the job for the prompt the person confirmed, or `null` when there was none. */\nexport function aiCreditsConfirmation(\n prompt: AiCreditsPrompt | null,\n by: string,\n at: Date,\n): AiJobCreditsConfirmation | null {\n if (!prompt) return null\n return { by, at, left: prompt.left, likely: prompt.likely, p90: prompt.p90, ceiling: prompt.ceiling }\n}\n\n/** The prompt a range needs against what is left, or `null` when it fits. */\nexport function aiCreditsPromptFor(\n range: AiCreditRange,\n credits: { left: number; resetsOn: string } | null | undefined,\n smaller: AiCreditsSmaller | null = null,\n): AiCreditsPrompt | null {\n if (!credits || !aiCreditsNeedConfirm(range, credits.left)) return null\n return { ...range, left: Math.max(0, Math.floor(credits.left)), resetsOn: credits.resetsOn, smaller }\n}\n"],"names":["AI_MEASURED_UNIT_CREDITS","page","median","p90","form","layout","theme","plan","AI_MEASURED_PAID_SITE_CREDITS","email","products","AI_MEASURED_IMPORT_CREDITS","frame","arranged","copied","AI_MEASURED_AT","read","since","AI_MEASURED_FREE_SITE_CREDITS","low","high","AI_MEASURED_PAGE_SECTIONS","aiMeasuredPageCredits","sections","count","Math","max","floor","Number","isFinite","ceil","AI_MEASURED_STANDALONE_PAGE","fixed","perSection","aiMeasuredStandalonePageCredits","AI_MEASURED_STANDALONE_CREATION_CREDITS","AI_MEASURED_PASS_CREDITS","AI_CREDIT_RANGE_ZERO","likely","ceiling","aiCreditRangeAdd","a","b","aiCreditRangeOf","measured","top","min","aiCreditRangeOrdered","range","n","value","toLocaleString","aiCreditRangeText","aiCreditsNeedConfirm","left","AI_CREDITS_CONFIRM_CODE","AI_CREDITS_BUILD_WHAT_FITS","aiCreditsSmallerText","smaller","label","credit","aiCreditsChoices","prompt","offers","smallerFits","choices","key","detail","recommended","push","upgrade","resetLabel","resetsOn","at","Date","getTime","toLocaleDateString","month","day","timeZone","aiCreditsPromptText","noun","size","aiCreditsConfirmation","by","aiCreditsPromptFor","credits"],"mappings":"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"}
|
|
@@ -23,13 +23,17 @@
|
|
|
23
23
|
* — against the stored document instead of an open canvas, and the change is
|
|
24
24
|
* saved where no visitor sees it:
|
|
25
25
|
*
|
|
26
|
-
* - on
|
|
27
|
-
* started from
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
26
|
+
* - by default, on every plan, into the WORKING DRAFT of the version it
|
|
27
|
+
* started from (AGL-4116) — the draft the Besigner saves to as an author
|
|
28
|
+
* works and offers with Open draft, so the author reviews the change and
|
|
29
|
+
* publishes it there exactly as they would their own. A draft the author
|
|
30
|
+
* already has is built on, never replaced;
|
|
31
|
+
* - when the request asks for a new version and the plan keeps version
|
|
32
|
+
* history, as a NEW version beside the one it started from, which becomes
|
|
33
|
+
* the page's current version only when the page is not published; a
|
|
34
|
+
* published page's and a layout's current version never move. Asked for
|
|
35
|
+
* on a plan without version history, the change goes into the draft and
|
|
36
|
+
* the output says why.
|
|
33
37
|
*
|
|
34
38
|
* Pure: the inputs, the sentences a person reads, and the outline the model
|
|
35
39
|
* is shown. The runner is `jobs/ai-job-edit-step.ts`.
|
|
@@ -44,6 +48,8 @@ export interface AiEditJobInputs {
|
|
|
44
48
|
targetKind: AiEditTargetKind | null;
|
|
45
49
|
/** The version to start from; the document's current version when absent. */
|
|
46
50
|
versionId: string | null;
|
|
51
|
+
/** The request asked for a new version; otherwise the change goes into the draft. */
|
|
52
|
+
newVersion: boolean;
|
|
47
53
|
}
|
|
48
54
|
export declare const AI_EDIT_TARGET_REQUIRED_COPY = "Name the page or layout to change.";
|
|
49
55
|
export declare const AI_EDIT_TARGET_KIND_COPY = "inputs.targetKind must be screen or layout";
|
|
@@ -78,13 +84,22 @@ export type AiEditPlacement =
|
|
|
78
84
|
'new-current'
|
|
79
85
|
/** A new version beside the one it started from, which is unchanged. */
|
|
80
86
|
| 'new-beside'
|
|
81
|
-
/** The version it started from, changed in place: one no visitor reaches. */
|
|
82
|
-
| 'in-place'
|
|
87
|
+
/** The version it started from, changed in place: one no visitor reaches. Written before AGL-4116. */
|
|
88
|
+
| 'in-place'
|
|
89
|
+
/** The working draft of the version it started from, which the Besigner offers with Open draft. */
|
|
90
|
+
| 'draft';
|
|
91
|
+
/**
|
|
92
|
+
* Why a change asked for as a new version went into the draft: the plan
|
|
93
|
+
* keeps no version history. The versions route's own words for the plan.
|
|
94
|
+
*/
|
|
95
|
+
export declare const AI_EDIT_NO_VERSIONS_DRAFT_COPY = "A new version needs version history, which requires a Pro plan, so the change went into the draft instead.";
|
|
83
96
|
/** The output's note: where the change is, that nothing visitors see moved, and what was left out. */
|
|
84
97
|
export declare function aiEditOutputNote(input: {
|
|
85
98
|
kind: AiEditTargetKind;
|
|
86
99
|
placement: AiEditPlacement;
|
|
87
100
|
leftOut: readonly string[];
|
|
101
|
+
/** A new version was asked for on a plan without version history. */
|
|
102
|
+
versionsUnavailable?: boolean;
|
|
88
103
|
}): string;
|
|
89
104
|
/** What the output says of search fields a published page keeps on the page itself. */
|
|
90
105
|
export declare const AI_EDIT_SEO_LEFT_OUT = "the search title and description, which a published page uses as soon as they are saved \u2014 change them in Page properties";
|
|
@@ -92,15 +107,17 @@ export declare const AI_EDIT_SEO_LEFT_OUT = "the search title and description, w
|
|
|
92
107
|
export declare const AI_EDIT_COMPONENT_LEFT_OUT = "saving part of the page as a reusable component, which is done in the editor";
|
|
93
108
|
/**
|
|
94
109
|
* The stored document as the outline the edit rung's protocol reads
|
|
95
|
-
* (`parseAssistEditContext` holds it to its limits): the
|
|
96
|
-
* every element
|
|
97
|
-
* chain the check walks is whole —
|
|
110
|
+
* (`parseAssistEditContext` with `whole` holds it to its limits): the
|
|
111
|
+
* document root, then every element in the order it reads on the page — each
|
|
112
|
+
* after its parent, so every ancestor chain the check walks is whole — with
|
|
113
|
+
* its settings and its styles, up to the whole-document cap.
|
|
98
114
|
*
|
|
99
115
|
* In the editor, an outline is the neighborhood of what the author selected.
|
|
100
|
-
* A job has no selection, so it is the
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
116
|
+
* A job has no selection and nobody to ask, so it is the WHOLE document: a
|
|
117
|
+
* request about a picture five levels down is about an element the model can
|
|
118
|
+
* see, and a restyle starts from the styles the element has. Each element
|
|
119
|
+
* keeps its real child count and the document its real size, so the prompt
|
|
120
|
+
* can say what a document past the cap leaves out. `null` without a root.
|
|
104
121
|
*/
|
|
105
122
|
export declare function aiEditJobOutline(nodes: Readonly<Record<string, unknown>>): {
|
|
106
123
|
selectedId: null;
|
|
@@ -15,7 +15,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
15
15
|
* See the License for the specific language governing permissions and
|
|
16
16
|
* limitations under the License.
|
|
17
17
|
*/ import { CANVAS_ROOT_ELEMENT_ID } from "@aglyn/aglyn/foundation/constants/canvas";
|
|
18
|
-
import {
|
|
18
|
+
import { ASSIST_EDIT_WHOLE_MAX_NODES } from "./assist-edit.js";
|
|
19
19
|
/**
|
|
20
20
|
* An `edit` job (AGL-3616): a change to a page or a layout the site already
|
|
21
21
|
* has, asked for in words, made without the Besigner open.
|
|
@@ -25,13 +25,17 @@ import { ASSIST_EDIT_CONTEXT_MAX_NODES } from "./assist-edit.js";
|
|
|
25
25
|
* — against the stored document instead of an open canvas, and the change is
|
|
26
26
|
* saved where no visitor sees it:
|
|
27
27
|
*
|
|
28
|
-
* - on
|
|
29
|
-
* started from
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
28
|
+
* - by default, on every plan, into the WORKING DRAFT of the version it
|
|
29
|
+
* started from (AGL-4116) — the draft the Besigner saves to as an author
|
|
30
|
+
* works and offers with Open draft, so the author reviews the change and
|
|
31
|
+
* publishes it there exactly as they would their own. A draft the author
|
|
32
|
+
* already has is built on, never replaced;
|
|
33
|
+
* - when the request asks for a new version and the plan keeps version
|
|
34
|
+
* history, as a NEW version beside the one it started from, which becomes
|
|
35
|
+
* the page's current version only when the page is not published; a
|
|
36
|
+
* published page's and a layout's current version never move. Asked for
|
|
37
|
+
* on a plan without version history, the change goes into the draft and
|
|
38
|
+
* the output says why.
|
|
35
39
|
*
|
|
36
40
|
* Pure: the inputs, the sentences a person reads, and the outline the model
|
|
37
41
|
* is shown. The runner is `jobs/ai-job-edit-step.ts`.
|
|
@@ -57,7 +61,8 @@ export const AI_EDIT_VERSION_ID_COPY = 'inputs.versionId must name a version of
|
|
|
57
61
|
return {
|
|
58
62
|
target,
|
|
59
63
|
targetKind: kind ? kind : null,
|
|
60
|
-
versionId: typeof versionId === 'string' && versionId ? versionId : null
|
|
64
|
+
versionId: typeof versionId === 'string' && versionId ? versionId : null,
|
|
65
|
+
newVersion: (inputs == null ? void 0 : inputs['newVersion']) === true
|
|
61
66
|
};
|
|
62
67
|
}
|
|
63
68
|
// ── What a person reads ───────────────────────────────────────────────────
|
|
@@ -84,11 +89,16 @@ const NOUNS = {
|
|
|
84
89
|
screen: 'page',
|
|
85
90
|
layout: 'layout'
|
|
86
91
|
};
|
|
92
|
+
/**
|
|
93
|
+
* Why a change asked for as a new version went into the draft: the plan
|
|
94
|
+
* keeps no version history. The versions route's own words for the plan.
|
|
95
|
+
*/ export const AI_EDIT_NO_VERSIONS_DRAFT_COPY = 'A new version needs version history, which requires a Pro plan, so the change went into the draft instead.';
|
|
87
96
|
/** The output's note: where the change is, that nothing visitors see moved, and what was left out. */ export function aiEditOutputNote(input) {
|
|
88
97
|
const noun = NOUNS[input.kind];
|
|
89
|
-
const where = input.placement === 'new-current' ? `Saved as a new version, now this ${noun}’s current one. The ${noun} is not published, so no visitor sees it until you publish it.` : input.placement === 'new-beside' ? `Saved as a new version beside the one it started from. Nothing visitors see has changed: open it to review it, and publish it when it is ready.` : `Saved on the version it started from, which no visitor sees: open it to review it.`;
|
|
98
|
+
const where = input.placement === 'draft' ? `Saved as this ${noun}’s draft. Nothing visitors see has changed: open the ${noun} in the Besigner and choose Open draft to review it, then publish it when it is ready.` : input.placement === 'new-current' ? `Saved as a new version, now this ${noun}’s current one. The ${noun} is not published, so no visitor sees it until you publish it.` : input.placement === 'new-beside' ? `Saved as a new version beside the one it started from. Nothing visitors see has changed: open it to review it, and publish it when it is ready.` : `Saved on the version it started from, which no visitor sees: open it to review it.`;
|
|
99
|
+
const versions = input.versionsUnavailable ? ` ${AI_EDIT_NO_VERSIONS_DRAFT_COPY}` : '';
|
|
90
100
|
const leftOut = input.leftOut.length ? ` Left out: ${input.leftOut.join('; ')}.` : '';
|
|
91
|
-
return `${where}${leftOut}`.slice(0, 1000);
|
|
101
|
+
return `${where}${versions}${leftOut}`.slice(0, 1000);
|
|
92
102
|
}
|
|
93
103
|
/** What the output says of search fields a published page keeps on the page itself. */ export const AI_EDIT_SEO_LEFT_OUT = 'the search title and description, which a published page uses as soon as they are saved — change them in Page properties';
|
|
94
104
|
/** What the output says of a part a job cannot save on its own. */ export const AI_EDIT_COMPONENT_LEFT_OUT = 'saving part of the page as a reusable component, which is done in the editor';
|
|
@@ -96,48 +106,52 @@ const NOUNS = {
|
|
|
96
106
|
const isRecord = (value)=>typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
97
107
|
/**
|
|
98
108
|
* The stored document as the outline the edit rung's protocol reads
|
|
99
|
-
* (`parseAssistEditContext` holds it to its limits): the
|
|
100
|
-
* every element
|
|
101
|
-
* chain the check walks is whole —
|
|
109
|
+
* (`parseAssistEditContext` with `whole` holds it to its limits): the
|
|
110
|
+
* document root, then every element in the order it reads on the page — each
|
|
111
|
+
* after its parent, so every ancestor chain the check walks is whole — with
|
|
112
|
+
* its settings and its styles, up to the whole-document cap.
|
|
102
113
|
*
|
|
103
114
|
* In the editor, an outline is the neighborhood of what the author selected.
|
|
104
|
-
* A job has no selection, so it is the
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
115
|
+
* A job has no selection and nobody to ask, so it is the WHOLE document: a
|
|
116
|
+
* request about a picture five levels down is about an element the model can
|
|
117
|
+
* see, and a restyle starts from the styles the element has. Each element
|
|
118
|
+
* keeps its real child count and the document its real size, so the prompt
|
|
119
|
+
* can say what a document past the cap leaves out. `null` without a root.
|
|
108
120
|
*/ export function aiEditJobOutline(nodes) {
|
|
109
121
|
const root = nodes[CANVAS_ROOT_ELEMENT_ID];
|
|
110
122
|
if (!isRecord(root)) return null;
|
|
111
123
|
const childrenOf = (node)=>Array.isArray(node['nodes']) ? node['nodes'].filter((id)=>typeof id === 'string' && isRecord(nodes[id])) : [];
|
|
112
|
-
const order = [
|
|
124
|
+
const order = [];
|
|
125
|
+
const seen = new Set([
|
|
126
|
+
CANVAS_ROOT_ELEMENT_ID
|
|
127
|
+
]);
|
|
128
|
+
// Depth first, children in their stored order: the page as it reads.
|
|
129
|
+
const stack = [
|
|
113
130
|
{
|
|
114
131
|
id: CANVAS_ROOT_ELEMENT_ID,
|
|
115
132
|
parentId: null,
|
|
116
133
|
index: 0
|
|
117
134
|
}
|
|
118
135
|
];
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
childrenOf(nodes[id]).forEach((child, index)=>{
|
|
127
|
-
if (seen.has(child)) return;
|
|
136
|
+
while(stack.length){
|
|
137
|
+
const at = stack.pop();
|
|
138
|
+
order.push(at);
|
|
139
|
+
const children = childrenOf(nodes[at.id]);
|
|
140
|
+
for(let index = children.length - 1; index >= 0; index -= 1){
|
|
141
|
+
const child = children[index];
|
|
142
|
+
if (seen.has(child)) continue;
|
|
128
143
|
seen.add(child);
|
|
129
|
-
|
|
130
|
-
order.push({
|
|
144
|
+
stack.push({
|
|
131
145
|
id: child,
|
|
132
|
-
parentId: id,
|
|
146
|
+
parentId: at.id,
|
|
133
147
|
index
|
|
134
148
|
});
|
|
135
|
-
}
|
|
149
|
+
}
|
|
136
150
|
}
|
|
137
151
|
return {
|
|
138
152
|
selectedId: null,
|
|
139
|
-
total,
|
|
140
|
-
nodes: order.slice(0,
|
|
153
|
+
total: order.length,
|
|
154
|
+
nodes: order.slice(0, ASSIST_EDIT_WHOLE_MAX_NODES).map(({ id, parentId, index })=>{
|
|
141
155
|
const node = nodes[id];
|
|
142
156
|
return _extends({
|
|
143
157
|
id,
|
|
@@ -149,6 +163,8 @@ const isRecord = (value)=>typeof value === 'object' && value !== null && !Array.
|
|
|
149
163
|
name: node['name']
|
|
150
164
|
} : {}, isRecord(node['props']) ? {
|
|
151
165
|
props: node['props']
|
|
166
|
+
} : {}, isRecord(node['sx']) ? {
|
|
167
|
+
sx: node['sx']
|
|
152
168
|
} : {});
|
|
153
169
|
})
|
|
154
170
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-edit-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 { CANVAS_ROOT_ELEMENT_ID } from '@aglyn/aglyn/foundation/constants/canvas'\nimport { ASSIST_EDIT_CONTEXT_MAX_NODES } from './assist-edit'\n\n/**\n * An `edit` job (AGL-3616): a change to a page or a layout the site already\n * has, asked for in words, made without the Besigner open.\n *\n * It speaks the Assist edit rung's own protocol — the same tool, the same\n * closed world of element ids, the same validators, the same canvas mutators\n * — against the stored document instead of an open canvas, and the change is\n * saved where no visitor sees it:\n *\n * - on a plan with version history, as a NEW version beside the one it\n * started from, which becomes the page's current version only when the\n * page is not published (no visitor reaches it); a published page's and a\n * layout's current version never move;\n * - on a plan without it, in place, and only on a version no visitor\n * reaches — an unpublished page's, or a version that is not the current\n * one. Anything else is refused, before anything is spent.\n *\n * Pure: the inputs, the sentences a person reads, and the outline the model\n * is shown. The runner is `jobs/ai-job-edit-step.ts`.\n */\n\n/** The documents an edit job changes: those that keep versions and render on a site. */\nexport const AI_EDIT_TARGET_KINDS = ['screen', 'layout'] as const\nexport type AiEditTargetKind = (typeof AI_EDIT_TARGET_KINDS)[number]\n\nexport interface AiEditJobInputs {\n /** The page's or layout's document id, on the job's site. */\n target: string\n /** Which collection the id is in; both are looked in, a page's first, when absent. */\n targetKind: AiEditTargetKind | null\n /** The version to start from; the document's current version when absent. */\n versionId: string | null\n}\n\nconst DOCUMENT_ID = /^[A-Za-z0-9_-]{1,64}$/\n\nexport const AI_EDIT_TARGET_REQUIRED_COPY = 'Name the page or layout to change.'\nexport const AI_EDIT_TARGET_KIND_COPY = 'inputs.targetKind must be screen or layout'\nexport const AI_EDIT_VERSION_ID_COPY = 'inputs.versionId must name a version of the page or layout'\n\n/** A job's inputs read, or the sentence that says what is wrong with them. */\nexport function parseAiEditJobInputs(\n inputs: Readonly<Record<string, unknown>> | null | undefined,\n): AiEditJobInputs | string {\n const target = inputs?.['target']\n if (typeof target !== 'string' || !DOCUMENT_ID.test(target)) return AI_EDIT_TARGET_REQUIRED_COPY\n const kind = inputs?.['targetKind']\n if (kind != null && kind !== '' && !(AI_EDIT_TARGET_KINDS as readonly unknown[]).includes(kind)) {\n return AI_EDIT_TARGET_KIND_COPY\n }\n const versionId = inputs?.['versionId']\n if (versionId != null && versionId !== '' && (typeof versionId !== 'string' || !DOCUMENT_ID.test(versionId))) {\n return AI_EDIT_VERSION_ID_COPY\n }\n return {\n target,\n targetKind: kind ? (kind as AiEditTargetKind) : null,\n versionId: typeof versionId === 'string' && versionId ? versionId : null,\n }\n}\n\n// ── What a person reads ───────────────────────────────────────────────────\n\nexport const AI_EDIT_NO_SITE_COPY = 'Open the site the change is for before starting the job'\nexport const AI_EDIT_GONE_COPY = 'The page or layout to change is not on the site.'\nexport const AI_EDIT_VERSION_GONE_COPY = 'The version to start from is no longer on the page or layout.'\nexport const AI_EDIT_EMAIL_COPY = 'That is an email design, not a page — change it from Emails → Templates.'\nexport const AI_EDIT_NO_NODES_COPY = 'That version has nothing on it to change.'\n\n/**\n * A change that would land on a version visitors see, on a plan without\n * version history: the versions route's own sentence, after what it means\n * here.\n */\nexport const AI_EDIT_NEEDS_VERSIONING_COPY =\n 'Visitors see that version, so the change has to go into a new version beside it. Version history requires a Pro plan — see Billing to upgrade.'\n\n/** The model answered and no change survived its checks, even after its re-ask. */\nexport const AI_EDIT_NOTHING_COPY =\n 'No change could be made from that request. Say which part of the page to change, the way it reads on the page, and ask again.'\n\n/** The stored document moved under the change between the read and the write. */\nexport const AI_EDIT_STALE_COPY =\n 'The page changed while this edit was being written, so nothing was saved. Ask again.'\n\n/** The canvas's own guards refused part of the change. */\nexport const AI_EDIT_REFUSED_COPY = 'The editor refused part of this change, so none of it was saved.'\n\n/** What a version an edit job makes is called, from the change's one-line summary. */\nexport const AI_EDIT_VERSION_PREFIX = 'AI edit'\n\n/** The longest label a version takes, as the versions route cuts one. */\nexport const AI_EDIT_VERSION_NAME_MAX_CHARS = 200\n\nexport function aiEditVersionName(summary: string): string {\n const said = String(summary ?? '').replace(/\\s+/g, ' ').trim()\n return (said ? `${AI_EDIT_VERSION_PREFIX}: ${said}` : AI_EDIT_VERSION_PREFIX).slice(0, AI_EDIT_VERSION_NAME_MAX_CHARS)\n}\n\n/** Where the change landed, which decides what the output's note says. */\nexport type AiEditPlacement =\n /** A new version, now the unpublished page's current one. */\n | 'new-current'\n /** A new version beside the one it started from, which is unchanged. */\n | 'new-beside'\n /** The version it started from, changed in place: one no visitor reaches. */\n | 'in-place'\n\nconst NOUNS: Readonly<Record<AiEditTargetKind, string>> = { screen: 'page', layout: 'layout' }\n\n/** The output's note: where the change is, that nothing visitors see moved, and what was left out. */\nexport function aiEditOutputNote(input: {\n kind: AiEditTargetKind\n placement: AiEditPlacement\n leftOut: readonly string[]\n}): string {\n const noun = NOUNS[input.kind]\n const where =\n input.placement === 'new-current'\n ? `Saved as a new version, now this ${noun}’s current one. The ${noun} is not published, so no visitor sees it until you publish it.`\n : input.placement === 'new-beside'\n ? `Saved as a new version beside the one it started from. Nothing visitors see has changed: open it to review it, and publish it when it is ready.`\n : `Saved on the version it started from, which no visitor sees: open it to review it.`\n const leftOut = input.leftOut.length ? ` Left out: ${input.leftOut.join('; ')}.` : ''\n return `${where}${leftOut}`.slice(0, 1_000)\n}\n\n/** What the output says of search fields a published page keeps on the page itself. */\nexport const AI_EDIT_SEO_LEFT_OUT =\n 'the search title and description, which a published page uses as soon as they are saved — change them in Page properties'\n\n/** What the output says of a part a job cannot save on its own. */\nexport const AI_EDIT_COMPONENT_LEFT_OUT = 'saving part of the page as a reusable component, which is done in the editor'\n\n// ── The outline the model is shown ────────────────────────────────────────\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value)\n\n/**\n * The stored document as the outline the edit rung's protocol reads\n * (`parseAssistEditContext` holds it to its limits): the document root, then\n * every element level by level — each after its parent, so every ancestor\n * chain the check walks is whole — until the rung's cap.\n *\n * In the editor, an outline is the neighborhood of what the author selected.\n * A job has no selection, so it is the TOP of the document, read breadth\n * first: every section, and as deep inside each as the cap reaches. Each\n * element keeps its real child count and the document its real size, so the\n * prompt can say how much the outline leaves out. `null` without a root.\n */\nexport function aiEditJobOutline(nodes: Readonly<Record<string, unknown>>): {\n selectedId: null\n total: number\n nodes: Array<Record<string, unknown>>\n} | null {\n const root = nodes[CANVAS_ROOT_ELEMENT_ID]\n if (!isRecord(root)) return null\n const childrenOf = (node: Record<string, unknown>): string[] =>\n Array.isArray(node['nodes']) ? node['nodes'].filter((id): id is string => typeof id === 'string' && isRecord(nodes[id])) : []\n const order: Array<{ id: string; parentId: string | null; index: number }> = [\n { id: CANVAS_ROOT_ELEMENT_ID, parentId: null, index: 0 },\n ]\n const seen = new Set<string>([CANVAS_ROOT_ELEMENT_ID])\n // Every element reachable through child lists, for the document's size.\n let total = 1\n for (let at = 0; at < order.length; at += 1) {\n const { id } = order[at]\n childrenOf(nodes[id] as Record<string, unknown>).forEach((child, index) => {\n if (seen.has(child)) return\n seen.add(child)\n total += 1\n order.push({ id: child, parentId: id, index })\n })\n }\n return {\n selectedId: null,\n total,\n nodes: order.slice(0, ASSIST_EDIT_CONTEXT_MAX_NODES).map(({ id, parentId, index }) => {\n const node = nodes[id] as Record<string, unknown>\n return {\n id,\n componentId: node['componentId'],\n parentId,\n index,\n childCount: childrenOf(node).length,\n ...(typeof node['name'] === 'string' && node['name'] ? { name: node['name'] } : {}),\n ...(isRecord(node['props']) ? { props: node['props'] } : {}),\n }\n }),\n }\n}\n"],"names":["CANVAS_ROOT_ELEMENT_ID","ASSIST_EDIT_CONTEXT_MAX_NODES","AI_EDIT_TARGET_KINDS","DOCUMENT_ID","AI_EDIT_TARGET_REQUIRED_COPY","AI_EDIT_TARGET_KIND_COPY","AI_EDIT_VERSION_ID_COPY","parseAiEditJobInputs","inputs","target","test","kind","includes","versionId","targetKind","AI_EDIT_NO_SITE_COPY","AI_EDIT_GONE_COPY","AI_EDIT_VERSION_GONE_COPY","AI_EDIT_EMAIL_COPY","AI_EDIT_NO_NODES_COPY","AI_EDIT_NEEDS_VERSIONING_COPY","AI_EDIT_NOTHING_COPY","AI_EDIT_STALE_COPY","AI_EDIT_REFUSED_COPY","AI_EDIT_VERSION_PREFIX","AI_EDIT_VERSION_NAME_MAX_CHARS","aiEditVersionName","summary","said","String","replace","trim","slice","NOUNS","screen","layout","aiEditOutputNote","input","noun","where","placement","leftOut","length","join","AI_EDIT_SEO_LEFT_OUT","AI_EDIT_COMPONENT_LEFT_OUT","isRecord","value","Array","isArray","aiEditJobOutline","nodes","root","childrenOf","node","filter","id","order","parentId","index","seen","Set","total","at","forEach","child","has","add","push","selectedId","map","componentId","childCount","name","props"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-edit-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 { CANVAS_ROOT_ELEMENT_ID } from '@aglyn/aglyn/foundation/constants/canvas'\nimport { ASSIST_EDIT_WHOLE_MAX_NODES } from './assist-edit'\n\n/**\n * An `edit` job (AGL-3616): a change to a page or a layout the site already\n * has, asked for in words, made without the Besigner open.\n *\n * It speaks the Assist edit rung's own protocol — the same tool, the same\n * closed world of element ids, the same validators, the same canvas mutators\n * — against the stored document instead of an open canvas, and the change is\n * saved where no visitor sees it:\n *\n * - by default, on every plan, into the WORKING DRAFT of the version it\n * started from (AGL-4116) — the draft the Besigner saves to as an author\n * works and offers with Open draft, so the author reviews the change and\n * publishes it there exactly as they would their own. A draft the author\n * already has is built on, never replaced;\n * - when the request asks for a new version and the plan keeps version\n * history, as a NEW version beside the one it started from, which becomes\n * the page's current version only when the page is not published; a\n * published page's and a layout's current version never move. Asked for\n * on a plan without version history, the change goes into the draft and\n * the output says why.\n *\n * Pure: the inputs, the sentences a person reads, and the outline the model\n * is shown. The runner is `jobs/ai-job-edit-step.ts`.\n */\n\n/** The documents an edit job changes: those that keep versions and render on a site. */\nexport const AI_EDIT_TARGET_KINDS = ['screen', 'layout'] as const\nexport type AiEditTargetKind = (typeof AI_EDIT_TARGET_KINDS)[number]\n\nexport interface AiEditJobInputs {\n /** The page's or layout's document id, on the job's site. */\n target: string\n /** Which collection the id is in; both are looked in, a page's first, when absent. */\n targetKind: AiEditTargetKind | null\n /** The version to start from; the document's current version when absent. */\n versionId: string | null\n /** The request asked for a new version; otherwise the change goes into the draft. */\n newVersion: boolean\n}\n\nconst DOCUMENT_ID = /^[A-Za-z0-9_-]{1,64}$/\n\nexport const AI_EDIT_TARGET_REQUIRED_COPY = 'Name the page or layout to change.'\nexport const AI_EDIT_TARGET_KIND_COPY = 'inputs.targetKind must be screen or layout'\nexport const AI_EDIT_VERSION_ID_COPY = 'inputs.versionId must name a version of the page or layout'\n\n/** A job's inputs read, or the sentence that says what is wrong with them. */\nexport function parseAiEditJobInputs(\n inputs: Readonly<Record<string, unknown>> | null | undefined,\n): AiEditJobInputs | string {\n const target = inputs?.['target']\n if (typeof target !== 'string' || !DOCUMENT_ID.test(target)) return AI_EDIT_TARGET_REQUIRED_COPY\n const kind = inputs?.['targetKind']\n if (kind != null && kind !== '' && !(AI_EDIT_TARGET_KINDS as readonly unknown[]).includes(kind)) {\n return AI_EDIT_TARGET_KIND_COPY\n }\n const versionId = inputs?.['versionId']\n if (versionId != null && versionId !== '' && (typeof versionId !== 'string' || !DOCUMENT_ID.test(versionId))) {\n return AI_EDIT_VERSION_ID_COPY\n }\n return {\n target,\n targetKind: kind ? (kind as AiEditTargetKind) : null,\n versionId: typeof versionId === 'string' && versionId ? versionId : null,\n newVersion: inputs?.['newVersion'] === true,\n }\n}\n\n// ── What a person reads ───────────────────────────────────────────────────\n\nexport const AI_EDIT_NO_SITE_COPY = 'Open the site the change is for before starting the job'\nexport const AI_EDIT_GONE_COPY = 'The page or layout to change is not on the site.'\nexport const AI_EDIT_VERSION_GONE_COPY = 'The version to start from is no longer on the page or layout.'\nexport const AI_EDIT_EMAIL_COPY = 'That is an email design, not a page — change it from Emails → Templates.'\nexport const AI_EDIT_NO_NODES_COPY = 'That version has nothing on it to change.'\n\n/**\n * A change that would land on a version visitors see, on a plan without\n * version history: the versions route's own sentence, after what it means\n * here.\n */\nexport const AI_EDIT_NEEDS_VERSIONING_COPY =\n 'Visitors see that version, so the change has to go into a new version beside it. Version history requires a Pro plan — see Billing to upgrade.'\n\n/** The model answered and no change survived its checks, even after its re-ask. */\nexport const AI_EDIT_NOTHING_COPY =\n 'No change could be made from that request. Say which part of the page to change, the way it reads on the page, and ask again.'\n\n/** The stored document moved under the change between the read and the write. */\nexport const AI_EDIT_STALE_COPY =\n 'The page changed while this edit was being written, so nothing was saved. Ask again.'\n\n/** The canvas's own guards refused part of the change. */\nexport const AI_EDIT_REFUSED_COPY = 'The editor refused part of this change, so none of it was saved.'\n\n/** What a version an edit job makes is called, from the change's one-line summary. */\nexport const AI_EDIT_VERSION_PREFIX = 'AI edit'\n\n/** The longest label a version takes, as the versions route cuts one. */\nexport const AI_EDIT_VERSION_NAME_MAX_CHARS = 200\n\nexport function aiEditVersionName(summary: string): string {\n const said = String(summary ?? '').replace(/\\s+/g, ' ').trim()\n return (said ? `${AI_EDIT_VERSION_PREFIX}: ${said}` : AI_EDIT_VERSION_PREFIX).slice(0, AI_EDIT_VERSION_NAME_MAX_CHARS)\n}\n\n/** Where the change landed, which decides what the output's note says. */\nexport type AiEditPlacement =\n /** A new version, now the unpublished page's current one. */\n | 'new-current'\n /** A new version beside the one it started from, which is unchanged. */\n | 'new-beside'\n /** The version it started from, changed in place: one no visitor reaches. Written before AGL-4116. */\n | 'in-place'\n /** The working draft of the version it started from, which the Besigner offers with Open draft. */\n | 'draft'\n\nconst NOUNS: Readonly<Record<AiEditTargetKind, string>> = { screen: 'page', layout: 'layout' }\n\n/**\n * Why a change asked for as a new version went into the draft: the plan\n * keeps no version history. The versions route's own words for the plan.\n */\nexport const AI_EDIT_NO_VERSIONS_DRAFT_COPY =\n 'A new version needs version history, which requires a Pro plan, so the change went into the draft instead.'\n\n/** The output's note: where the change is, that nothing visitors see moved, and what was left out. */\nexport function aiEditOutputNote(input: {\n kind: AiEditTargetKind\n placement: AiEditPlacement\n leftOut: readonly string[]\n /** A new version was asked for on a plan without version history. */\n versionsUnavailable?: boolean\n}): string {\n const noun = NOUNS[input.kind]\n const where =\n input.placement === 'draft'\n ? `Saved as this ${noun}’s draft. Nothing visitors see has changed: open the ${noun} in the Besigner and choose Open draft to review it, then publish it when it is ready.`\n : input.placement === 'new-current'\n ? `Saved as a new version, now this ${noun}’s current one. The ${noun} is not published, so no visitor sees it until you publish it.`\n : input.placement === 'new-beside'\n ? `Saved as a new version beside the one it started from. Nothing visitors see has changed: open it to review it, and publish it when it is ready.`\n : `Saved on the version it started from, which no visitor sees: open it to review it.`\n const versions = input.versionsUnavailable ? ` ${AI_EDIT_NO_VERSIONS_DRAFT_COPY}` : ''\n const leftOut = input.leftOut.length ? ` Left out: ${input.leftOut.join('; ')}.` : ''\n return `${where}${versions}${leftOut}`.slice(0, 1_000)\n}\n\n/** What the output says of search fields a published page keeps on the page itself. */\nexport const AI_EDIT_SEO_LEFT_OUT =\n 'the search title and description, which a published page uses as soon as they are saved — change them in Page properties'\n\n/** What the output says of a part a job cannot save on its own. */\nexport const AI_EDIT_COMPONENT_LEFT_OUT = 'saving part of the page as a reusable component, which is done in the editor'\n\n// ── The outline the model is shown ────────────────────────────────────────\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value)\n\n/**\n * The stored document as the outline the edit rung's protocol reads\n * (`parseAssistEditContext` with `whole` holds it to its limits): the\n * document root, then every element in the order it reads on the page — each\n * after its parent, so every ancestor chain the check walks is whole — with\n * its settings and its styles, up to the whole-document cap.\n *\n * In the editor, an outline is the neighborhood of what the author selected.\n * A job has no selection and nobody to ask, so it is the WHOLE document: a\n * request about a picture five levels down is about an element the model can\n * see, and a restyle starts from the styles the element has. Each element\n * keeps its real child count and the document its real size, so the prompt\n * can say what a document past the cap leaves out. `null` without a root.\n */\nexport function aiEditJobOutline(nodes: Readonly<Record<string, unknown>>): {\n selectedId: null\n total: number\n nodes: Array<Record<string, unknown>>\n} | null {\n const root = nodes[CANVAS_ROOT_ELEMENT_ID]\n if (!isRecord(root)) return null\n const childrenOf = (node: Record<string, unknown>): string[] =>\n Array.isArray(node['nodes']) ? node['nodes'].filter((id): id is string => typeof id === 'string' && isRecord(nodes[id])) : []\n const order: Array<{ id: string; parentId: string | null; index: number }> = []\n const seen = new Set<string>([CANVAS_ROOT_ELEMENT_ID])\n // Depth first, children in their stored order: the page as it reads.\n const stack: Array<{ id: string; parentId: string | null; index: number }> = [\n { id: CANVAS_ROOT_ELEMENT_ID, parentId: null, index: 0 },\n ]\n while (stack.length) {\n const at = stack.pop() as (typeof stack)[number]\n order.push(at)\n const children = childrenOf(nodes[at.id] as Record<string, unknown>)\n for (let index = children.length - 1; index >= 0; index -= 1) {\n const child = children[index]\n if (seen.has(child)) continue\n seen.add(child)\n stack.push({ id: child, parentId: at.id, index })\n }\n }\n return {\n selectedId: null,\n total: order.length,\n nodes: order.slice(0, ASSIST_EDIT_WHOLE_MAX_NODES).map(({ id, parentId, index }) => {\n const node = nodes[id] as Record<string, unknown>\n return {\n id,\n componentId: node['componentId'],\n parentId,\n index,\n childCount: childrenOf(node).length,\n ...(typeof node['name'] === 'string' && node['name'] ? { name: node['name'] } : {}),\n ...(isRecord(node['props']) ? { props: node['props'] } : {}),\n ...(isRecord(node['sx']) ? { sx: node['sx'] } : {}),\n }\n }),\n }\n}\n"],"names":["CANVAS_ROOT_ELEMENT_ID","ASSIST_EDIT_WHOLE_MAX_NODES","AI_EDIT_TARGET_KINDS","DOCUMENT_ID","AI_EDIT_TARGET_REQUIRED_COPY","AI_EDIT_TARGET_KIND_COPY","AI_EDIT_VERSION_ID_COPY","parseAiEditJobInputs","inputs","target","test","kind","includes","versionId","targetKind","newVersion","AI_EDIT_NO_SITE_COPY","AI_EDIT_GONE_COPY","AI_EDIT_VERSION_GONE_COPY","AI_EDIT_EMAIL_COPY","AI_EDIT_NO_NODES_COPY","AI_EDIT_NEEDS_VERSIONING_COPY","AI_EDIT_NOTHING_COPY","AI_EDIT_STALE_COPY","AI_EDIT_REFUSED_COPY","AI_EDIT_VERSION_PREFIX","AI_EDIT_VERSION_NAME_MAX_CHARS","aiEditVersionName","summary","said","String","replace","trim","slice","NOUNS","screen","layout","AI_EDIT_NO_VERSIONS_DRAFT_COPY","aiEditOutputNote","input","noun","where","placement","versions","versionsUnavailable","leftOut","length","join","AI_EDIT_SEO_LEFT_OUT","AI_EDIT_COMPONENT_LEFT_OUT","isRecord","value","Array","isArray","aiEditJobOutline","nodes","root","childrenOf","node","filter","id","order","seen","Set","stack","parentId","index","at","pop","push","children","child","has","add","selectedId","total","map","componentId","childCount","name","props","sx"],"mappings":"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"}
|
|
@@ -138,20 +138,16 @@ export declare function aiJobKindNoun(kind: AiJobKind): string;
|
|
|
138
138
|
* the site as it was, and race for what its plan holds one of.
|
|
139
139
|
*/
|
|
140
140
|
export declare const AI_PAGE_BUILDING_JOB_KINDS: readonly AiJobKind[];
|
|
141
|
-
/**
|
|
142
|
-
* How long a plan waiting for its confirmation still counts as a build on its
|
|
143
|
-
* way (AGL-3765): one the person is reading now, not one left unconfirmed
|
|
144
|
-
* days ago.
|
|
145
|
-
*/
|
|
146
|
-
export declare const AI_SITE_BUILD_PLAN_FRESH_MS: number;
|
|
147
141
|
/** What a door that starts a page says while another is being built on the site (AGL-3765). */
|
|
148
142
|
export declare const AI_SITE_BUILD_IN_FLIGHT_COPY = "A page is already being built. Wait for it, or start another anyway.";
|
|
149
143
|
/**
|
|
150
144
|
* Another job building pages on this site (AGL-3765): queued, planning or
|
|
151
|
-
* building
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
145
|
+
* building. `null` when there is none. A door warns with it and still starts
|
|
146
|
+
* the job: two builds can share a site, they just cannot both make what its
|
|
147
|
+
* plan holds only one of. A plan waiting for its confirmation is not one
|
|
148
|
+
* (AGL-4116): nothing is being built until it is confirmed, and saying so
|
|
149
|
+
* over an unconfirmed plan told a person a page was being built that they
|
|
150
|
+
* had never asked for.
|
|
155
151
|
*/
|
|
156
152
|
export declare function aiSiteBuildInFlight<T extends AiJobPhaseSource & {
|
|
157
153
|
id: string;
|
|
@@ -160,7 +156,6 @@ export declare function aiSiteBuildInFlight<T extends AiJobPhaseSource & {
|
|
|
160
156
|
}>(jobs: readonly T[], input: {
|
|
161
157
|
hostId: string | null | undefined;
|
|
162
158
|
exceptJobId?: string | null;
|
|
163
|
-
now?: number;
|
|
164
159
|
}): T | null;
|
|
165
160
|
/** The fields of a job {@link aiJobSettled} and {@link aiJobProblem} read. */
|
|
166
161
|
export interface AiJobSettleSource {
|
|
@@ -181,30 +181,23 @@ export function aiJobsActivity(jobs) {
|
|
|
181
181
|
'build',
|
|
182
182
|
'import'
|
|
183
183
|
];
|
|
184
|
-
/**
|
|
185
|
-
* How long a plan waiting for its confirmation still counts as a build on its
|
|
186
|
-
* way (AGL-3765): one the person is reading now, not one left unconfirmed
|
|
187
|
-
* days ago.
|
|
188
|
-
*/ export const AI_SITE_BUILD_PLAN_FRESH_MS = 15 * 60000;
|
|
189
184
|
/** What a door that starts a page says while another is being built on the site (AGL-3765). */ export const AI_SITE_BUILD_IN_FLIGHT_COPY = 'A page is already being built. Wait for it, or start another anyway.';
|
|
190
185
|
/**
|
|
191
186
|
* Another job building pages on this site (AGL-3765): queued, planning or
|
|
192
|
-
* building
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
187
|
+
* building. `null` when there is none. A door warns with it and still starts
|
|
188
|
+
* the job: two builds can share a site, they just cannot both make what its
|
|
189
|
+
* plan holds only one of. A plan waiting for its confirmation is not one
|
|
190
|
+
* (AGL-4116): nothing is being built until it is confirmed, and saying so
|
|
191
|
+
* over an unconfirmed plan told a person a page was being built that they
|
|
192
|
+
* had never asked for.
|
|
196
193
|
*/ export function aiSiteBuildInFlight(jobs, input) {
|
|
197
|
-
var
|
|
194
|
+
var _jobs_find;
|
|
198
195
|
if (!input.hostId) return null;
|
|
199
|
-
const now = (_input_now = input.now) != null ? _input_now : Date.now();
|
|
200
196
|
return (_jobs_find = jobs.find((job)=>{
|
|
201
197
|
if (job.id === input.exceptJobId || job.hostId !== input.hostId) return false;
|
|
202
198
|
if (!AI_PAGE_BUILDING_JOB_KINDS.includes(job.kind)) return false;
|
|
203
199
|
const phase = aiJobPhase(job);
|
|
204
|
-
|
|
205
|
-
if (phase !== 'plan-ready') return false;
|
|
206
|
-
const at = typeof job.updatedAt === 'string' ? Date.parse(job.updatedAt) : NaN;
|
|
207
|
-
return Number.isFinite(at) && now - at < AI_SITE_BUILD_PLAN_FRESH_MS;
|
|
200
|
+
return phase === 'queued' || phase === 'planning' || phase === 'building';
|
|
208
201
|
})) != null ? _jobs_find : null;
|
|
209
202
|
}
|
|
210
203
|
/** Whether a job has stopped moving: finished, or waiting on something only a person can change. */ export function aiJobSettled(job) {
|