@aglyn/plugins-ai 1.0.0-beta.235 → 1.0.0-beta.237

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/package.json +17 -17
  2. package/src/lib/components/ai-credits-prompt.component.d.ts +37 -0
  3. package/src/lib/components/ai-credits-prompt.component.js +67 -0
  4. package/src/lib/components/ai-credits-prompt.component.js.map +1 -0
  5. package/src/lib/components/ai-job-plan.component.d.ts +5 -4
  6. package/src/lib/components/ai-job-plan.component.js +37 -12
  7. package/src/lib/components/ai-job-plan.component.js.map +1 -1
  8. package/src/lib/components/ai-job-requests.d.ts +10 -0
  9. package/src/lib/components/ai-job-requests.js +12 -4
  10. package/src/lib/components/ai-job-requests.js.map +1 -1
  11. package/src/lib/components/ai-site-build-page.component.js +26 -7
  12. package/src/lib/components/ai-site-build-page.component.js.map +1 -1
  13. package/src/lib/components/ai-site-start-card.component.js +68 -22
  14. package/src/lib/components/ai-site-start-card.component.js.map +1 -1
  15. package/src/lib/components/assist-build-card.component.d.ts +3 -1
  16. package/src/lib/components/assist-build-card.component.js +58 -7
  17. package/src/lib/components/assist-build-card.component.js.map +1 -1
  18. package/src/lib/components/assist-jobs-drawer.component.js +54 -6
  19. package/src/lib/components/assist-jobs-drawer.component.js.map +1 -1
  20. package/src/lib/components/assist-panel.component.js +2 -1
  21. package/src/lib/components/assist-panel.component.js.map +1 -1
  22. package/src/lib/jobs/ai-job-admission.d.ts +22 -2
  23. package/src/lib/jobs/ai-job-admission.js.map +1 -1
  24. package/src/lib/jobs/ai-job-build-step.d.ts +24 -11
  25. package/src/lib/jobs/ai-job-build-step.js +50 -34
  26. package/src/lib/jobs/ai-job-build-step.js.map +1 -1
  27. package/src/lib/jobs/ai-job-layout-language.js +1 -1
  28. package/src/lib/jobs/ai-job-layout-language.js.map +1 -1
  29. package/src/lib/jobs/ai-job-page-language.js +4 -0
  30. package/src/lib/jobs/ai-job-page-language.js.map +1 -1
  31. package/src/lib/jobs/ai-job-page-step.d.ts +13 -0
  32. package/src/lib/jobs/ai-job-page-step.js +34 -3
  33. package/src/lib/jobs/ai-job-page-step.js.map +1 -1
  34. package/src/lib/jobs/ai-job-plan-step.d.ts +23 -0
  35. package/src/lib/jobs/ai-job-plan-step.js +75 -15
  36. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  37. package/src/lib/jobs/ai-job-site-content.d.ts +34 -3
  38. package/src/lib/jobs/ai-job-site-content.js +132 -32
  39. package/src/lib/jobs/ai-job-site-content.js.map +1 -1
  40. package/src/lib/jobs/ai-job-site-step.js +45 -15
  41. package/src/lib/jobs/ai-job-site-step.js.map +1 -1
  42. package/src/lib/jobs/ai-job-text-step.d.ts +8 -0
  43. package/src/lib/jobs/ai-job-text-step.js.map +1 -1
  44. package/src/lib/jobs/ai-jobs.d.ts +18 -1
  45. package/src/lib/jobs/ai-jobs.js +29 -13
  46. package/src/lib/jobs/ai-jobs.js.map +1 -1
  47. package/src/lib/jobs/ai-layout-stock-photos.d.ts +105 -18
  48. package/src/lib/jobs/ai-layout-stock-photos.js +356 -114
  49. package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
  50. package/src/lib/layout-language/ai-layout-compiler.d.ts +27 -0
  51. package/src/lib/layout-language/ai-layout-compiler.js +78 -11
  52. package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
  53. package/src/lib/layout-language/ai-layout-gaps.js +3 -1
  54. package/src/lib/layout-language/ai-layout-gaps.js.map +1 -1
  55. package/src/lib/layout-language/ai-layout-language.js +1 -1
  56. package/src/lib/layout-language/ai-layout-language.js.map +1 -1
  57. package/src/lib/layout-language/ai-layout-listings.d.ts +37 -1
  58. package/src/lib/layout-language/ai-layout-listings.js +66 -11
  59. package/src/lib/layout-language/ai-layout-listings.js.map +1 -1
  60. package/src/lib/layout-language/ai-layout-pictures.d.ts +2 -1
  61. package/src/lib/layout-language/ai-layout-pictures.js +5 -2
  62. package/src/lib/layout-language/ai-layout-pictures.js.map +1 -1
  63. package/src/lib/model/ai-build-job.d.ts +31 -0
  64. package/src/lib/model/ai-build-job.js +86 -1
  65. package/src/lib/model/ai-build-job.js.map +1 -1
  66. package/src/lib/model/ai-build-progress.d.ts +8 -0
  67. package/src/lib/model/ai-build-progress.js +33 -0
  68. package/src/lib/model/ai-build-progress.js.map +1 -1
  69. package/src/lib/model/ai-credit-estimate.d.ts +182 -0
  70. package/src/lib/model/ai-credit-estimate.js +190 -0
  71. package/src/lib/model/ai-credit-estimate.js.map +1 -0
  72. package/src/lib/model/ai-jobs.types.d.ts +15 -0
  73. package/src/lib/model/ai-jobs.types.js.map +1 -1
  74. package/src/lib/model/ai-site-job.d.ts +59 -22
  75. package/src/lib/model/ai-site-job.js +104 -23
  76. package/src/lib/model/ai-site-job.js.map +1 -1
  77. package/src/lib/model/ai-site-kinds.js +3 -3
  78. package/src/lib/model/ai-site-kinds.js.map +1 -1
  79. package/src/lib/runtime/ai-doctrine-validators.js +10 -1
  80. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  81. package/src/lib/runtime/ai-node-tree.d.ts +11 -0
  82. package/src/lib/runtime/ai-node-tree.js +26 -0
  83. package/src/lib/runtime/ai-node-tree.js.map +1 -1
  84. package/src/lib/runtime/ai-palette.generated.js +539 -7
  85. package/src/lib/runtime/ai-palette.generated.js.map +1 -1
  86. package/src/lib/server/ai-jobs-resume.js +93 -35
  87. package/src/lib/server/ai-jobs-resume.js.map +1 -1
  88. package/src/lib/server/ai-jobs-route.js +45 -4
  89. package/src/lib/server/ai-jobs-route.js.map +1 -1
  90. package/src/lib/server/assist-chat.js +4 -1
  91. package/src/lib/server/assist-chat.js.map +1 -1
  92. package/src/lib/server/assist-edit.js +3 -1
  93. package/src/lib/server/assist-edit.js.map +1 -1
  94. package/src/lib/server/assist-music.d.ts +36 -0
  95. package/src/lib/server/assist-music.js +36 -0
  96. package/src/lib/server/assist-music.js.map +1 -0
  97. package/src/lib/server.js +6 -0
  98. package/src/lib/server.js.map +1 -1
@@ -15,6 +15,7 @@
15
15
  * limitations under the License.
16
16
  */
17
17
  import { type AiJobItemLedger, type AiJobSummary } from './ai-jobs.types';
18
+ import { type AiCreditRange } from './ai-credit-estimate';
18
19
  /**
19
20
  * What a `build` job shows of each item (AGL-3616), wherever it is shown —
20
21
  * the AI jobs drawer, the job's own page, the chat card: the item, where it
@@ -65,6 +66,13 @@ export declare function aiBuildItemRows(job: Pick<AiJobSummary, 'items'> & Parti
65
66
  export declare function aiBuildOutcomeLine(job: Pick<AiJobSummary, 'items' | 'status'>): string | null;
66
67
  /** Whether a finished build — or a site that built part of itself — has failed items to try again. */
67
68
  export declare function aiBuildCanRetry(job: Pick<AiJobSummary, 'kind' | 'status' | 'items'>): boolean;
69
+ /**
70
+ * What Try again is likely to cost (AGL-3722), quoted beside its button: a
71
+ * build's retried items by `aiBuildCreditRange`; a site's retried rows each
72
+ * at what its kind measured — a page at the nominal sections — held at the
73
+ * reserve a row is retried at. `null` when nothing would run.
74
+ */
75
+ export declare function aiBuildRetryCreditRange(job: Pick<AiJobSummary, 'kind' | 'items' | 'plan'>): AiCreditRange | null;
68
76
  /**
69
77
  * What a guided start that built part of its site says (AGL-3616), in the
70
78
  * words every surface uses: "Built 1 of 2 pages. Home couldn’t be built —
@@ -16,6 +16,9 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
16
16
  * limitations under the License.
17
17
  */ import { aiCustomerSafeItem } from "./ai-job-failure-copy.js";
18
18
  import { AI_JOB_PLAN_STEP } from "./ai-jobs.types.js";
19
+ import { aiBuildCreditRange, aiBuildRetryLedger, aiBuildUnits, aiLedgerUnits } from "./ai-build-job.js";
20
+ import { AI_CREDIT_RANGE_ZERO, aiCreditRangeAdd, aiCreditRangeOf, aiCreditRangeOrdered } from "./ai-credit-estimate.js";
21
+ import { AI_SITE_NOMINAL_SECTIONS, AI_SITE_PASS_CREDITS, aiCreationMeasuredCredits, aiScreenCreditRange } from "./ai-site-job.js";
19
22
  const OP_NOUNS = {
20
23
  page: 'Page',
21
24
  layout: 'Layout',
@@ -143,6 +146,36 @@ const STATES = {
143
146
  return row.status === 'failed' || row.status === 'skipped' && !((_row_degradedBy = row.degradedBy) == null ? void 0 : _row_degradedBy.length);
144
147
  });
145
148
  }
149
+ /**
150
+ * What Try again is likely to cost (AGL-3722), quoted beside its button: a
151
+ * build's retried items by `aiBuildCreditRange`; a site's retried rows each
152
+ * at what its kind measured — a page at the nominal sections — held at the
153
+ * reserve a row is retried at. `null` when nothing would run.
154
+ */ export function aiBuildRetryCreditRange(job) {
155
+ var _job_items;
156
+ const items = (_job_items = job.items) != null ? _job_items : [];
157
+ if (!items.length) return null;
158
+ if (job.kind === 'build') {
159
+ if (!job.plan) return null;
160
+ const plan = job.plan;
161
+ const { retried } = aiBuildRetryLedger(items, aiBuildUnits(plan));
162
+ return retried.length ? aiBuildCreditRange(plan, {
163
+ slots: new Set(retried)
164
+ }) : null;
165
+ }
166
+ const { retried } = aiBuildRetryLedger(items, aiLedgerUnits(items));
167
+ if (!retried.length) return null;
168
+ const ops = new Map(items.map((row)=>[
169
+ row.slot,
170
+ row.op
171
+ ]));
172
+ return aiCreditRangeOrdered(retried.reduce((total, slot)=>{
173
+ var _ops_get;
174
+ const op = (_ops_get = ops.get(slot)) != null ? _ops_get : '';
175
+ const row = op === 'page' ? aiScreenCreditRange(AI_SITE_NOMINAL_SECTIONS) : aiCreditRangeOf(aiCreationMeasuredCredits(op), AI_SITE_PASS_CREDITS);
176
+ return aiCreditRangeAdd(total, row);
177
+ }, AI_CREDIT_RANGE_ZERO));
178
+ }
146
179
  /**
147
180
  * What a guided start that built part of its site says (AGL-3616), in the
148
181
  * words every surface uses: "Built 1 of 2 pages. Home couldn’t be built —
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-build-progress.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 { aiCustomerSafeItem } from './ai-job-failure-copy'\nimport { AI_JOB_PLAN_STEP, type AiJobItemLedger, type AiJobSummary } from './ai-jobs.types'\n\n/**\n * What a `build` job shows of each item (AGL-3616), wherever it is shown —\n * the AI jobs drawer, the job's own page, the chat card: the item, where it\n * stands, what it cost, and what became of its credits when it failed.\n *\n * Pure: no React, no request.\n */\n\n/** Where one item stands, as a row's icon reads it. */\nexport type AiBuildItemRowState = 'done' | 'active' | 'waiting' | 'failed' | 'skipped'\n\nexport interface AiBuildItemRow {\n slot: string\n /** \"Page: About\", \"Form: Contact\". */\n label: string\n state: AiBuildItemRowState\n /** What the person should know: the failure, the note, what was given back. */\n detail: string | null\n /** When the active item started, for the elapsed time beside it; absent on every other row. */\n startedAt?: string | null\n}\n\nconst OP_NOUNS: Readonly<Record<string, string>> = {\n page: 'Page',\n layout: 'Layout',\n form: 'Form',\n component: 'Component',\n email: 'Email design',\n template: 'Page template',\n campaign: 'Email campaign',\n workflow: 'Automation',\n // A guided start's first posts and products (AGL-3676).\n posts: 'Blog',\n products: 'Store',\n}\n\n/** What an operation is called at the start of a row: its own noun, or the op spelled as words. */\nexport function aiBuildOpNoun(op: string): string {\n const known = OP_NOUNS[op]\n if (known) return known\n const words = op.replace(/[-_]+/g, ' ').trim()\n return words ? `${words[0].toUpperCase()}${words.slice(1)}` : 'Item'\n}\n\n/**\n * What became of a failed item's credits (AGL-3616), in the words every\n * surface uses for a failure on our side: \"This one’s on us — you weren’t\n * charged.\" A failure that was not ours — the model declining, a site at its\n * allowance — says what it used, which was charged. `null` for an item that\n * did not fail.\n */\nexport function aiBuildItemRefundCopy(\n row: Pick<AiJobItemLedger, 'status' | 'failure' | 'creditsRefunded' | 'creditsSpent'>,\n): string | null {\n if (row.status !== 'failed') return null\n if (row.failure?.ours) {\n const refunded = Math.max(0, Math.floor(row.creditsRefunded ?? 0))\n if (refunded > 0) {\n return `This one’s on us — you weren’t charged. The ${refunded} ${refunded === 1 ? 'credit' : 'credits'} it used ${refunded === 1 ? 'is' : 'are'} back in your AI credits.`\n }\n return 'This one’s on us — you weren’t charged.'\n }\n const spent = Math.max(0, Math.floor(row.creditsSpent ?? 0))\n return spent > 0 ? `It used ${spent} ${spent === 1 ? 'credit' : 'credits'}.` : null\n}\n\nconst STATES: Readonly<Record<AiJobItemLedger['status'], AiBuildItemRowState>> = {\n pending: 'waiting',\n running: 'active',\n succeeded: 'done',\n degraded: 'done',\n failed: 'failed',\n skipped: 'skipped',\n}\n\n/**\n * The item the build is on (AGL-3596): the one its ledger marks running, else\n * — while the job moves — the first still pending, which is the one its runner\n * builds next. The runner settles an item in the write that records its pass,\n * so the item it is writing reads `pending` until it is done; read as such,\n * every row waited and the build looked stalled. `null` when the job is not\n * moving or nothing is left.\n */\nexport function aiBuildActiveSlot(\n job: Pick<AiJobSummary, 'items'> & Partial<Pick<AiJobSummary, 'status'>>,\n): string | null {\n const items = job.items ?? []\n const running = items.find((row) => row.status === 'running')\n if (running) return running.slot\n if (job.status !== 'queued' && job.status !== 'running') return null\n return items.find((row) => row.status === 'pending')?.slot ?? null\n}\n\n/**\n * When the active item started: when the last item before it settled, which\n * the ledger records, else when the build step started — the first item's\n * start, and the best a ledger written before settling was recorded has.\n */\nexport function aiBuildActiveStartedAt(\n job: Pick<AiJobSummary, 'items'> & Partial<Pick<AiJobSummary, 'steps'>>,\n): string | null {\n let latest: { at: number; iso: string } | null = null\n for (const row of job.items ?? []) {\n const at = row.settledAt ? Date.parse(row.settledAt) : NaN\n if (Number.isFinite(at) && (!latest || at > latest.at)) latest = { at, iso: row.settledAt as string }\n }\n if (latest) return latest.iso\n const step = (job.steps ?? []).find((one) => one.name !== AI_JOB_PLAN_STEP && one.startedAt)\n return step?.startedAt ?? null\n}\n\n/** One row an item, in the order the build planned them; the item it is on is the active one. */\nexport function aiBuildItemRows(\n job: Pick<AiJobSummary, 'items'> & Partial<Pick<AiJobSummary, 'status' | 'steps'>>,\n): AiBuildItemRow[] {\n const active = aiBuildActiveSlot(job)\n return (job.items ?? []).map((ledger) => {\n // A building rule's own words never reach a customer (AGL-3596).\n const row = aiCustomerSafeItem(ledger)\n const parts: string[] = []\n if (row.status === 'failed' && row.failure?.message) parts.push(row.failure.message)\n if (row.note) parts.push(row.note)\n const refund = aiBuildItemRefundCopy(row)\n if (refund) parts.push(refund)\n return {\n slot: row.slot,\n label: `${aiBuildOpNoun(row.op)}: ${row.label}`,\n state: row.slot === active ? 'active' : (STATES[row.status] ?? 'waiting'),\n detail: parts.length ? parts.join(' ') : null,\n ...(row.slot === active ? { startedAt: aiBuildActiveStartedAt(job) } : {}),\n }\n })\n}\n\n/** How a build came out, in one line: \"4 of 5 built; 1 failed.\" `null` while it runs. */\nexport function aiBuildOutcomeLine(job: Pick<AiJobSummary, 'items' | 'status'>): string | null {\n const items = job.items ?? []\n if (!items.length || (job.status !== 'done' && job.status !== 'failed')) return null\n const built = items.filter((row) => row.status === 'succeeded' || row.status === 'degraded').length\n const failed = items.filter((row) => row.status === 'failed').length\n const skipped = items.filter((row) => row.status === 'skipped').length\n const tail = [failed ? `${failed} failed` : '', skipped ? `${skipped} not built` : ''].filter(Boolean)\n return `${built} of ${items.length} built${tail.length ? `; ${tail.join(', ')}` : ''}.`\n}\n\n/** Whether a finished build — or a site that built part of itself — has failed items to try again. */\nexport function aiBuildCanRetry(job: Pick<AiJobSummary, 'kind' | 'status' | 'items'>): boolean {\n // A site that built nothing starts over from its own answers instead.\n const finished = job.kind === 'build' ? job.status === 'done' || job.status === 'failed' : job.kind === 'site' && job.status === 'done'\n return (\n finished &&\n (job.items ?? []).some((row) => row.status === 'failed' || (row.status === 'skipped' && !row.degradedBy?.length))\n )\n}\n\n/**\n * What a guided start that built part of its site says (AGL-3616), in the\n * words every surface uses: \"Built 1 of 2 pages. Home couldn’t be built —\n * that one’s on us…\". `null` when every page was built, or none was.\n */\nexport function aiSitePartialCopy(job: Pick<AiJobSummary, 'items' | 'status'>): string | null {\n const pages = (job.items ?? []).filter((row) => row.op === 'page')\n const built = pages.filter((row) => row.status === 'succeeded' || row.status === 'degraded')\n const unbuilt = (job.items ?? []).filter((row) => row.status === 'failed' || row.status === 'skipped')\n if (job.status !== 'done' || !unbuilt.length || !built.length) return null\n const names = unbuilt.map((row) => (row.op === 'page' ? row.label : `The ${aiBuildOpNoun(row.op).toLowerCase()} “${row.label}”`))\n const list = names.length === 1 ? names[0] : `${names.slice(0, -1).join(', ')} and ${names[names.length - 1]}`\n const refunded = unbuilt.reduce((total, row) => total + Math.max(0, Math.floor(row.creditsRefunded ?? 0)), 0)\n const back = refunded > 0 ? ` The ${refunded} ${refunded === 1 ? 'credit' : 'credits'} it used ${refunded === 1 ? 'is' : 'are'} back in your AI credits.` : ''\n // Only a failure on our side says so; a part the AI declined or an allowance\n // stopped was charged for what it used, and the row says that.\n const ours = unbuilt.every((row) => row.status === 'skipped' || row.failure?.ours === true)\n const why = ours ? ` — that one’s on us, you weren’t charged for ${unbuilt.length === 1 ? 'it' : 'them'}.` : '.'\n return `Built ${built.length} of ${pages.length} ${pages.length === 1 ? 'page' : 'pages'}. ${list} couldn’t be built${why}${back} Try again builds only what failed.`\n}\n"],"names":["aiCustomerSafeItem","AI_JOB_PLAN_STEP","OP_NOUNS","page","layout","form","component","email","template","campaign","workflow","posts","products","aiBuildOpNoun","op","known","words","replace","trim","toUpperCase","slice","aiBuildItemRefundCopy","row","status","failure","ours","refunded","Math","max","floor","creditsRefunded","spent","creditsSpent","STATES","pending","running","succeeded","degraded","failed","skipped","aiBuildActiveSlot","job","items","find","slot","aiBuildActiveStartedAt","latest","at","settledAt","Date","parse","NaN","Number","isFinite","iso","step","steps","one","name","startedAt","aiBuildItemRows","active","map","ledger","parts","message","push","note","refund","label","state","detail","length","join","aiBuildOutcomeLine","built","filter","tail","Boolean","aiBuildCanRetry","finished","kind","some","degradedBy","aiSitePartialCopy","pages","unbuilt","names","toLowerCase","list","reduce","total","back","every","why"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-build-progress.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 { aiCustomerSafeItem } from './ai-job-failure-copy'\nimport { AI_JOB_PLAN_STEP, type AiJobItemLedger, type AiJobSummary } from './ai-jobs.types'\nimport { aiBuildCreditRange, aiBuildRetryLedger, aiBuildUnits, aiLedgerUnits } from './ai-build-job'\nimport {\n AI_CREDIT_RANGE_ZERO,\n aiCreditRangeAdd,\n aiCreditRangeOf,\n aiCreditRangeOrdered,\n type AiCreditRange,\n} from './ai-credit-estimate'\nimport {\n AI_SITE_NOMINAL_SECTIONS,\n AI_SITE_PASS_CREDITS,\n aiCreationMeasuredCredits,\n aiScreenCreditRange,\n} from './ai-site-job'\n\n/**\n * What a `build` job shows of each item (AGL-3616), wherever it is shown —\n * the AI jobs drawer, the job's own page, the chat card: the item, where it\n * stands, what it cost, and what became of its credits when it failed.\n *\n * Pure: no React, no request.\n */\n\n/** Where one item stands, as a row's icon reads it. */\nexport type AiBuildItemRowState = 'done' | 'active' | 'waiting' | 'failed' | 'skipped'\n\nexport interface AiBuildItemRow {\n slot: string\n /** \"Page: About\", \"Form: Contact\". */\n label: string\n state: AiBuildItemRowState\n /** What the person should know: the failure, the note, what was given back. */\n detail: string | null\n /** When the active item started, for the elapsed time beside it; absent on every other row. */\n startedAt?: string | null\n}\n\nconst OP_NOUNS: Readonly<Record<string, string>> = {\n page: 'Page',\n layout: 'Layout',\n form: 'Form',\n component: 'Component',\n email: 'Email design',\n template: 'Page template',\n campaign: 'Email campaign',\n workflow: 'Automation',\n // A guided start's first posts and products (AGL-3676).\n posts: 'Blog',\n products: 'Store',\n}\n\n/** What an operation is called at the start of a row: its own noun, or the op spelled as words. */\nexport function aiBuildOpNoun(op: string): string {\n const known = OP_NOUNS[op]\n if (known) return known\n const words = op.replace(/[-_]+/g, ' ').trim()\n return words ? `${words[0].toUpperCase()}${words.slice(1)}` : 'Item'\n}\n\n/**\n * What became of a failed item's credits (AGL-3616), in the words every\n * surface uses for a failure on our side: \"This one’s on us — you weren’t\n * charged.\" A failure that was not ours — the model declining, a site at its\n * allowance — says what it used, which was charged. `null` for an item that\n * did not fail.\n */\nexport function aiBuildItemRefundCopy(\n row: Pick<AiJobItemLedger, 'status' | 'failure' | 'creditsRefunded' | 'creditsSpent'>,\n): string | null {\n if (row.status !== 'failed') return null\n if (row.failure?.ours) {\n const refunded = Math.max(0, Math.floor(row.creditsRefunded ?? 0))\n if (refunded > 0) {\n return `This one’s on us — you weren’t charged. The ${refunded} ${refunded === 1 ? 'credit' : 'credits'} it used ${refunded === 1 ? 'is' : 'are'} back in your AI credits.`\n }\n return 'This one’s on us — you weren’t charged.'\n }\n const spent = Math.max(0, Math.floor(row.creditsSpent ?? 0))\n return spent > 0 ? `It used ${spent} ${spent === 1 ? 'credit' : 'credits'}.` : null\n}\n\nconst STATES: Readonly<Record<AiJobItemLedger['status'], AiBuildItemRowState>> = {\n pending: 'waiting',\n running: 'active',\n succeeded: 'done',\n degraded: 'done',\n failed: 'failed',\n skipped: 'skipped',\n}\n\n/**\n * The item the build is on (AGL-3596): the one its ledger marks running, else\n * — while the job moves — the first still pending, which is the one its runner\n * builds next. The runner settles an item in the write that records its pass,\n * so the item it is writing reads `pending` until it is done; read as such,\n * every row waited and the build looked stalled. `null` when the job is not\n * moving or nothing is left.\n */\nexport function aiBuildActiveSlot(\n job: Pick<AiJobSummary, 'items'> & Partial<Pick<AiJobSummary, 'status'>>,\n): string | null {\n const items = job.items ?? []\n const running = items.find((row) => row.status === 'running')\n if (running) return running.slot\n if (job.status !== 'queued' && job.status !== 'running') return null\n return items.find((row) => row.status === 'pending')?.slot ?? null\n}\n\n/**\n * When the active item started: when the last item before it settled, which\n * the ledger records, else when the build step started — the first item's\n * start, and the best a ledger written before settling was recorded has.\n */\nexport function aiBuildActiveStartedAt(\n job: Pick<AiJobSummary, 'items'> & Partial<Pick<AiJobSummary, 'steps'>>,\n): string | null {\n let latest: { at: number; iso: string } | null = null\n for (const row of job.items ?? []) {\n const at = row.settledAt ? Date.parse(row.settledAt) : NaN\n if (Number.isFinite(at) && (!latest || at > latest.at)) latest = { at, iso: row.settledAt as string }\n }\n if (latest) return latest.iso\n const step = (job.steps ?? []).find((one) => one.name !== AI_JOB_PLAN_STEP && one.startedAt)\n return step?.startedAt ?? null\n}\n\n/** One row an item, in the order the build planned them; the item it is on is the active one. */\nexport function aiBuildItemRows(\n job: Pick<AiJobSummary, 'items'> & Partial<Pick<AiJobSummary, 'status' | 'steps'>>,\n): AiBuildItemRow[] {\n const active = aiBuildActiveSlot(job)\n return (job.items ?? []).map((ledger) => {\n // A building rule's own words never reach a customer (AGL-3596).\n const row = aiCustomerSafeItem(ledger)\n const parts: string[] = []\n if (row.status === 'failed' && row.failure?.message) parts.push(row.failure.message)\n if (row.note) parts.push(row.note)\n const refund = aiBuildItemRefundCopy(row)\n if (refund) parts.push(refund)\n return {\n slot: row.slot,\n label: `${aiBuildOpNoun(row.op)}: ${row.label}`,\n state: row.slot === active ? 'active' : (STATES[row.status] ?? 'waiting'),\n detail: parts.length ? parts.join(' ') : null,\n ...(row.slot === active ? { startedAt: aiBuildActiveStartedAt(job) } : {}),\n }\n })\n}\n\n/** How a build came out, in one line: \"4 of 5 built; 1 failed.\" `null` while it runs. */\nexport function aiBuildOutcomeLine(job: Pick<AiJobSummary, 'items' | 'status'>): string | null {\n const items = job.items ?? []\n if (!items.length || (job.status !== 'done' && job.status !== 'failed')) return null\n const built = items.filter((row) => row.status === 'succeeded' || row.status === 'degraded').length\n const failed = items.filter((row) => row.status === 'failed').length\n const skipped = items.filter((row) => row.status === 'skipped').length\n const tail = [failed ? `${failed} failed` : '', skipped ? `${skipped} not built` : ''].filter(Boolean)\n return `${built} of ${items.length} built${tail.length ? `; ${tail.join(', ')}` : ''}.`\n}\n\n/** Whether a finished build — or a site that built part of itself — has failed items to try again. */\nexport function aiBuildCanRetry(job: Pick<AiJobSummary, 'kind' | 'status' | 'items'>): boolean {\n // A site that built nothing starts over from its own answers instead.\n const finished = job.kind === 'build' ? job.status === 'done' || job.status === 'failed' : job.kind === 'site' && job.status === 'done'\n return (\n finished &&\n (job.items ?? []).some((row) => row.status === 'failed' || (row.status === 'skipped' && !row.degradedBy?.length))\n )\n}\n\n/**\n * What Try again is likely to cost (AGL-3722), quoted beside its button: a\n * build's retried items by `aiBuildCreditRange`; a site's retried rows each\n * at what its kind measured — a page at the nominal sections — held at the\n * reserve a row is retried at. `null` when nothing would run.\n */\nexport function aiBuildRetryCreditRange(\n job: Pick<AiJobSummary, 'kind' | 'items' | 'plan'>,\n): AiCreditRange | null {\n const items = job.items ?? []\n if (!items.length) return null\n if (job.kind === 'build') {\n if (!job.plan) return null\n const plan = job.plan as unknown as Parameters<typeof aiBuildUnits>[0]\n const { retried } = aiBuildRetryLedger(items, aiBuildUnits(plan))\n return retried.length ? aiBuildCreditRange(plan, { slots: new Set(retried) }) : null\n }\n const { retried } = aiBuildRetryLedger(items, aiLedgerUnits(items))\n if (!retried.length) return null\n const ops = new Map(items.map((row) => [row.slot, row.op]))\n return aiCreditRangeOrdered(\n retried.reduce((total, slot) => {\n const op = ops.get(slot) ?? ''\n const row =\n op === 'page'\n ? aiScreenCreditRange(AI_SITE_NOMINAL_SECTIONS)\n : aiCreditRangeOf(aiCreationMeasuredCredits(op), AI_SITE_PASS_CREDITS)\n return aiCreditRangeAdd(total, row)\n }, AI_CREDIT_RANGE_ZERO),\n )\n}\n\n/**\n * What a guided start that built part of its site says (AGL-3616), in the\n * words every surface uses: \"Built 1 of 2 pages. Home couldn’t be built —\n * that one’s on us…\". `null` when every page was built, or none was.\n */\nexport function aiSitePartialCopy(job: Pick<AiJobSummary, 'items' | 'status'>): string | null {\n const pages = (job.items ?? []).filter((row) => row.op === 'page')\n const built = pages.filter((row) => row.status === 'succeeded' || row.status === 'degraded')\n const unbuilt = (job.items ?? []).filter((row) => row.status === 'failed' || row.status === 'skipped')\n if (job.status !== 'done' || !unbuilt.length || !built.length) return null\n const names = unbuilt.map((row) => (row.op === 'page' ? row.label : `The ${aiBuildOpNoun(row.op).toLowerCase()} “${row.label}”`))\n const list = names.length === 1 ? names[0] : `${names.slice(0, -1).join(', ')} and ${names[names.length - 1]}`\n const refunded = unbuilt.reduce((total, row) => total + Math.max(0, Math.floor(row.creditsRefunded ?? 0)), 0)\n const back = refunded > 0 ? ` The ${refunded} ${refunded === 1 ? 'credit' : 'credits'} it used ${refunded === 1 ? 'is' : 'are'} back in your AI credits.` : ''\n // Only a failure on our side says so; a part the AI declined or an allowance\n // stopped was charged for what it used, and the row says that.\n const ours = unbuilt.every((row) => row.status === 'skipped' || row.failure?.ours === true)\n const why = ours ? ` — that one’s on us, you weren’t charged for ${unbuilt.length === 1 ? 'it' : 'them'}.` : '.'\n return `Built ${built.length} of ${pages.length} ${pages.length === 1 ? 'page' : 'pages'}. ${list} couldn’t be built${why}${back} Try again builds only what failed.`\n}\n"],"names":["aiCustomerSafeItem","AI_JOB_PLAN_STEP","aiBuildCreditRange","aiBuildRetryLedger","aiBuildUnits","aiLedgerUnits","AI_CREDIT_RANGE_ZERO","aiCreditRangeAdd","aiCreditRangeOf","aiCreditRangeOrdered","AI_SITE_NOMINAL_SECTIONS","AI_SITE_PASS_CREDITS","aiCreationMeasuredCredits","aiScreenCreditRange","OP_NOUNS","page","layout","form","component","email","template","campaign","workflow","posts","products","aiBuildOpNoun","op","known","words","replace","trim","toUpperCase","slice","aiBuildItemRefundCopy","row","status","failure","ours","refunded","Math","max","floor","creditsRefunded","spent","creditsSpent","STATES","pending","running","succeeded","degraded","failed","skipped","aiBuildActiveSlot","job","items","find","slot","aiBuildActiveStartedAt","latest","at","settledAt","Date","parse","NaN","Number","isFinite","iso","step","steps","one","name","startedAt","aiBuildItemRows","active","map","ledger","parts","message","push","note","refund","label","state","detail","length","join","aiBuildOutcomeLine","built","filter","tail","Boolean","aiBuildCanRetry","finished","kind","some","degradedBy","aiBuildRetryCreditRange","plan","retried","slots","Set","ops","Map","reduce","total","get","aiSitePartialCopy","pages","unbuilt","names","toLowerCase","list","back","every","why"],"mappings":"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"}
@@ -0,0 +1,182 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ /**
18
+ * WHAT A JOB REALLY COSTS, NEXT TO WHAT IT CAN COST (AGL-3722).
19
+ *
20
+ * Every job's cost used to be quoted at its CEILING: each pass at the
21
+ * `AI_SITE_PASS_CREDITS` (50) a pass is reserved against. A Free Assist
22
+ * build of two pages, a quote form and a layout was quoted 600 credits and
23
+ * refused with 164 left, when builds like it measure about a fifth of that.
24
+ *
25
+ * So a job is now quoted as a RANGE: what it is likely to cost (the sum of
26
+ * the measured medians of what it builds), what it costs nine times in ten
27
+ * (the measured p90s), and its ceiling (the old figure, unchanged). The
28
+ * meter still charges actual use, and the reservation each pass takes is
29
+ * still the ceiling's 50 — only the words and the Free admission move:
30
+ * a Free job is admitted outright when its p90 fits what is left, and asked
31
+ * to be confirmed when it does not.
32
+ *
33
+ * This module is pure: the console, the doors and the specs read one table.
34
+ */
35
+ /** One measured cost: the median and the 90th percentile, in credits. */
36
+ export interface AiMeasuredCredits {
37
+ median: number;
38
+ p90: number;
39
+ }
40
+ /**
41
+ * MEASURED IN PRODUCTION, credits per unit, over every `done` AI job since
42
+ * 2026-10-01 (read 2026-10-09; a whole Free site job came to 148–160):
43
+ *
44
+ * page item 30 / 40 (a page of about `AI_MEASURED_PAGE_SECTIONS` sections, its listing included)
45
+ * form 29 / 31
46
+ * layout 23 / 24 (header and footer, one answer)
47
+ * theme 4 / 8 (a site start's look)
48
+ * plan step 21 / 70
49
+ *
50
+ * HOW TO RE-MEASURE: a read-only collection-group read of `aiJobs` in
51
+ * production where `status == 'done'` and `createdAt` is in the window; group
52
+ * each ledger row's (`items[]`) `creditsSpent` by its `op` (the site start's
53
+ * look is the `theme` row), take the plan step's `steps[].creditsSpent` for
54
+ * `plan`, and each job's `creditsSpent` for the whole-site band. Put each
55
+ * group's median and 90th percentile here, and the dates in
56
+ * `AI_MEASURED_AT`. `ai-credit-estimate.spec.ts` holds the table, the page
57
+ * scaling and the measured whole-site band together, so a re-measure that
58
+ * moves one moves the spec with it.
59
+ */
60
+ export declare const AI_MEASURED_UNIT_CREDITS: {
61
+ readonly page: {
62
+ readonly median: 30;
63
+ readonly p90: 40;
64
+ };
65
+ readonly form: {
66
+ readonly median: 29;
67
+ readonly p90: 31;
68
+ };
69
+ readonly layout: {
70
+ readonly median: 23;
71
+ readonly p90: 24;
72
+ };
73
+ readonly theme: {
74
+ readonly median: 4;
75
+ readonly p90: 8;
76
+ };
77
+ readonly plan: {
78
+ readonly median: 21;
79
+ readonly p90: 70;
80
+ };
81
+ };
82
+ /** When the table above was read, and from when its jobs ran. */
83
+ export declare const AI_MEASURED_AT: {
84
+ readonly read: "2026-10-09";
85
+ readonly since: "2026-10-01";
86
+ };
87
+ /** What a whole Free two-page site job measured at, in credits, in the same window. */
88
+ export declare const AI_MEASURED_FREE_SITE_CREDITS: {
89
+ readonly low: 148;
90
+ readonly high: 160;
91
+ };
92
+ /** The sections the measured page item is about: a page is scaled around it. */
93
+ export declare const AI_MEASURED_PAGE_SECTIONS = 5;
94
+ /**
95
+ * What a page of this many sections is likely to cost: the measured page
96
+ * scaled by its sections around the ~5-section median — about 6 credits a
97
+ * section, 8 at the p90.
98
+ */
99
+ export declare function aiMeasuredPageCredits(sections: number): AiMeasuredCredits;
100
+ /**
101
+ * What one pass of a kind with no measurement of its own is likely to cost
102
+ * (a component, an email design, a template, an item another runner
103
+ * builds): a form's, the measured single-answer creation closest to them.
104
+ * A declared stand-in, replaced the day those kinds are measured.
105
+ */
106
+ export declare const AI_MEASURED_PASS_CREDITS: AiMeasuredCredits;
107
+ /** A job's cost as it is quoted: likely, nine times in ten, and at its worst. */
108
+ export interface AiCreditRange {
109
+ /** The sum of the measured medians: what the job is likely to spend. */
110
+ likely: number;
111
+ /** The sum of the measured p90s: what a Free job is admitted on. */
112
+ p90: number;
113
+ /** Every pass at its reserve: what it can spend at most. */
114
+ ceiling: number;
115
+ }
116
+ export declare const AI_CREDIT_RANGE_ZERO: AiCreditRange;
117
+ /** Two ranges added. */
118
+ export declare function aiCreditRangeAdd(a: AiCreditRange, b: AiCreditRange): AiCreditRange;
119
+ /** A measured cost as a range with its own ceiling, the likely and p90 never above it. */
120
+ export declare function aiCreditRangeOf(measured: AiMeasuredCredits, ceiling: number): AiCreditRange;
121
+ /** A range whose likely and p90 are each at least the last, and at most the ceiling. */
122
+ export declare function aiCreditRangeOrdered(range: AiCreditRange): AiCreditRange;
123
+ /**
124
+ * The range as every surface quotes it: "About 106 credits (up to 650)".
125
+ * A job whose likely figure is its ceiling says one number.
126
+ */
127
+ export declare function aiCreditRangeText(range: AiCreditRange): string;
128
+ /**
129
+ * Whether a Free job of this range needs the person's go-ahead before it
130
+ * starts (AGL-3722): its p90 is more than what is left. A job whose p90
131
+ * fits starts with no prompt; one that does not starts only when confirmed,
132
+ * builds as much as what is left pays for, and pauses with Resume.
133
+ */
134
+ export declare function aiCreditsNeedConfirm(range: Pick<AiCreditRange, 'p90'>, left: number): boolean;
135
+ /** The smaller first build a Free job that does not fit is offered (AGL-3722). */
136
+ export interface AiCreditsSmaller {
137
+ /** "Build the home page first". */
138
+ label: string;
139
+ likely: number;
140
+ p90: number;
141
+ ceiling: number;
142
+ }
143
+ /**
144
+ * What a Free job that does not fit what is left says before it starts, and
145
+ * the choices it offers: build what fits, a smaller first build, or Upgrade.
146
+ * The door that refuses an unconfirmed start answers with it (status 409,
147
+ * `code: AI_CREDITS_CONFIRM_CODE`), and the plan card and the guided start
148
+ * draw it from the same figures.
149
+ */
150
+ export interface AiCreditsPrompt extends AiCreditRange {
151
+ left: number;
152
+ resetsOn: string;
153
+ smaller: AiCreditsSmaller | null;
154
+ }
155
+ /** The `code` a door answers an unconfirmed Free start with. */
156
+ export declare const AI_CREDITS_CONFIRM_CODE = "credits-confirm";
157
+ /** "Build what fits": the go-ahead for a Free job past what is left. */
158
+ export declare const AI_CREDITS_BUILD_WHAT_FITS = "Build what fits";
159
+ /** The smaller first build's button: "Build the home page first (about 64 credits)". */
160
+ export declare function aiCreditsSmallerText(smaller: Pick<AiCreditsSmaller, 'label' | 'likely'>): string;
161
+ /**
162
+ * The plain sentence (Zach, 2026-10-09): "This build is about N credits (up
163
+ * to M). You have L left, so it will build as much as it can and pause when
164
+ * your credits run out. You can upgrade or resume when they renew."
165
+ */
166
+ export declare function aiCreditsPromptText(prompt: AiCreditsPrompt, noun?: 'build' | 'site'): string;
167
+ /**
168
+ * The record a confirmed Free start keeps (AGL-3722): who said go, when,
169
+ * what was left then and the range they were shown.
170
+ */
171
+ export interface AiJobCreditsConfirmation extends AiCreditRange {
172
+ by: string;
173
+ at: Date | string;
174
+ left: number;
175
+ }
176
+ /** The confirmation a door keeps on the job for the prompt the person confirmed, or `null` when there was none. */
177
+ export declare function aiCreditsConfirmation(prompt: AiCreditsPrompt | null, by: string, at: Date): AiJobCreditsConfirmation | null;
178
+ /** The prompt a range needs against what is left, or `null` when it fits. */
179
+ export declare function aiCreditsPromptFor(range: AiCreditRange, credits: {
180
+ left: number;
181
+ resetsOn: string;
182
+ } | null | undefined, smaller?: AiCreditsSmaller | null): AiCreditsPrompt | null;
@@ -0,0 +1,190 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ /**
17
+ * WHAT A JOB REALLY COSTS, NEXT TO WHAT IT CAN COST (AGL-3722).
18
+ *
19
+ * Every job's cost used to be quoted at its CEILING: each pass at the
20
+ * `AI_SITE_PASS_CREDITS` (50) a pass is reserved against. A Free Assist
21
+ * build of two pages, a quote form and a layout was quoted 600 credits and
22
+ * refused with 164 left, when builds like it measure about a fifth of that.
23
+ *
24
+ * So a job is now quoted as a RANGE: what it is likely to cost (the sum of
25
+ * the measured medians of what it builds), what it costs nine times in ten
26
+ * (the measured p90s), and its ceiling (the old figure, unchanged). The
27
+ * meter still charges actual use, and the reservation each pass takes is
28
+ * still the ceiling's 50 — only the words and the Free admission move:
29
+ * a Free job is admitted outright when its p90 fits what is left, and asked
30
+ * to be confirmed when it does not.
31
+ *
32
+ * This module is pure: the console, the doors and the specs read one table.
33
+ */ /** One measured cost: the median and the 90th percentile, in credits. */ import { _ as _extends } from "@swc/helpers/_/_extends";
34
+ /**
35
+ * MEASURED IN PRODUCTION, credits per unit, over every `done` AI job since
36
+ * 2026-10-01 (read 2026-10-09; a whole Free site job came to 148–160):
37
+ *
38
+ * page item 30 / 40 (a page of about `AI_MEASURED_PAGE_SECTIONS` sections, its listing included)
39
+ * form 29 / 31
40
+ * layout 23 / 24 (header and footer, one answer)
41
+ * theme 4 / 8 (a site start's look)
42
+ * plan step 21 / 70
43
+ *
44
+ * HOW TO RE-MEASURE: a read-only collection-group read of `aiJobs` in
45
+ * production where `status == 'done'` and `createdAt` is in the window; group
46
+ * each ledger row's (`items[]`) `creditsSpent` by its `op` (the site start's
47
+ * look is the `theme` row), take the plan step's `steps[].creditsSpent` for
48
+ * `plan`, and each job's `creditsSpent` for the whole-site band. Put each
49
+ * group's median and 90th percentile here, and the dates in
50
+ * `AI_MEASURED_AT`. `ai-credit-estimate.spec.ts` holds the table, the page
51
+ * scaling and the measured whole-site band together, so a re-measure that
52
+ * moves one moves the spec with it.
53
+ */ export const AI_MEASURED_UNIT_CREDITS = {
54
+ page: {
55
+ median: 30,
56
+ p90: 40
57
+ },
58
+ form: {
59
+ median: 29,
60
+ p90: 31
61
+ },
62
+ layout: {
63
+ median: 23,
64
+ p90: 24
65
+ },
66
+ theme: {
67
+ median: 4,
68
+ p90: 8
69
+ },
70
+ plan: {
71
+ median: 21,
72
+ p90: 70
73
+ }
74
+ };
75
+ /** When the table above was read, and from when its jobs ran. */ export const AI_MEASURED_AT = {
76
+ read: '2026-10-09',
77
+ since: '2026-10-01'
78
+ };
79
+ /** What a whole Free two-page site job measured at, in credits, in the same window. */ export const AI_MEASURED_FREE_SITE_CREDITS = {
80
+ low: 148,
81
+ high: 160
82
+ };
83
+ /** The sections the measured page item is about: a page is scaled around it. */ export const AI_MEASURED_PAGE_SECTIONS = 5;
84
+ /**
85
+ * What a page of this many sections is likely to cost: the measured page
86
+ * scaled by its sections around the ~5-section median — about 6 credits a
87
+ * section, 8 at the p90.
88
+ */ export function aiMeasuredPageCredits(sections) {
89
+ const count = Math.max(1, Math.floor(Number.isFinite(sections) ? sections : AI_MEASURED_PAGE_SECTIONS));
90
+ const { median, p90 } = AI_MEASURED_UNIT_CREDITS.page;
91
+ return {
92
+ median: Math.ceil(median * count / AI_MEASURED_PAGE_SECTIONS),
93
+ p90: Math.ceil(p90 * count / AI_MEASURED_PAGE_SECTIONS)
94
+ };
95
+ }
96
+ /**
97
+ * What one pass of a kind with no measurement of its own is likely to cost
98
+ * (a component, an email design, a template, an item another runner
99
+ * builds): a form's, the measured single-answer creation closest to them.
100
+ * A declared stand-in, replaced the day those kinds are measured.
101
+ */ export const AI_MEASURED_PASS_CREDITS = AI_MEASURED_UNIT_CREDITS.form;
102
+ export const AI_CREDIT_RANGE_ZERO = {
103
+ likely: 0,
104
+ p90: 0,
105
+ ceiling: 0
106
+ };
107
+ /** Two ranges added. */ export function aiCreditRangeAdd(a, b) {
108
+ return {
109
+ likely: a.likely + b.likely,
110
+ p90: a.p90 + b.p90,
111
+ ceiling: a.ceiling + b.ceiling
112
+ };
113
+ }
114
+ /** A measured cost as a range with its own ceiling, the likely and p90 never above it. */ export function aiCreditRangeOf(measured, ceiling) {
115
+ const top = Math.max(0, Math.ceil(ceiling));
116
+ return {
117
+ likely: Math.min(measured.median, top),
118
+ p90: Math.min(measured.p90, top),
119
+ ceiling: top
120
+ };
121
+ }
122
+ /** A range whose likely and p90 are each at least the last, and at most the ceiling. */ export function aiCreditRangeOrdered(range) {
123
+ const ceiling = Math.max(0, Math.ceil(range.ceiling));
124
+ const likely = Math.min(Math.max(0, Math.ceil(range.likely)), ceiling);
125
+ const p90 = Math.min(Math.max(likely, Math.ceil(range.p90)), ceiling);
126
+ return {
127
+ likely,
128
+ p90,
129
+ ceiling
130
+ };
131
+ }
132
+ const n = (value)=>value.toLocaleString('en-US');
133
+ /**
134
+ * The range as every surface quotes it: "About 106 credits (up to 650)".
135
+ * A job whose likely figure is its ceiling says one number.
136
+ */ export function aiCreditRangeText(range) {
137
+ if (range.likely >= range.ceiling) return `About ${n(range.ceiling)} credits`;
138
+ return `About ${n(range.likely)} credits (up to ${n(range.ceiling)})`;
139
+ }
140
+ /**
141
+ * Whether a Free job of this range needs the person's go-ahead before it
142
+ * starts (AGL-3722): its p90 is more than what is left. A job whose p90
143
+ * fits starts with no prompt; one that does not starts only when confirmed,
144
+ * builds as much as what is left pays for, and pauses with Resume.
145
+ */ export function aiCreditsNeedConfirm(range, left) {
146
+ return Number.isFinite(left) && range.p90 > Math.max(0, Math.floor(left));
147
+ }
148
+ /** The `code` a door answers an unconfirmed Free start with. */ export const AI_CREDITS_CONFIRM_CODE = 'credits-confirm';
149
+ /** "Build what fits": the go-ahead for a Free job past what is left. */ export const AI_CREDITS_BUILD_WHAT_FITS = 'Build what fits';
150
+ /** The smaller first build's button: "Build the home page first (about 64 credits)". */ export function aiCreditsSmallerText(smaller) {
151
+ return `${smaller.label} (about ${n(smaller.likely)} credits)`;
152
+ }
153
+ /** A reset day as a person reads it: "November 1". */ function resetLabel(resetsOn) {
154
+ const at = new Date(`${resetsOn}T00:00:00.000Z`);
155
+ return Number.isFinite(at.getTime()) ? at.toLocaleDateString('en-US', {
156
+ month: 'long',
157
+ day: 'numeric',
158
+ timeZone: 'UTC'
159
+ }) : 'the first of next month';
160
+ }
161
+ /**
162
+ * The plain sentence (Zach, 2026-10-09): "This build is about N credits (up
163
+ * to M). You have L left, so it will build as much as it can and pause when
164
+ * your credits run out. You can upgrade or resume when they renew."
165
+ */ export function aiCreditsPromptText(prompt, noun = 'build') {
166
+ const size = prompt.likely >= prompt.ceiling ? `about ${n(prompt.ceiling)} credits` : `about ${n(prompt.likely)} credits (up to ${n(prompt.ceiling)})`;
167
+ const left = Math.max(0, Math.floor(prompt.left));
168
+ return `This ${noun} is ${size}. You have ${n(left)} left, so it will build as much as it can and pause ` + `when your credits run out. You can upgrade or resume when they renew on ${resetLabel(prompt.resetsOn)}.`;
169
+ }
170
+ /** The confirmation a door keeps on the job for the prompt the person confirmed, or `null` when there was none. */ export function aiCreditsConfirmation(prompt, by, at) {
171
+ if (!prompt) return null;
172
+ return {
173
+ by,
174
+ at,
175
+ left: prompt.left,
176
+ likely: prompt.likely,
177
+ p90: prompt.p90,
178
+ ceiling: prompt.ceiling
179
+ };
180
+ }
181
+ /** The prompt a range needs against what is left, or `null` when it fits. */ export function aiCreditsPromptFor(range, credits, smaller = null) {
182
+ if (!credits || !aiCreditsNeedConfirm(range, credits.left)) return null;
183
+ return _extends({}, range, {
184
+ left: Math.max(0, Math.floor(credits.left)),
185
+ resetsOn: credits.resetsOn,
186
+ smaller
187
+ });
188
+ }
189
+
190
+ //# sourceMappingURL=ai-credit-estimate.js.map
@@ -0,0 +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 * 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 page: { median: 30, p90: 40 },\n form: { median: 29, p90: 31 },\n layout: { median: 23, p90: 24 },\n theme: { median: 4, p90: 8 },\n plan: { median: 21, p90: 70 },\n} as const satisfies Record<string, AiMeasuredCredits>\n\n/** When the table above was read, and from when its jobs ran. */\nexport const AI_MEASURED_AT = { read: '2026-10-09', since: '2026-10-01' } as const\n\n/** What a whole Free two-page site job measured at, in credits, in the same window. */\nexport const AI_MEASURED_FREE_SITE_CREDITS = { low: 148, high: 160 } 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 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): a form's, the measured single-answer creation closest to them.\n * A declared stand-in, replaced the day those kinds are measured.\n */\nexport const AI_MEASURED_PASS_CREDITS: AiMeasuredCredits = AI_MEASURED_UNIT_CREDITS.form\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/** 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_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_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","resetLabel","resetsOn","at","Date","getTime","toLocaleDateString","month","day","timeZone","aiCreditsPromptText","prompt","noun","size","aiCreditsConfirmation","by","aiCreditsPromptFor","credits"],"mappings":"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"}
@@ -18,6 +18,8 @@ import type { ITimestamp } from '@aglyn/shared-util-timestamp';
18
18
  import type { AiEffort } from '../providers/contract';
19
19
  import type { AiLoadEstimate } from '../runtime/ai-palette';
20
20
  import type { AiBuildPlan } from './ai-build-plan';
21
+ import type { AiFreeCreditsLeft } from './ai-site-job';
22
+ import type { AiJobCreditsConfirmation } from './ai-credit-estimate';
21
23
  /**
22
24
  * AI generation jobs (AGL-2904).
23
25
  *
@@ -305,6 +307,13 @@ export interface AiJobReview {
305
307
  * disabled with this beside it.
306
308
  */
307
309
  retryRefusal?: string;
310
+ /**
311
+ * What a Free workspace had left when a build's plan was kept (AGL-3722),
312
+ * so its plan card says, before Confirm, whether the build fits and offers
313
+ * the go-ahead or a smaller first build when it does not. The confirm door
314
+ * reads what is left again; this is what the card shows.
315
+ */
316
+ freeCredits?: AiFreeCreditsLeft;
308
317
  }
309
318
  export type AiJobPlanStatus = 'proposed' | 'confirmed';
310
319
  /** The plan a job builds from, as the job keeps it once its plan step proposed it. */
@@ -453,6 +462,12 @@ export interface AiJob {
453
462
  items?: AiJobItemLedger[] | null;
454
463
  /** A `build` job's planning spend and how it settled (AGL-3616). */
455
464
  orchestration?: AiJobOrchestration | null;
465
+ /**
466
+ * A Free job started past what was left (AGL-3722): the person chose to
467
+ * build what fits, knowing it pauses where the credits run out. Who, when,
468
+ * what was left and the range they were shown; absent on every job that fit.
469
+ */
470
+ creditsConfirmed?: AiJobCreditsConfirmation | null;
456
471
  }
457
472
  /**
458
473
  * A guided site start's publish (AGL-3596): the pages that went live, with