@aglyn/plugins-ai 1.0.0-beta.219 → 1.0.0-beta.221

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 (181) hide show
  1. package/package.json +18 -18
  2. package/src/lib/billing/ai-month-meter.d.ts +51 -0
  3. package/src/lib/billing/ai-month-meter.js +89 -0
  4. package/src/lib/billing/ai-month-meter.js.map +1 -0
  5. package/src/lib/billing/ai-overage-close.d.ts +11 -33
  6. package/src/lib/billing/ai-overage-close.js +4 -3
  7. package/src/lib/billing/ai-overage-close.js.map +1 -1
  8. package/src/lib/billing/ai-overage-cutover.d.ts +28 -12
  9. package/src/lib/billing/ai-overage-cutover.js +1 -28
  10. package/src/lib/billing/ai-overage-cutover.js.map +1 -1
  11. package/src/lib/billing/ai-overage-ledger.js +1 -1
  12. package/src/lib/billing/ai-overage-ledger.js.map +1 -1
  13. package/src/lib/billing/ai-overage-standing.js +1 -1
  14. package/src/lib/billing/ai-overage-standing.js.map +1 -1
  15. package/src/lib/components/ai-allotment-editor.component.js +1 -1
  16. package/src/lib/components/ai-allotment-editor.component.js.map +1 -1
  17. package/src/lib/components/ai-assist-provider.component.js +3 -2
  18. package/src/lib/components/ai-assist-provider.component.js.map +1 -1
  19. package/src/lib/components/ai-automation-zones.d.ts +63 -0
  20. package/src/lib/components/ai-automation-zones.js +31 -0
  21. package/src/lib/components/ai-automation-zones.js.map +1 -0
  22. package/src/lib/components/ai-describe-automation.component.d.ts +1 -1
  23. package/src/lib/components/ai-describe-automation.component.js.map +1 -1
  24. package/src/lib/components/ai-describe-button.component.d.ts +6 -3
  25. package/src/lib/components/ai-describe-button.component.js +4 -1
  26. package/src/lib/components/ai-describe-button.component.js.map +1 -1
  27. package/src/lib/components/ai-explain-automation.component.d.ts +1 -1
  28. package/src/lib/components/ai-explain-automation.component.js.map +1 -1
  29. package/src/lib/components/billing-ai-allotments.component.js +1 -1
  30. package/src/lib/components/billing-ai-allotments.component.js.map +1 -1
  31. package/src/lib/components/billing-assist-overage-card.component.js +1 -1
  32. package/src/lib/components/billing-assist-overage-card.component.js.map +1 -1
  33. package/src/lib/constants.d.ts +5 -0
  34. package/src/lib/constants.js +4 -0
  35. package/src/lib/constants.js.map +1 -1
  36. package/src/lib/declarations.js +2 -0
  37. package/src/lib/declarations.js.map +1 -1
  38. package/src/lib/declarations.server.js +24 -10
  39. package/src/lib/declarations.server.js.map +1 -1
  40. package/src/lib/insights/ai-dataset-catalog.d.ts +49 -0
  41. package/src/lib/insights/ai-dataset-catalog.js +57 -0
  42. package/src/lib/insights/ai-dataset-catalog.js.map +1 -0
  43. package/src/lib/insights/ai-figure-readers.d.ts +6 -39
  44. package/src/lib/insights/ai-figure-readers.js +2 -391
  45. package/src/lib/insights/ai-figure-readers.js.map +1 -1
  46. package/src/lib/insights/ai-insight-digest.d.ts +10 -1
  47. package/src/lib/insights/ai-insight-digest.js +16 -4
  48. package/src/lib/insights/ai-insight-digest.js.map +1 -1
  49. package/src/lib/insights/ai-insight-readers.d.ts +1 -1
  50. package/src/lib/insights/ai-insight-readers.js.map +1 -1
  51. package/src/lib/jobs/ai-job-component-step.d.ts +6 -1
  52. package/src/lib/jobs/ai-job-component-step.js +23 -6
  53. package/src/lib/jobs/ai-job-component-step.js.map +1 -1
  54. package/src/lib/jobs/ai-job-draft-ids.js +11 -0
  55. package/src/lib/jobs/ai-job-draft-ids.js.map +1 -1
  56. package/src/lib/jobs/ai-job-email-step.d.ts +5 -1
  57. package/src/lib/jobs/ai-job-email-step.js +10 -1
  58. package/src/lib/jobs/ai-job-email-step.js.map +1 -1
  59. package/src/lib/jobs/ai-job-form-step.d.ts +17 -1
  60. package/src/lib/jobs/ai-job-form-step.js +60 -8
  61. package/src/lib/jobs/ai-job-form-step.js.map +1 -1
  62. package/src/lib/jobs/ai-job-generation.d.ts +21 -0
  63. package/src/lib/jobs/ai-job-generation.js +53 -1
  64. package/src/lib/jobs/ai-job-generation.js.map +1 -1
  65. package/src/lib/jobs/ai-job-insight-step.js +1 -1
  66. package/src/lib/jobs/ai-job-insight-step.js.map +1 -1
  67. package/src/lib/jobs/ai-job-layout-step.d.ts +15 -4
  68. package/src/lib/jobs/ai-job-layout-step.js +45 -12
  69. package/src/lib/jobs/ai-job-layout-step.js.map +1 -1
  70. package/src/lib/jobs/ai-job-plan-conformance.d.ts +5 -0
  71. package/src/lib/jobs/ai-job-plan-conformance.js +7 -1
  72. package/src/lib/jobs/ai-job-plan-conformance.js.map +1 -1
  73. package/src/lib/jobs/ai-job-plan-creations.d.ts +80 -0
  74. package/src/lib/jobs/ai-job-plan-creations.js +199 -0
  75. package/src/lib/jobs/ai-job-plan-creations.js.map +1 -0
  76. package/src/lib/jobs/ai-job-plan-step.d.ts +8 -2
  77. package/src/lib/jobs/ai-job-plan-step.js +20 -6
  78. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  79. package/src/lib/jobs/ai-job-site-step.d.ts +2 -0
  80. package/src/lib/jobs/ai-job-site-step.js +6 -6
  81. package/src/lib/jobs/ai-job-site-step.js.map +1 -1
  82. package/src/lib/jobs/ai-job-template-step.d.ts +9 -6
  83. package/src/lib/jobs/ai-job-template-step.js +72 -15
  84. package/src/lib/jobs/ai-job-template-step.js.map +1 -1
  85. package/src/lib/jobs/ai-jobs.js +1 -1
  86. package/src/lib/jobs/ai-jobs.js.map +1 -1
  87. package/src/lib/jobs/ai-workflow-records.d.ts +5 -4
  88. package/src/lib/jobs/ai-workflow-records.js +26 -22
  89. package/src/lib/jobs/ai-workflow-records.js.map +1 -1
  90. package/src/lib/model/ai-allotments.d.ts +16 -3
  91. package/src/lib/model/ai-allotments.js +34 -11
  92. package/src/lib/model/ai-allotments.js.map +1 -1
  93. package/src/lib/model/ai-job-creations.d.ts +55 -0
  94. package/src/lib/model/ai-job-creations.js +85 -0
  95. package/src/lib/model/ai-job-creations.js.map +1 -0
  96. package/src/lib/model/ai-page-job.d.ts +3 -2
  97. package/src/lib/model/ai-page-job.js +4 -3
  98. package/src/lib/model/ai-page-job.js.map +1 -1
  99. package/src/lib/model/ai-site-job.d.ts +4 -2
  100. package/src/lib/model/ai-site-job.js +8 -4
  101. package/src/lib/model/ai-site-job.js.map +1 -1
  102. package/src/lib/model/ai-template-subjects.d.ts +10 -0
  103. package/src/lib/model/ai-template-subjects.js +14 -0
  104. package/src/lib/model/ai-template-subjects.js.map +1 -1
  105. package/src/lib/model/ai-tokens.js +1 -1
  106. package/src/lib/model/ai-tokens.js.map +1 -1
  107. package/src/lib/model/ai-usage-by-user.js +1 -1
  108. package/src/lib/model/ai-usage-by-user.js.map +1 -1
  109. package/src/lib/providers/catalog.js +1 -1
  110. package/src/lib/providers/catalog.js.map +1 -1
  111. package/src/lib/providers/model-choice.js +1 -1
  112. package/src/lib/providers/model-choice.js.map +1 -1
  113. package/src/lib/runtime/ai-device-audit.d.ts +1 -1
  114. package/src/lib/runtime/ai-device-audit.js +1 -1
  115. package/src/lib/runtime/ai-device-audit.js.map +1 -1
  116. package/src/lib/runtime/ai-eval-live.js +1 -1
  117. package/src/lib/runtime/ai-eval-live.js.map +1 -1
  118. package/src/lib/runtime/ai-eval.d.ts +1 -1
  119. package/src/lib/runtime/ai-eval.js.map +1 -1
  120. package/src/lib/runtime/ai-gate.js +1 -1
  121. package/src/lib/runtime/ai-gate.js.map +1 -1
  122. package/src/lib/runtime/ai-palette.d.ts +1 -1
  123. package/src/lib/runtime/ai-palette.generated.d.ts +1 -1
  124. package/src/lib/runtime/ai-palette.generated.js +1 -1
  125. package/src/lib/runtime/ai-palette.generated.js.map +1 -1
  126. package/src/lib/runtime/ai-palette.js.map +1 -1
  127. package/src/lib/runtime/site-inventory.js +28 -22
  128. package/src/lib/runtime/site-inventory.js.map +1 -1
  129. package/src/lib/server/ai-admin-org.js +1 -1
  130. package/src/lib/server/ai-admin-org.js.map +1 -1
  131. package/src/lib/server/ai-admin-orgs-spend.js +1 -1
  132. package/src/lib/server/ai-admin-orgs-spend.js.map +1 -1
  133. package/src/lib/server/ai-admin-signals.js +1 -1
  134. package/src/lib/server/ai-admin-signals.js.map +1 -1
  135. package/src/lib/server/ai-allotments.js +1 -1
  136. package/src/lib/server/ai-allotments.js.map +1 -1
  137. package/src/lib/server/ai-assist.js +2 -2
  138. package/src/lib/server/ai-assist.js.map +1 -1
  139. package/src/lib/server/assist-chat.d.ts +1 -1
  140. package/src/lib/server/assist-chat.js +2 -2
  141. package/src/lib/server/assist-chat.js.map +1 -1
  142. package/src/lib/server/billing-credits.d.ts +5 -0
  143. package/src/lib/server/billing-credits.js +8 -2
  144. package/src/lib/server/billing-credits.js.map +1 -1
  145. package/src/lib/server/billing-overage.js +2 -2
  146. package/src/lib/server/billing-overage.js.map +1 -1
  147. package/src/lib/testing/stand-in-dataset-index.d.ts +20 -0
  148. package/src/lib/testing/stand-in-dataset-index.js +80 -0
  149. package/src/lib/testing/stand-in-dataset-index.js.map +1 -0
  150. package/src/lib/usage/ai-allotment-alerts.d.ts +10 -7
  151. package/src/lib/usage/ai-allotment-alerts.js +31 -14
  152. package/src/lib/usage/ai-allotment-alerts.js.map +1 -1
  153. package/src/lib/usage/ai-quota-checks.d.ts +37 -0
  154. package/src/lib/usage/ai-quota-checks.js +69 -0
  155. package/src/lib/usage/ai-quota-checks.js.map +1 -0
  156. package/src/lib/usage/ai-usage-alerts.js +22 -10
  157. package/src/lib/usage/ai-usage-alerts.js.map +1 -1
  158. package/src/lib/usage/ai-usage-by-user.js +1 -1
  159. package/src/lib/usage/ai-usage-by-user.js.map +1 -1
  160. package/src/lib/usage/assist-ceiling.d.ts +2 -2
  161. package/src/lib/usage/assist-ceiling.js +4 -4
  162. package/src/lib/usage/assist-ceiling.js.map +1 -1
  163. package/src/lib/usage/assist-credits.d.ts +494 -0
  164. package/src/lib/usage/assist-credits.js +524 -0
  165. package/src/lib/usage/assist-credits.js.map +1 -0
  166. package/src/lib/usage/assist-free-taste.d.ts +1 -1
  167. package/src/lib/usage/assist-free-taste.js +1 -1
  168. package/src/lib/usage/assist-free-taste.js.map +1 -1
  169. package/src/lib/usage/assist-signal-mining.js +1 -1
  170. package/src/lib/usage/assist-signal-mining.js.map +1 -1
  171. package/src/lib/usage/assist-spend-guards.d.ts +153 -0
  172. package/src/lib/usage/assist-spend-guards.js +182 -0
  173. package/src/lib/usage/assist-spend-guards.js.map +1 -0
  174. package/src/lib/usage/assist-usage.d.ts +1 -1
  175. package/src/lib/usage/assist-usage.js +1 -1
  176. package/src/lib/usage/assist-usage.js.map +1 -1
  177. package/src/lib/usage/staff-org-ai.js +1 -1
  178. package/src/lib/usage/staff-org-ai.js.map +1 -1
  179. package/src/lib/usage-axes.d.ts +10 -0
  180. package/src/lib/usage-axes.js +40 -2
  181. package/src/lib/usage-axes.js.map +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/runtime/ai-palette.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 type { JsonLinealOrder } from '@aglyn/aglyn/app-utils/lineal-order'\n\n/**\n * The element palette as a model sees it (AGL-2905).\n *\n * `ai-palette.generated.ts` is produced from the registered plugin bundles by\n * `tools/scripts/generate-ai-palette.mts` and holds the data; this module\n * holds the shapes that data is typed against and the small readers over it,\n * so a regeneration never rewrites a type.\n */\n\n/** The document kinds a model is asked to compose. */\nexport type AiSurface = 'screen' | 'email' | 'form' | 'layout' | 'component'\n\nexport const AI_SURFACE_NAMES: readonly AiSurface[] = [\n 'screen',\n 'email',\n 'form',\n 'layout',\n 'component',\n]\n\n/**\n * The subset of JSON Schema a prop declaration is expressed in. Enough to\n * validate what an attribute field can persist; nothing the field editor\n * cannot itself model.\n */\nexport interface AiPropSchema {\n type?: 'string' | 'number' | 'integer' | 'boolean'\n enum?: readonly string[]\n minLength?: number\n maxLength?: number\n minimum?: number\n maximum?: number\n pattern?: string\n description?: string\n}\n\nexport interface AiPropsSchema {\n type: 'object'\n properties: Record<string, AiPropSchema>\n required: readonly string[]\n additionalProperties: false\n}\n\n/**\n * What a string prop MEANS, which decides how the validator checks it.\n *\n * - `text` — copy the visitor reads; held to a length ceiling.\n * - `url` — a navigable address: an `https:` URL or a root-relative path.\n * - `screen` — a screen id, checked against the screens the host has.\n * - `media` — an image or video source: a DAM reference or an `https:` URL.\n */\nexport type AiPropRole = 'text' | 'url' | 'screen' | 'media'\n\n/**\n * A `restrictChildren` / `restrictParent` directive as generated data: the\n * same tuple the schema declares, with the flag spelled as the string the\n * enum holds so a JSON emitter can write it.\n */\nexport type AiLinealOrder = JsonLinealOrder\n\nexport interface AiPaletteEntry {\n pluginId: string\n kind: 'element' | 'plaintext' | 'markdown'\n category: string\n displayName: string\n /** One sentence, from the schema's own description. */\n summary: string\n /** Whether the editor lets anything be dropped inside this element. */\n acceptsChildren: boolean\n restrictChildren?: AiLinealOrder\n restrictParent?: AiLinealOrder\n propsSchema: AiPropsSchema\n /** Roles of the string props the validator treats specially. */\n propRoles: Record<string, AiPropRole>\n /**\n * The attribute field kind each declared prop is edited with, as its\n * `FieldComponentType` value (`switch`, `select`, `screen-select`, …): what\n * the editor reads to decide which component properties a field can be\n * bound to (AGL-2908).\n */\n propFields: Record<string, string>\n /** Length ceilings for `text` props, in characters. */\n textLimits: Record<string, number>\n /** Names of the presets that place this component. */\n presets: readonly string[]\n}\n\n/**\n * What every NODE can do, as against what one component declares (AGL-3156).\n *\n * Generated from the node capabilities core declares, beside the per-component\n * schemas rather than out of them: repeating is not a Stack's feature, so no\n * component schema names it and a palette built from schemas alone advertises\n * it on nothing. Declared ONCE for the whole palette — a model may write these\n * props on any element the surface allows.\n */\nexport interface AiNodeCapability {\n /** Stable id, as core declares it. */\n id: string\n displayName: string\n /** One sentence, for the prompt catalog. */\n summary: string\n propsSchema: AiPropsSchema\n propRoles: Record<string, AiPropRole>\n propFields: Record<string, string>\n textLimits: Record<string, number>\n /** Props offered only on a node that holds a child list. */\n childrenOnlyProps: readonly string[]\n /**\n * Declared attributes the palette deliberately does not offer: a picker\n * that chooses a record the site holds, which a model cannot name from a\n * description. Recorded so a field left off on purpose reads differently\n * from one that went missing.\n */\n omittedProps: readonly string[]\n}\n\n/**\n * The capability props a node with this child contract may carry, merged into\n * one map the prop sanitizer can read beside the element's own declarations.\n */\nexport function nodeCapabilityProps(\n capabilities: Readonly<Record<string, AiNodeCapability>>,\n acceptsChildren: boolean,\n): {\n properties: Record<string, AiPropSchema>\n propRoles: Record<string, AiPropRole>\n textLimits: Record<string, number>\n} {\n const properties: Record<string, AiPropSchema> = {}\n const propRoles: Record<string, AiPropRole> = {}\n const textLimits: Record<string, number> = {}\n for (const capability of Object.values(capabilities)) {\n for (const [name, schema] of Object.entries(\n capability.propsSchema.properties,\n )) {\n if (!acceptsChildren && capability.childrenOnlyProps.includes(name))\n continue\n properties[name] = schema\n const role = capability.propRoles[name]\n if (role) propRoles[name] = role\n const limit = capability.textLimits[name]\n if (limit !== undefined) textLimits[name] = limit\n }\n }\n return { properties, propRoles, textLimits }\n}\n\n/**\n * What a node capability declared in core and one carried by the palette\n * disagree about (AGL-3156).\n *\n * `check:ai-palette` pins the generated file to the generator's output for its\n * inputs, which cannot notice a capability leaving those inputs: when repeat\n * stopped being a component's attribute, the generator regenerated cleanly and\n * the palette simply stopped mentioning it. This reads the core declarations\n * instead and asks whether the palette still accounts for each one, so the\n * next generic capability cannot vanish the same way.\n *\n * `offeredFieldKinds` is every field kind the palette DOES carry somewhere,\n * derived from the generated entries rather than restated here: a capability\n * attribute drawn with one of those kinds has no excuse for being omitted.\n */\nexport function auditNodeCapabilities(\n declared: ReadonlyArray<{\n id: string\n label: string\n attributes: ReadonlyArray<{ name: string; component: string }>\n }>,\n carried: Readonly<Record<string, AiNodeCapability>>,\n offeredFieldKinds: ReadonlySet<string>,\n): string[] {\n const problems: string[] = []\n for (const capability of declared) {\n const entry = carried[capability.id]\n if (!entry) {\n problems.push(\n `the palette carries no node capability \"${capability.id}\": a model cannot use it at all`,\n )\n continue\n }\n if (!Object.keys(entry.propsSchema.properties).length) {\n problems.push(\n `node capability \"${capability.id}\" is carried with no prop a model can write`,\n )\n }\n for (const attribute of capability.attributes) {\n if (entry.propsSchema.properties[attribute.name]) continue\n if (!entry.omittedProps.includes(attribute.name)) {\n problems.push(\n `node capability \"${capability.id}\" declares \"${attribute.name}\", which the palette neither offers nor records as omitted`,\n )\n continue\n }\n if (offeredFieldKinds.has(attribute.component)) {\n problems.push(\n `node capability \"${capability.id}\" omits \"${attribute.name}\", whose \"${attribute.component}\" field the palette offers elsewhere`,\n )\n }\n }\n }\n for (const id of Object.keys(carried)) {\n if (!declared.some((capability) => capability.id === id)) {\n problems.push(\n `the palette carries node capability \"${id}\", which core no longer declares`,\n )\n }\n }\n return problems\n}\n\nexport interface AiSurfaceDefinition {\n /** The component id at the top of a tree for this surface. */\n root: string\n /** Every component id a node in this surface may carry. */\n allow: readonly string[]\n}\n\n/**\n * Theme vocabulary an `sx` value may name, read off the theme sources so the\n * validator's allowlist is derived rather than typed by hand.\n */\nexport interface AiSxTokens {\n /** Dotted palette paths such as `primary.main`. */\n palette: readonly string[]\n /** Responsive object keys. */\n breakpoints: readonly string[]\n /** Values `typography` may take. */\n typographyVariants: readonly string[]\n}\n\n/**\n * Text ceilings per role of copy (AGL-2905). A headline is one line of a\n * hero, body copy is a paragraph or two, and a button label has to fit a\n * button.\n */\nexport const AI_TEXT_LIMITS = {\n headline: 120,\n body: 2_000,\n button: 40,\n /** A short label, title or caption. */\n label: 200,\n} as const\n\n/**\n * Prompt budget per surface catalog, in characters at four per token.\n * Asserted by a spec so a component that grows its description cannot\n * silently push a system prompt past what the route pays for.\n */\nexport const AI_PALETTE_CATALOG_MAX_TOKENS = 2_500\nexport const AI_PALETTE_CATALOG_MAX_CHARS = AI_PALETTE_CATALOG_MAX_TOKENS * 4\n\n/** The model's own token estimate: characters over four. */\nexport function estimateCatalogTokens(catalog: string): number {\n return Math.ceil(catalog.length / 4)\n}\n\n/**\n * The documents a generator emits (AGL-2935). Each is composed on one\n * surface of the palette and held to its own budget below.\n */\nexport type AiOutputKind =\n | 'page'\n | 'template'\n | 'component'\n | 'layout'\n | 'form'\n | 'email'\n\nexport const AI_OUTPUT_KINDS: readonly AiOutputKind[] = [\n 'page',\n 'template',\n 'component',\n 'layout',\n 'form',\n 'email',\n]\n\n/** The surface each output kind is composed on. */\nexport const AI_OUTPUT_SURFACE: Record<AiOutputKind, AiSurface> = {\n page: 'screen',\n template: 'screen',\n component: 'component',\n layout: 'layout',\n form: 'form',\n email: 'email',\n}\n\n/**\n * What the rule-17 scorer measures on an emitted tree:\n *\n * - `nodes` — how many elements the tree carries;\n * - `bytes` — the stored node map, in the unit the save ceiling uses\n * (`nodeMapBytes`, msgpack);\n * - `imageBytes` — what the placed library images request at the slot's\n * variant, from each asset's recorded size;\n * - `embeds` — third-party players and frames the output loads;\n * - `fontFamilies` — families named beyond the theme's own;\n * - `emailHtmlBytes` — an email's rendered HTML, which is what a mail\n * client clips.\n */\nexport type AiBudgetMetric =\n | 'nodes'\n | 'bytes'\n | 'imageBytes'\n | 'embeds'\n | 'fontFamilies'\n | 'emailHtmlBytes'\n\nexport type AiOutputBudget = Record<Exclude<AiBudgetMetric, 'emailHtmlBytes'>, number> & {\n emailHtmlBytes?: number\n}\n\n/**\n * The size past which Gmail clips a message and hides the rest behind a\n * \"View entire message\" link — the unsubscribe footer included.\n */\nexport const AI_EMAIL_CLIP_BYTES = 102_000\n\n/**\n * The budget per output kind (AGL-2935, rule 17), beside the catalog budget\n * above for the same reason: a generator that grows its output past what\n * its kind is for is caught by a number, not noticed by a reader.\n *\n * A page is a few hundred elements and tens of kilobytes of stored map; a\n * reusable component is one repeated block; a layout is the chrome every\n * page loads, so it carries the least imagery; a form is fields and a\n * button; an email is held to the clipping threshold on its rendered HTML.\n * No kind names a font beyond the theme's.\n *\n * A third-party player is a page's or a component's to carry, and only where\n * a confirmed plan lists it (rule 16). A template is rendered for every\n * record it serves and a layout around every page, so neither carries one\n * (AGL-3433): an embed there loads a stranger's code on every page of a\n * collection or a site.\n */\nexport const AI_OUTPUT_BUDGETS: Record<AiOutputKind, AiOutputBudget> = {\n page: {\n nodes: 400,\n bytes: 60_000,\n imageBytes: 1_500_000,\n embeds: 1,\n fontFamilies: 0,\n },\n template: {\n nodes: 400,\n bytes: 60_000,\n imageBytes: 1_500_000,\n embeds: 0,\n fontFamilies: 0,\n },\n component: {\n nodes: 80,\n bytes: 12_000,\n imageBytes: 500_000,\n embeds: 1,\n fontFamilies: 0,\n },\n layout: {\n nodes: 160,\n bytes: 24_000,\n imageBytes: 200_000,\n embeds: 0,\n fontFamilies: 0,\n },\n form: {\n nodes: 60,\n bytes: 8_000,\n imageBytes: 0,\n embeds: 0,\n fontFamilies: 0,\n },\n email: {\n nodes: 150,\n bytes: 24_000,\n imageBytes: 1_000_000,\n embeds: 0,\n fontFamilies: 0,\n emailHtmlBytes: AI_EMAIL_CLIP_BYTES,\n },\n}\n\n/**\n * What a first visit to a generated output is estimated to transfer — the\n * weight the proposal shows before anything is applied (rule 17). A page\n * starts from the platform's measured page weight; a component or a layout\n * reports only what it adds to a page; an email is not a page load.\n */\nexport interface AiLoadEstimate {\n /** The measured weight of a published page before this output's own bytes; 0 for a fragment. */\n pageBytes: number\n /** The stored node map, in the save's own bytes. */\n documentBytes: number\n /** What the placed library images request at their slots. */\n imageBytes: number\n /** Images the estimate could not size: no recorded size, or not from the library. */\n imagesUnmeasured: number\n embeds: number\n totalBytes: number\n}\n\n/** Typography variants that read as headlines, for the `children` ceiling. */\nexport function isHeadlineVariant(variant: unknown): boolean {\n return typeof variant === 'string' && /^(display\\w*|h[1-6]|heading)$/.test(variant)\n}\n"],"names":["AI_SURFACE_NAMES","nodeCapabilityProps","capabilities","acceptsChildren","properties","propRoles","textLimits","capability","Object","values","name","schema","entries","propsSchema","childrenOnlyProps","includes","role","limit","undefined","auditNodeCapabilities","declared","carried","offeredFieldKinds","problems","entry","id","push","keys","length","attribute","attributes","omittedProps","has","component","some","AI_TEXT_LIMITS","headline","body","button","label","AI_PALETTE_CATALOG_MAX_TOKENS","AI_PALETTE_CATALOG_MAX_CHARS","estimateCatalogTokens","catalog","Math","ceil","AI_OUTPUT_KINDS","AI_OUTPUT_SURFACE","page","template","layout","form","email","AI_EMAIL_CLIP_BYTES","AI_OUTPUT_BUDGETS","nodes","bytes","imageBytes","embeds","fontFamilies","emailHtmlBytes","isHeadlineVariant","variant","test"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/runtime/ai-palette.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 type { JsonLinealOrder } from '@aglyn/aglyn/app-utils/lineal-order'\n\n/**\n * The element palette as a model sees it (AGL-2905).\n *\n * `ai-palette.generated.ts` is produced from the registered plugin bundles by\n * `libs/plugins/ai/scripts/generate-ai-palette.mts` and holds the data; this module\n * holds the shapes that data is typed against and the small readers over it,\n * so a regeneration never rewrites a type.\n */\n\n/** The document kinds a model is asked to compose. */\nexport type AiSurface = 'screen' | 'email' | 'form' | 'layout' | 'component'\n\nexport const AI_SURFACE_NAMES: readonly AiSurface[] = [\n 'screen',\n 'email',\n 'form',\n 'layout',\n 'component',\n]\n\n/**\n * The subset of JSON Schema a prop declaration is expressed in. Enough to\n * validate what an attribute field can persist; nothing the field editor\n * cannot itself model.\n */\nexport interface AiPropSchema {\n type?: 'string' | 'number' | 'integer' | 'boolean'\n enum?: readonly string[]\n minLength?: number\n maxLength?: number\n minimum?: number\n maximum?: number\n pattern?: string\n description?: string\n}\n\nexport interface AiPropsSchema {\n type: 'object'\n properties: Record<string, AiPropSchema>\n required: readonly string[]\n additionalProperties: false\n}\n\n/**\n * What a string prop MEANS, which decides how the validator checks it.\n *\n * - `text` — copy the visitor reads; held to a length ceiling.\n * - `url` — a navigable address: an `https:` URL or a root-relative path.\n * - `screen` — a screen id, checked against the screens the host has.\n * - `media` — an image or video source: a DAM reference or an `https:` URL.\n */\nexport type AiPropRole = 'text' | 'url' | 'screen' | 'media'\n\n/**\n * A `restrictChildren` / `restrictParent` directive as generated data: the\n * same tuple the schema declares, with the flag spelled as the string the\n * enum holds so a JSON emitter can write it.\n */\nexport type AiLinealOrder = JsonLinealOrder\n\nexport interface AiPaletteEntry {\n pluginId: string\n kind: 'element' | 'plaintext' | 'markdown'\n category: string\n displayName: string\n /** One sentence, from the schema's own description. */\n summary: string\n /** Whether the editor lets anything be dropped inside this element. */\n acceptsChildren: boolean\n restrictChildren?: AiLinealOrder\n restrictParent?: AiLinealOrder\n propsSchema: AiPropsSchema\n /** Roles of the string props the validator treats specially. */\n propRoles: Record<string, AiPropRole>\n /**\n * The attribute field kind each declared prop is edited with, as its\n * `FieldComponentType` value (`switch`, `select`, `screen-select`, …): what\n * the editor reads to decide which component properties a field can be\n * bound to (AGL-2908).\n */\n propFields: Record<string, string>\n /** Length ceilings for `text` props, in characters. */\n textLimits: Record<string, number>\n /** Names of the presets that place this component. */\n presets: readonly string[]\n}\n\n/**\n * What every NODE can do, as against what one component declares (AGL-3156).\n *\n * Generated from the node capabilities core declares, beside the per-component\n * schemas rather than out of them: repeating is not a Stack's feature, so no\n * component schema names it and a palette built from schemas alone advertises\n * it on nothing. Declared ONCE for the whole palette — a model may write these\n * props on any element the surface allows.\n */\nexport interface AiNodeCapability {\n /** Stable id, as core declares it. */\n id: string\n displayName: string\n /** One sentence, for the prompt catalog. */\n summary: string\n propsSchema: AiPropsSchema\n propRoles: Record<string, AiPropRole>\n propFields: Record<string, string>\n textLimits: Record<string, number>\n /** Props offered only on a node that holds a child list. */\n childrenOnlyProps: readonly string[]\n /**\n * Declared attributes the palette deliberately does not offer: a picker\n * that chooses a record the site holds, which a model cannot name from a\n * description. Recorded so a field left off on purpose reads differently\n * from one that went missing.\n */\n omittedProps: readonly string[]\n}\n\n/**\n * The capability props a node with this child contract may carry, merged into\n * one map the prop sanitizer can read beside the element's own declarations.\n */\nexport function nodeCapabilityProps(\n capabilities: Readonly<Record<string, AiNodeCapability>>,\n acceptsChildren: boolean,\n): {\n properties: Record<string, AiPropSchema>\n propRoles: Record<string, AiPropRole>\n textLimits: Record<string, number>\n} {\n const properties: Record<string, AiPropSchema> = {}\n const propRoles: Record<string, AiPropRole> = {}\n const textLimits: Record<string, number> = {}\n for (const capability of Object.values(capabilities)) {\n for (const [name, schema] of Object.entries(\n capability.propsSchema.properties,\n )) {\n if (!acceptsChildren && capability.childrenOnlyProps.includes(name))\n continue\n properties[name] = schema\n const role = capability.propRoles[name]\n if (role) propRoles[name] = role\n const limit = capability.textLimits[name]\n if (limit !== undefined) textLimits[name] = limit\n }\n }\n return { properties, propRoles, textLimits }\n}\n\n/**\n * What a node capability declared in core and one carried by the palette\n * disagree about (AGL-3156).\n *\n * `check:ai-palette` pins the generated file to the generator's output for its\n * inputs, which cannot notice a capability leaving those inputs: when repeat\n * stopped being a component's attribute, the generator regenerated cleanly and\n * the palette simply stopped mentioning it. This reads the core declarations\n * instead and asks whether the palette still accounts for each one, so the\n * next generic capability cannot vanish the same way.\n *\n * `offeredFieldKinds` is every field kind the palette DOES carry somewhere,\n * derived from the generated entries rather than restated here: a capability\n * attribute drawn with one of those kinds has no excuse for being omitted.\n */\nexport function auditNodeCapabilities(\n declared: ReadonlyArray<{\n id: string\n label: string\n attributes: ReadonlyArray<{ name: string; component: string }>\n }>,\n carried: Readonly<Record<string, AiNodeCapability>>,\n offeredFieldKinds: ReadonlySet<string>,\n): string[] {\n const problems: string[] = []\n for (const capability of declared) {\n const entry = carried[capability.id]\n if (!entry) {\n problems.push(\n `the palette carries no node capability \"${capability.id}\": a model cannot use it at all`,\n )\n continue\n }\n if (!Object.keys(entry.propsSchema.properties).length) {\n problems.push(\n `node capability \"${capability.id}\" is carried with no prop a model can write`,\n )\n }\n for (const attribute of capability.attributes) {\n if (entry.propsSchema.properties[attribute.name]) continue\n if (!entry.omittedProps.includes(attribute.name)) {\n problems.push(\n `node capability \"${capability.id}\" declares \"${attribute.name}\", which the palette neither offers nor records as omitted`,\n )\n continue\n }\n if (offeredFieldKinds.has(attribute.component)) {\n problems.push(\n `node capability \"${capability.id}\" omits \"${attribute.name}\", whose \"${attribute.component}\" field the palette offers elsewhere`,\n )\n }\n }\n }\n for (const id of Object.keys(carried)) {\n if (!declared.some((capability) => capability.id === id)) {\n problems.push(\n `the palette carries node capability \"${id}\", which core no longer declares`,\n )\n }\n }\n return problems\n}\n\nexport interface AiSurfaceDefinition {\n /** The component id at the top of a tree for this surface. */\n root: string\n /** Every component id a node in this surface may carry. */\n allow: readonly string[]\n}\n\n/**\n * Theme vocabulary an `sx` value may name, read off the theme sources so the\n * validator's allowlist is derived rather than typed by hand.\n */\nexport interface AiSxTokens {\n /** Dotted palette paths such as `primary.main`. */\n palette: readonly string[]\n /** Responsive object keys. */\n breakpoints: readonly string[]\n /** Values `typography` may take. */\n typographyVariants: readonly string[]\n}\n\n/**\n * Text ceilings per role of copy (AGL-2905). A headline is one line of a\n * hero, body copy is a paragraph or two, and a button label has to fit a\n * button.\n */\nexport const AI_TEXT_LIMITS = {\n headline: 120,\n body: 2_000,\n button: 40,\n /** A short label, title or caption. */\n label: 200,\n} as const\n\n/**\n * Prompt budget per surface catalog, in characters at four per token.\n * Asserted by a spec so a component that grows its description cannot\n * silently push a system prompt past what the route pays for.\n */\nexport const AI_PALETTE_CATALOG_MAX_TOKENS = 2_500\nexport const AI_PALETTE_CATALOG_MAX_CHARS = AI_PALETTE_CATALOG_MAX_TOKENS * 4\n\n/** The model's own token estimate: characters over four. */\nexport function estimateCatalogTokens(catalog: string): number {\n return Math.ceil(catalog.length / 4)\n}\n\n/**\n * The documents a generator emits (AGL-2935). Each is composed on one\n * surface of the palette and held to its own budget below.\n */\nexport type AiOutputKind =\n | 'page'\n | 'template'\n | 'component'\n | 'layout'\n | 'form'\n | 'email'\n\nexport const AI_OUTPUT_KINDS: readonly AiOutputKind[] = [\n 'page',\n 'template',\n 'component',\n 'layout',\n 'form',\n 'email',\n]\n\n/** The surface each output kind is composed on. */\nexport const AI_OUTPUT_SURFACE: Record<AiOutputKind, AiSurface> = {\n page: 'screen',\n template: 'screen',\n component: 'component',\n layout: 'layout',\n form: 'form',\n email: 'email',\n}\n\n/**\n * What the rule-17 scorer measures on an emitted tree:\n *\n * - `nodes` — how many elements the tree carries;\n * - `bytes` — the stored node map, in the unit the save ceiling uses\n * (`nodeMapBytes`, msgpack);\n * - `imageBytes` — what the placed library images request at the slot's\n * variant, from each asset's recorded size;\n * - `embeds` — third-party players and frames the output loads;\n * - `fontFamilies` — families named beyond the theme's own;\n * - `emailHtmlBytes` — an email's rendered HTML, which is what a mail\n * client clips.\n */\nexport type AiBudgetMetric =\n | 'nodes'\n | 'bytes'\n | 'imageBytes'\n | 'embeds'\n | 'fontFamilies'\n | 'emailHtmlBytes'\n\nexport type AiOutputBudget = Record<Exclude<AiBudgetMetric, 'emailHtmlBytes'>, number> & {\n emailHtmlBytes?: number\n}\n\n/**\n * The size past which Gmail clips a message and hides the rest behind a\n * \"View entire message\" link — the unsubscribe footer included.\n */\nexport const AI_EMAIL_CLIP_BYTES = 102_000\n\n/**\n * The budget per output kind (AGL-2935, rule 17), beside the catalog budget\n * above for the same reason: a generator that grows its output past what\n * its kind is for is caught by a number, not noticed by a reader.\n *\n * A page is a few hundred elements and tens of kilobytes of stored map; a\n * reusable component is one repeated block; a layout is the chrome every\n * page loads, so it carries the least imagery; a form is fields and a\n * button; an email is held to the clipping threshold on its rendered HTML.\n * No kind names a font beyond the theme's.\n *\n * A third-party player is a page's or a component's to carry, and only where\n * a confirmed plan lists it (rule 16). A template is rendered for every\n * record it serves and a layout around every page, so neither carries one\n * (AGL-3433): an embed there loads a stranger's code on every page of a\n * collection or a site.\n */\nexport const AI_OUTPUT_BUDGETS: Record<AiOutputKind, AiOutputBudget> = {\n page: {\n nodes: 400,\n bytes: 60_000,\n imageBytes: 1_500_000,\n embeds: 1,\n fontFamilies: 0,\n },\n template: {\n nodes: 400,\n bytes: 60_000,\n imageBytes: 1_500_000,\n embeds: 0,\n fontFamilies: 0,\n },\n component: {\n nodes: 80,\n bytes: 12_000,\n imageBytes: 500_000,\n embeds: 1,\n fontFamilies: 0,\n },\n layout: {\n nodes: 160,\n bytes: 24_000,\n imageBytes: 200_000,\n embeds: 0,\n fontFamilies: 0,\n },\n form: {\n nodes: 60,\n bytes: 8_000,\n imageBytes: 0,\n embeds: 0,\n fontFamilies: 0,\n },\n email: {\n nodes: 150,\n bytes: 24_000,\n imageBytes: 1_000_000,\n embeds: 0,\n fontFamilies: 0,\n emailHtmlBytes: AI_EMAIL_CLIP_BYTES,\n },\n}\n\n/**\n * What a first visit to a generated output is estimated to transfer — the\n * weight the proposal shows before anything is applied (rule 17). A page\n * starts from the platform's measured page weight; a component or a layout\n * reports only what it adds to a page; an email is not a page load.\n */\nexport interface AiLoadEstimate {\n /** The measured weight of a published page before this output's own bytes; 0 for a fragment. */\n pageBytes: number\n /** The stored node map, in the save's own bytes. */\n documentBytes: number\n /** What the placed library images request at their slots. */\n imageBytes: number\n /** Images the estimate could not size: no recorded size, or not from the library. */\n imagesUnmeasured: number\n embeds: number\n totalBytes: number\n}\n\n/** Typography variants that read as headlines, for the `children` ceiling. */\nexport function isHeadlineVariant(variant: unknown): boolean {\n return typeof variant === 'string' && /^(display\\w*|h[1-6]|heading)$/.test(variant)\n}\n"],"names":["AI_SURFACE_NAMES","nodeCapabilityProps","capabilities","acceptsChildren","properties","propRoles","textLimits","capability","Object","values","name","schema","entries","propsSchema","childrenOnlyProps","includes","role","limit","undefined","auditNodeCapabilities","declared","carried","offeredFieldKinds","problems","entry","id","push","keys","length","attribute","attributes","omittedProps","has","component","some","AI_TEXT_LIMITS","headline","body","button","label","AI_PALETTE_CATALOG_MAX_TOKENS","AI_PALETTE_CATALOG_MAX_CHARS","estimateCatalogTokens","catalog","Math","ceil","AI_OUTPUT_KINDS","AI_OUTPUT_SURFACE","page","template","layout","form","email","AI_EMAIL_CLIP_BYTES","AI_OUTPUT_BUDGETS","nodes","bytes","imageBytes","embeds","fontFamilies","emailHtmlBytes","isHeadlineVariant","variant","test"],"mappings":"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"}
@@ -15,14 +15,13 @@ 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 { hostCollectionKind } from "@aglyn/aglyn/app-utils/collection-kind";
18
- import { effectiveDatasetModel } from "@aglyn/aglyn/app-utils/dataset-models";
19
- import { datasetDisplayName } from "@aglyn/aglyn/app-utils/datasets";
20
18
  import { isFormArchived } from "@aglyn/aglyn/app-utils/forms";
21
19
  import { describeTheme, resolveSiteTheme } from "@aglyn/aglyn/app-utils/site-theme";
22
20
  import { REUSABLE_COMPONENT_KIND_EMAIL, reusableComponentKindOf } from "@aglyn/aglyn/app-utils/reusable-component-kind";
23
21
  import { SCREEN_KIND_TEMPLATE } from "@aglyn/aglyn/app-utils/screen-route";
22
+ import { pluginRecordIndex } from "@aglyn/aglyn/plugin-manager/plugin-record-index";
24
23
  import { firebaseAdmin } from "@aglyn/tenant-data-admin/server/firebase-admin";
25
- import { resolveOrgIdForHost, scopedToHost } from "@aglyn/tenant-data-admin/server/organizations";
24
+ import { resolveOrgIdForHost } from "@aglyn/tenant-data-admin/server/organizations";
26
25
  import { AI_INVENTORY_KINDS, AI_SITE_INVENTORY_MAX_PER_KIND, AiInventoryScopeError } from "../model/ai-site-inventory.js";
27
26
  import { aiBracketedFacts } from "./ai-doctrine-validators.js";
28
27
  function text(value) {
@@ -80,6 +79,31 @@ async function readWindow(query, fields, cap, map) {
80
79
  truncated: snapshot.docs.length > cap
81
80
  };
82
81
  }
82
+ /**
83
+ * The org's datasets shared with the site, through the `dataset` index the
84
+ * data plugin publishes (AGL-3080) rather than its collection: their names
85
+ * and their fields' names, in the dataset page's order. None where no plugin
86
+ * keeps datasets here.
87
+ */ async function readIndexedDatasets(orgId, hostId, cap) {
88
+ const datasets = pluginRecordIndex('dataset');
89
+ if (!datasets) return {
90
+ rows: [],
91
+ truncated: false
92
+ };
93
+ const { records, truncated } = await datasets.index.list({
94
+ orgId,
95
+ hostId,
96
+ limit: cap
97
+ });
98
+ return {
99
+ rows: records.map((record)=>({
100
+ id: record.id,
101
+ name: record.name,
102
+ fields: (Array.isArray(record.facts['fields']) ? record.facts['fields'] : []).map((field)=>text(field == null ? void 0 : field['name']) || text(field == null ? void 0 : field['id'])).filter(Boolean)
103
+ })),
104
+ truncated
105
+ };
106
+ }
83
107
  /**
84
108
  * Read one site's inventory for a generation.
85
109
  *
@@ -161,25 +185,7 @@ async function readWindow(query, fields, cap, map) {
161
185
  name: nameOf(data, id, 'displayName', 'slug'),
162
186
  fields: (Array.isArray(data['fields']) ? data['fields'] : []).map((field)=>text(field == null ? void 0 : field['fieldName'])).filter(Boolean)
163
187
  }),
164
- readWindow(scopedToHost(firestore.collection('orgs').doc(orgId).collection('datasets'), hostId), [
165
- 'displayName',
166
- 'name',
167
- 'fields',
168
- 'model',
169
- 'deletedAt'
170
- ], cap, (id, data)=>{
171
- if (data['deletedAt']) return null;
172
- const model = effectiveDatasetModel(data);
173
- const fields = model.order.map((fieldId)=>{
174
- var _model_fields_fieldId;
175
- return text((_model_fields_fieldId = model.fields[fieldId]) == null ? void 0 : _model_fields_fieldId.name) || fieldId;
176
- }).filter(Boolean);
177
- return {
178
- id,
179
- name: datasetDisplayName(data) || id,
180
- fields
181
- };
182
- }),
188
+ readIndexedDatasets(orgId, hostId, cap),
183
189
  readWindow(host.collection('collections'), [
184
190
  'displayName',
185
191
  'name',
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/runtime/site-inventory.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 { hostCollectionKind } from '@aglyn/aglyn/app-utils/collection-kind'\nimport { effectiveDatasetModel } from '@aglyn/aglyn/app-utils/dataset-models'\nimport { datasetDisplayName } from '@aglyn/aglyn/app-utils/datasets'\nimport { isFormArchived } from '@aglyn/aglyn/app-utils/forms'\nimport {\n describeTheme,\n resolveSiteTheme,\n type ThemeHostDocument,\n} from '@aglyn/aglyn/app-utils/site-theme'\nimport {\n REUSABLE_COMPONENT_KIND_EMAIL,\n reusableComponentKindOf,\n} from '@aglyn/aglyn/app-utils/reusable-component-kind'\nimport { SCREEN_KIND_TEMPLATE } from '@aglyn/aglyn/app-utils/screen-route'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin/server/firebase-admin'\nimport {\n resolveOrgIdForHost,\n scopedToHost,\n} from '@aglyn/tenant-data-admin/server/organizations'\nimport {\n AI_INVENTORY_KINDS,\n AI_SITE_INVENTORY_MAX_PER_KIND,\n AiInventoryScopeError,\n type AiComponentPropTypes,\n type AiInventoryCollection,\n type AiInventoryComponent,\n type AiInventoryDataset,\n type AiInventoryForm,\n type AiInventoryKind,\n type AiInventoryLayout,\n type AiInventoryScreen,\n type AiInventoryTemplate,\n type AiInventoryTheme,\n type AiSiteInventory,\n} from '../model/ai-site-inventory'\nimport { aiBracketedFacts } from './ai-doctrine-validators'\n\n/**\n * The reader behind the site inventory (AGL-2935): one site's components,\n * layouts, templates, forms, datasets, content collections, screens and\n * theme, read as projections through the Admin SDK and reduced to the\n * compact shape `model/ai-site-inventory.ts` describes.\n *\n * Every kind is a projection (`select`) of the fields the inventory names —\n * never a node map, never a record's content — read one row past the cap so\n * a full page is told apart from a cut one. Retired rows (soft-deleted,\n * archived, a component never published) are filtered after the read, so a\n * site with many retired rows can list fewer than the cap; the kind is still\n * marked truncated when the read was cut, which is the honest answer.\n *\n * The window is WIDER than the prompt lists (AGL-2937). The prompt block\n * lists `AI_SITE_INVENTORY_LISTED_PER_KIND` records of a kind because every\n * character of it is billed on every attempt; the rest of the window is what\n * the lookup tool searches, and a record never read cannot be found. The\n * difference is paid in document reads rather than tokens — a projection of\n * a few fields, once per job step — which is the cheaper of the two by some\n * orders of magnitude.\n */\n\ntype Firestore = FirebaseFirestore.Firestore\ntype Data = Record<string, unknown>\n\nexport interface ReadSiteInventoryOptions {\n /** The Admin SDK handle; the platform's default app when absent. */\n firestore?: Firestore\n /** Rows read per kind; `AI_SITE_INVENTORY_MAX_PER_KIND` when absent, and never above it. */\n maxPerKind?: number\n}\n\nfunction text(value: unknown): string {\n return typeof value === 'string' ? value.trim() : ''\n}\n\n/** A record's name, or its id when it was saved without one. */\nfunction nameOf(data: Data, id: string, ...fields: string[]): string {\n for (const field of fields) {\n const value = text(data[field])\n if (value) return value\n }\n return id\n}\n\n/** The flat `path → color` leaves of one scheme, e.g. `primary.main`. */\nfunction schemeColors(scheme: unknown): Record<string, string> {\n const colors: Record<string, string> = {}\n const visit = (value: unknown, path: string) => {\n if (typeof value === 'string') {\n if (path) colors[path] = value\n return\n }\n if (!value || typeof value !== 'object' || Array.isArray(value)) return\n for (const [key, inner] of Object.entries(value as Data)) {\n visit(inner, path ? `${path}.${key}` : key)\n }\n }\n visit(scheme, '')\n return colors\n}\n\n/** The theme a site renders with, reduced to what a plan and an email can act on. */\nexport function aiInventoryTheme(host: Data | null): AiInventoryTheme | null {\n const theme = resolveSiteTheme(host as ThemeHostDocument | null)\n if (!theme) return null\n const fonts = new Set<string>()\n for (const font of theme.fonts ?? []) {\n if (text(font?.family)) fonts.add(text(font.family))\n }\n return {\n summary: describeTheme(theme),\n colors: schemeColors(theme.colorSchemes?.light),\n fonts: [...fonts],\n }\n}\n\ninterface Window<T> {\n rows: T[]\n truncated: boolean\n}\n\nasync function readWindow<T>(\n query: FirebaseFirestore.Query,\n fields: string[],\n cap: number,\n map: (id: string, data: Data) => T | null,\n): Promise<Window<T>> {\n const snapshot = await query\n .select(...fields)\n .limit(cap + 1)\n .get()\n const rows: T[] = []\n for (const doc of snapshot.docs.slice(0, cap)) {\n const row = map(doc.id, (doc.data() ?? {}) as Data)\n if (row) rows.push(row)\n }\n return { rows, truncated: snapshot.docs.length > cap }\n}\n\n/**\n * Read one site's inventory for a generation.\n *\n * SCOPED before anything is read. The job names a site and an org, and\n * nothing the reader is handed proves the site belongs to the org, so it asks\n * the host index first and refuses a pairing it does not record: another\n * workspace's component names are not this workspace's prompt. Datasets are\n * org-owned and narrowed to what this site may see.\n *\n * Throws `AiInventoryScopeError` for a site outside the org, and lets a\n * Firestore failure propagate: a planner handed an empty inventory by\n * mistake would create everything the site already has.\n */\nexport async function readSiteInventory(\n orgId: string,\n hostId: string,\n options: ReadSiteInventoryOptions = {},\n): Promise<AiSiteInventory> {\n const owner = await resolveOrgIdForHost(hostId)\n if (!owner || owner !== orgId) throw new AiInventoryScopeError(hostId)\n const firestore = options.firestore ?? firebaseAdmin.app().firestore()\n const cap = Math.min(\n AI_SITE_INVENTORY_MAX_PER_KIND,\n Math.max(1, Math.floor(options.maxPerKind ?? AI_SITE_INVENTORY_MAX_PER_KIND)),\n )\n const host = firestore.collection('hosts').doc(hostId)\n\n const [components, layouts, templates, forms, datasets, collections, screens, hostDoc] =\n await Promise.all([\n readWindow<AiInventoryComponent>(\n host.collection('components'),\n ['displayName', 'props', 'rootId', 'versionId', 'deletedAt', 'kind'],\n cap,\n (id, data) => {\n // Soft-deleted, or never published: an instance of either renders nothing.\n if (data['deletedAt'] || !(data['rootId'] || data['versionId'])) return null\n // An email block (AGL-3287) is built from email elements and placed\n // in emails alone: no page, layout or component a job builds can\n // render one, and the email a job builds places no instances.\n if (reusableComponentKindOf(data) === REUSABLE_COMPONENT_KIND_EMAIL) return null\n const props: AiComponentPropTypes = {}\n const bracketedDefaults: Record<string, string[]> = {}\n for (const prop of Array.isArray(data['props']) ? (data['props'] as Data[]) : []) {\n const name = text(prop?.['name'])\n if (!name) continue\n props[name] = text(prop['type']) || 'text'\n const facts = aiBracketedFacts([text(prop['defaultValue'])])\n if (facts.length) bracketedDefaults[name] = facts\n }\n return {\n id,\n name: nameOf(data, id, 'displayName'),\n props,\n ...(Object.keys(bracketedDefaults).length ? { bracketedDefaults } : {}),\n }\n },\n ),\n readWindow<AiInventoryLayout>(\n host.collection('layouts'),\n ['displayName', 'layoutId', 'deletedAt'],\n cap,\n (id, data) =>\n data['deletedAt']\n ? null\n : {\n id,\n name: nameOf(data, id, 'displayName'),\n parentId: text(data['layoutId']) || null,\n },\n ),\n readWindow<AiInventoryTemplate>(\n host.collection('templates'),\n ['displayName', 'kind', 'deletedAt'],\n cap,\n (id, data) =>\n data['deletedAt']\n ? null\n : { id, name: nameOf(data, id, 'displayName'), kind: text(data['kind']) || 'page' },\n ),\n readWindow<AiInventoryForm>(\n host.collection('forms'),\n ['displayName', 'slug', 'fields', 'archivedAt'],\n cap,\n (id, data) =>\n isFormArchived(data as { archivedAt?: unknown })\n ? null\n : {\n id,\n name: nameOf(data, id, 'displayName', 'slug'),\n fields: (Array.isArray(data['fields']) ? (data['fields'] as Data[]) : [])\n .map((field) => text(field?.['fieldName']))\n .filter(Boolean),\n },\n ),\n readWindow<AiInventoryDataset>(\n scopedToHost(firestore.collection('orgs').doc(orgId).collection('datasets'), hostId),\n ['displayName', 'name', 'fields', 'model', 'deletedAt'],\n cap,\n (id, data) => {\n if (data['deletedAt']) return null\n const model = effectiveDatasetModel(\n data as Parameters<typeof effectiveDatasetModel>[0],\n )\n const fields = model.order\n .map((fieldId) => text(model.fields[fieldId]?.name) || fieldId)\n .filter(Boolean)\n return { id, name: datasetDisplayName(data) || id, fields }\n },\n ),\n readWindow<AiInventoryCollection>(\n host.collection('collections'),\n ['displayName', 'name', 'slug', 'kind'],\n cap,\n (id, data) =>\n hostCollectionKind(data) !== 'content'\n ? null\n : {\n id,\n name: nameOf(data, id, 'displayName', 'name', 'slug'),\n slug: text(data['slug']),\n },\n ),\n readWindow<AiInventoryScreen>(\n host.collection('screens'),\n ['displayName', 'slug', 'layoutId', 'kind', 'deletedAt'],\n cap,\n (id, data) => {\n const kind = text(data['kind'])\n // An email design and an error body are not pages a plan links or duplicates.\n if (data['deletedAt'] || (kind && kind !== SCREEN_KIND_TEMPLATE)) return null\n return {\n id,\n name: nameOf(data, id, 'displayName', 'slug'),\n slug: text(data['slug']),\n layoutId: text(data['layoutId']) || null,\n template: kind === SCREEN_KIND_TEMPLATE,\n }\n },\n ),\n host.get(),\n ])\n\n const windows: Record<AiInventoryKind, Window<unknown>> = {\n components,\n layouts,\n templates,\n forms,\n datasets,\n collections,\n screens,\n }\n return {\n hostId,\n components: components.rows,\n layouts: layouts.rows,\n templates: templates.rows,\n forms: forms.rows,\n datasets: datasets.rows,\n collections: collections.rows,\n screens: screens.rows,\n theme: aiInventoryTheme(hostDoc.exists ? ((hostDoc.data() ?? {}) as Data) : null),\n truncated: AI_INVENTORY_KINDS.filter((kind) => windows[kind].truncated),\n }\n}\n"],"names":["hostCollectionKind","effectiveDatasetModel","datasetDisplayName","isFormArchived","describeTheme","resolveSiteTheme","REUSABLE_COMPONENT_KIND_EMAIL","reusableComponentKindOf","SCREEN_KIND_TEMPLATE","firebaseAdmin","resolveOrgIdForHost","scopedToHost","AI_INVENTORY_KINDS","AI_SITE_INVENTORY_MAX_PER_KIND","AiInventoryScopeError","aiBracketedFacts","text","value","trim","nameOf","data","id","fields","field","schemeColors","scheme","colors","visit","path","Array","isArray","key","inner","Object","entries","aiInventoryTheme","host","theme","fonts","Set","font","family","add","summary","colorSchemes","light","readWindow","query","cap","map","snapshot","select","limit","get","rows","doc","docs","slice","row","push","truncated","length","readSiteInventory","orgId","hostId","options","hostDoc","owner","firestore","app","Math","min","max","floor","maxPerKind","collection","components","layouts","templates","forms","datasets","collections","screens","Promise","all","props","bracketedDefaults","prop","name","facts","keys","parentId","kind","filter","Boolean","model","order","fieldId","slug","layoutId","template","windows","exists"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/runtime/site-inventory.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 { hostCollectionKind } from '@aglyn/aglyn/app-utils/collection-kind'\nimport { isFormArchived } from '@aglyn/aglyn/app-utils/forms'\nimport {\n describeTheme,\n resolveSiteTheme,\n type ThemeHostDocument,\n} from '@aglyn/aglyn/app-utils/site-theme'\nimport {\n REUSABLE_COMPONENT_KIND_EMAIL,\n reusableComponentKindOf,\n} from '@aglyn/aglyn/app-utils/reusable-component-kind'\nimport { SCREEN_KIND_TEMPLATE } from '@aglyn/aglyn/app-utils/screen-route'\nimport { pluginRecordIndex } from '@aglyn/aglyn/plugin-manager/plugin-record-index'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin/server/firebase-admin'\nimport { resolveOrgIdForHost } from '@aglyn/tenant-data-admin/server/organizations'\nimport {\n AI_INVENTORY_KINDS,\n AI_SITE_INVENTORY_MAX_PER_KIND,\n AiInventoryScopeError,\n type AiComponentPropTypes,\n type AiInventoryCollection,\n type AiInventoryComponent,\n type AiInventoryDataset,\n type AiInventoryForm,\n type AiInventoryKind,\n type AiInventoryLayout,\n type AiInventoryScreen,\n type AiInventoryTemplate,\n type AiInventoryTheme,\n type AiSiteInventory,\n} from '../model/ai-site-inventory'\nimport { aiBracketedFacts } from './ai-doctrine-validators'\n\n/**\n * The reader behind the site inventory (AGL-2935): one site's components,\n * layouts, templates, forms, datasets, content collections, screens and\n * theme, read as projections through the Admin SDK and reduced to the\n * compact shape `model/ai-site-inventory.ts` describes.\n *\n * Every kind is a projection (`select`) of the fields the inventory names —\n * never a node map, never a record's content — read one row past the cap so\n * a full page is told apart from a cut one. Retired rows (soft-deleted,\n * archived, a component never published) are filtered after the read, so a\n * site with many retired rows can list fewer than the cap; the kind is still\n * marked truncated when the read was cut, which is the honest answer.\n *\n * The window is WIDER than the prompt lists (AGL-2937). The prompt block\n * lists `AI_SITE_INVENTORY_LISTED_PER_KIND` records of a kind because every\n * character of it is billed on every attempt; the rest of the window is what\n * the lookup tool searches, and a record never read cannot be found. The\n * difference is paid in document reads rather than tokens — a projection of\n * a few fields, once per job step — which is the cheaper of the two by some\n * orders of magnitude.\n */\n\ntype Firestore = FirebaseFirestore.Firestore\ntype Data = Record<string, unknown>\n\nexport interface ReadSiteInventoryOptions {\n /** The Admin SDK handle; the platform's default app when absent. */\n firestore?: Firestore\n /** Rows read per kind; `AI_SITE_INVENTORY_MAX_PER_KIND` when absent, and never above it. */\n maxPerKind?: number\n}\n\nfunction text(value: unknown): string {\n return typeof value === 'string' ? value.trim() : ''\n}\n\n/** A record's name, or its id when it was saved without one. */\nfunction nameOf(data: Data, id: string, ...fields: string[]): string {\n for (const field of fields) {\n const value = text(data[field])\n if (value) return value\n }\n return id\n}\n\n/** The flat `path → color` leaves of one scheme, e.g. `primary.main`. */\nfunction schemeColors(scheme: unknown): Record<string, string> {\n const colors: Record<string, string> = {}\n const visit = (value: unknown, path: string) => {\n if (typeof value === 'string') {\n if (path) colors[path] = value\n return\n }\n if (!value || typeof value !== 'object' || Array.isArray(value)) return\n for (const [key, inner] of Object.entries(value as Data)) {\n visit(inner, path ? `${path}.${key}` : key)\n }\n }\n visit(scheme, '')\n return colors\n}\n\n/** The theme a site renders with, reduced to what a plan and an email can act on. */\nexport function aiInventoryTheme(host: Data | null): AiInventoryTheme | null {\n const theme = resolveSiteTheme(host as ThemeHostDocument | null)\n if (!theme) return null\n const fonts = new Set<string>()\n for (const font of theme.fonts ?? []) {\n if (text(font?.family)) fonts.add(text(font.family))\n }\n return {\n summary: describeTheme(theme),\n colors: schemeColors(theme.colorSchemes?.light),\n fonts: [...fonts],\n }\n}\n\ninterface Window<T> {\n rows: T[]\n truncated: boolean\n}\n\nasync function readWindow<T>(\n query: FirebaseFirestore.Query,\n fields: string[],\n cap: number,\n map: (id: string, data: Data) => T | null,\n): Promise<Window<T>> {\n const snapshot = await query\n .select(...fields)\n .limit(cap + 1)\n .get()\n const rows: T[] = []\n for (const doc of snapshot.docs.slice(0, cap)) {\n const row = map(doc.id, (doc.data() ?? {}) as Data)\n if (row) rows.push(row)\n }\n return { rows, truncated: snapshot.docs.length > cap }\n}\n\n/**\n * The org's datasets shared with the site, through the `dataset` index the\n * data plugin publishes (AGL-3080) rather than its collection: their names\n * and their fields' names, in the dataset page's order. None where no plugin\n * keeps datasets here.\n */\nasync function readIndexedDatasets(\n orgId: string,\n hostId: string,\n cap: number,\n): Promise<Window<AiInventoryDataset>> {\n const datasets = pluginRecordIndex('dataset')\n if (!datasets) return { rows: [], truncated: false }\n const { records, truncated } = await datasets.index.list({ orgId, hostId, limit: cap })\n return {\n rows: records.map((record) => ({\n id: record.id,\n name: record.name,\n fields: (Array.isArray(record.facts['fields']) ? (record.facts['fields'] as Data[]) : [])\n .map((field) => text(field?.['name']) || text(field?.['id']))\n .filter(Boolean),\n })),\n truncated,\n }\n}\n\n/**\n * Read one site's inventory for a generation.\n *\n * SCOPED before anything is read. The job names a site and an org, and\n * nothing the reader is handed proves the site belongs to the org, so it asks\n * the host index first and refuses a pairing it does not record: another\n * workspace's component names are not this workspace's prompt. Datasets are\n * org-owned and narrowed to what this site may see.\n *\n * Throws `AiInventoryScopeError` for a site outside the org, and lets a\n * Firestore failure propagate: a planner handed an empty inventory by\n * mistake would create everything the site already has.\n */\nexport async function readSiteInventory(\n orgId: string,\n hostId: string,\n options: ReadSiteInventoryOptions = {},\n): Promise<AiSiteInventory> {\n const owner = await resolveOrgIdForHost(hostId)\n if (!owner || owner !== orgId) throw new AiInventoryScopeError(hostId)\n const firestore = options.firestore ?? firebaseAdmin.app().firestore()\n const cap = Math.min(\n AI_SITE_INVENTORY_MAX_PER_KIND,\n Math.max(1, Math.floor(options.maxPerKind ?? AI_SITE_INVENTORY_MAX_PER_KIND)),\n )\n const host = firestore.collection('hosts').doc(hostId)\n\n const [components, layouts, templates, forms, datasets, collections, screens, hostDoc] =\n await Promise.all([\n readWindow<AiInventoryComponent>(\n host.collection('components'),\n ['displayName', 'props', 'rootId', 'versionId', 'deletedAt', 'kind'],\n cap,\n (id, data) => {\n // Soft-deleted, or never published: an instance of either renders nothing.\n if (data['deletedAt'] || !(data['rootId'] || data['versionId'])) return null\n // An email block (AGL-3287) is built from email elements and placed\n // in emails alone: no page, layout or component a job builds can\n // render one, and the email a job builds places no instances.\n if (reusableComponentKindOf(data) === REUSABLE_COMPONENT_KIND_EMAIL) return null\n const props: AiComponentPropTypes = {}\n const bracketedDefaults: Record<string, string[]> = {}\n for (const prop of Array.isArray(data['props']) ? (data['props'] as Data[]) : []) {\n const name = text(prop?.['name'])\n if (!name) continue\n props[name] = text(prop['type']) || 'text'\n const facts = aiBracketedFacts([text(prop['defaultValue'])])\n if (facts.length) bracketedDefaults[name] = facts\n }\n return {\n id,\n name: nameOf(data, id, 'displayName'),\n props,\n ...(Object.keys(bracketedDefaults).length ? { bracketedDefaults } : {}),\n }\n },\n ),\n readWindow<AiInventoryLayout>(\n host.collection('layouts'),\n ['displayName', 'layoutId', 'deletedAt'],\n cap,\n (id, data) =>\n data['deletedAt']\n ? null\n : {\n id,\n name: nameOf(data, id, 'displayName'),\n parentId: text(data['layoutId']) || null,\n },\n ),\n readWindow<AiInventoryTemplate>(\n host.collection('templates'),\n ['displayName', 'kind', 'deletedAt'],\n cap,\n (id, data) =>\n data['deletedAt']\n ? null\n : { id, name: nameOf(data, id, 'displayName'), kind: text(data['kind']) || 'page' },\n ),\n readWindow<AiInventoryForm>(\n host.collection('forms'),\n ['displayName', 'slug', 'fields', 'archivedAt'],\n cap,\n (id, data) =>\n isFormArchived(data as { archivedAt?: unknown })\n ? null\n : {\n id,\n name: nameOf(data, id, 'displayName', 'slug'),\n fields: (Array.isArray(data['fields']) ? (data['fields'] as Data[]) : [])\n .map((field) => text(field?.['fieldName']))\n .filter(Boolean),\n },\n ),\n readIndexedDatasets(orgId, hostId, cap),\n readWindow<AiInventoryCollection>(\n host.collection('collections'),\n ['displayName', 'name', 'slug', 'kind'],\n cap,\n (id, data) =>\n hostCollectionKind(data) !== 'content'\n ? null\n : {\n id,\n name: nameOf(data, id, 'displayName', 'name', 'slug'),\n slug: text(data['slug']),\n },\n ),\n readWindow<AiInventoryScreen>(\n host.collection('screens'),\n ['displayName', 'slug', 'layoutId', 'kind', 'deletedAt'],\n cap,\n (id, data) => {\n const kind = text(data['kind'])\n // An email design and an error body are not pages a plan links or duplicates.\n if (data['deletedAt'] || (kind && kind !== SCREEN_KIND_TEMPLATE)) return null\n return {\n id,\n name: nameOf(data, id, 'displayName', 'slug'),\n slug: text(data['slug']),\n layoutId: text(data['layoutId']) || null,\n template: kind === SCREEN_KIND_TEMPLATE,\n }\n },\n ),\n host.get(),\n ])\n\n const windows: Record<AiInventoryKind, Window<unknown>> = {\n components,\n layouts,\n templates,\n forms,\n datasets,\n collections,\n screens,\n }\n return {\n hostId,\n components: components.rows,\n layouts: layouts.rows,\n templates: templates.rows,\n forms: forms.rows,\n datasets: datasets.rows,\n collections: collections.rows,\n screens: screens.rows,\n theme: aiInventoryTheme(hostDoc.exists ? ((hostDoc.data() ?? {}) as Data) : null),\n truncated: AI_INVENTORY_KINDS.filter((kind) => windows[kind].truncated),\n }\n}\n"],"names":["hostCollectionKind","isFormArchived","describeTheme","resolveSiteTheme","REUSABLE_COMPONENT_KIND_EMAIL","reusableComponentKindOf","SCREEN_KIND_TEMPLATE","pluginRecordIndex","firebaseAdmin","resolveOrgIdForHost","AI_INVENTORY_KINDS","AI_SITE_INVENTORY_MAX_PER_KIND","AiInventoryScopeError","aiBracketedFacts","text","value","trim","nameOf","data","id","fields","field","schemeColors","scheme","colors","visit","path","Array","isArray","key","inner","Object","entries","aiInventoryTheme","host","theme","fonts","Set","font","family","add","summary","colorSchemes","light","readWindow","query","cap","map","snapshot","select","limit","get","rows","doc","docs","slice","row","push","truncated","length","readIndexedDatasets","orgId","hostId","datasets","records","index","list","record","name","facts","filter","Boolean","readSiteInventory","options","hostDoc","owner","firestore","app","Math","min","max","floor","maxPerKind","collection","components","layouts","templates","forms","collections","screens","Promise","all","props","bracketedDefaults","prop","keys","parentId","kind","slug","layoutId","template","windows","exists"],"mappings":"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"}
@@ -22,7 +22,7 @@ import { readOrgAiUsageByUser } from "../usage/ai-usage-by-user.js";
22
22
  import { invalidIdTokenResponse } from "@aglyn/tenant-data-admin/server/id-token-refusal";
23
23
  import { addonKindFromPriceId } from "@aglyn/tenant-data-admin/server/billing-addons";
24
24
  import { orgMarginRow } from "@aglyn/aglyn/app-utils/margin-utilization";
25
- import { assistCogsAlertThresholdUsd, assistMarginMultiple } from "@aglyn/aglyn/app-utils/usage-budget";
25
+ import { assistCogsAlertThresholdUsd, assistMarginMultiple } from "../usage/assist-spend-guards.js";
26
26
  import { composeStaffOrgAiAddon, composeStaffOrgAiMargin, composeStaffOrgAiOverage, composeStaffOrgAiPool, composeStaffOrgAiTokens, emptyJobCounts, STAFF_ORG_AI_JOB_STATUSES } from "../usage/staff-org-ai.js";
27
27
  /**
28
28
  * THE STAFF AI CARD'S BACKING READ (AGL-2930).
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/server/ai-admin-org.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n isLiveSubscriptionStatus,\n ORG_BILLING_DOC_ID,\n ORG_BILLING_SUBCOLLECTION,\n orgCogsInputFrom,\n pluginRequestFromWeb,\n} from '@aglyn/aglyn/server'\nimport {\n emailUnverifiedResponse,\n firebaseAdmin,\n isImpersonationSession,\n readOrgBilling,\n} from '@aglyn/tenant-data-admin'\nimport { assistUsageMonth } from '../usage/assist-usage'\nimport { assistRefusalCounts } from '../usage/assist-refusals'\nimport { readOrgAiUsageByUser } from '../usage/ai-usage-by-user'\nimport { invalidIdTokenResponse } from '@aglyn/tenant-data-admin/server/id-token-refusal'\nimport { addonKindFromPriceId } from '@aglyn/tenant-data-admin/server/billing-addons'\nimport { orgMarginRow } from '@aglyn/aglyn/app-utils/margin-utilization'\nimport {\n assistCogsAlertThresholdUsd,\n assistMarginMultiple,\n} from '@aglyn/aglyn/app-utils/usage-budget'\nimport {\n composeStaffOrgAiAddon,\n composeStaffOrgAiMargin,\n composeStaffOrgAiOverage,\n composeStaffOrgAiPool,\n composeStaffOrgAiTokens,\n emptyJobCounts,\n STAFF_ORG_AI_JOB_STATUSES,\n type StaffOrgAiAddon,\n type StaffOrgAiJob,\n type StaffOrgAiJobs,\n type StaffOrgAiResponse,\n type StaffOrgAiUser,\n} from '../usage/staff-org-ai'\n\n/**\n * THE STAFF AI CARD'S BACKING READ (AGL-2930).\n *\n * One request composes everything the card on the staff org page shows: the\n * add-on and when it was bought, the credit pool and its parts, the month's\n * overage and the org's own ceilings, the refusal counter, the generation\n * jobs, the per-user rollup and the margin. Staff-gated exactly as\n * `/api/admin/org-usage` is, and read-only — every access below is a `get()`.\n *\n * ## The reads, stated\n *\n * The org document and its billing mirror; this month's `assistUsage`\n * document; the newest usage rollup (for the contribution margin, the same\n * read the margin route makes); a bounded page of `aiJobs`; the per-user\n * rollup for the month; and, when the add-on is on, ONE Stripe subscription\n * lookup that is cached in-process — see `addonSinceFor`.\n *\n * ## No audit row\n *\n * The card loads every time the staff org page opens, so an `adminAudit` row\n * per load recorded page views rather than decisions and buried the org's\n * real admin actions under them. The route only reads.\n */\n\n/** Generation jobs read per request; the counts are a floor past it. */\nconst JOBS_SCAN = 200\n\n/** How many jobs the card lists. */\nconst JOBS_RECENT = 10\n\n/** How many people the leaderboard names. */\nconst TOP_USERS = 10\n\n/**\n * How long a Stripe answer to \"when was the add-on bought\" is kept.\n *\n * The date does not move once the item exists, and the card is opened far\n * more often than an add-on is bought, so the only cost of a stale entry is\n * a card that says \"since —\" for up to this long after a purchase. The\n * alternative — a Stripe round trip on every render — is the thing the brief\n * forbade.\n */\nconst ADDON_SINCE_TTL_MS = 15 * 60 * 1000\n\ninterface AddonSince {\n since: string | null\n sinceSource: StaffOrgAiAddon['sinceSource']\n at: number\n}\n\nconst addonSinceCache = new Map<string, AddonSince>()\n\n/** Test seam: drop every cached Stripe answer. */\nexport function resetAddonSinceCache(): void {\n addonSinceCache.clear()\n}\n\n/**\n * The instant the add-on's subscription item was created, from Stripe.\n *\n * The org document carries the add-on as a quantity (`seatAddons.aiAddon`)\n * and nothing about WHEN — the webhook mirrors items, not their dates. Stripe\n * has it as `created` on the subscription item, found by the price id the\n * add-on sells at (`addonKindFromPriceId`), so a rename or a price change\n * does not lose it.\n */\nasync function addonSinceFor(orgId: string): Promise<AddonSince> {\n const cached = addonSinceCache.get(orgId)\n if (cached && Date.now() - cached.at < ADDON_SINCE_TTL_MS) return cached\n const answer = await readAddonSince(orgId)\n addonSinceCache.set(orgId, answer)\n return answer\n}\n\nasync function readAddonSince(orgId: string): Promise<AddonSince> {\n const at = Date.now()\n const stripeKey = process.env.STRIPE_SECRET_KEY\n if (!stripeKey) return { since: null, sinceSource: 'unavailable', at }\n try {\n const customerId = (await readOrgBilling(orgId)).stripeCustomerId\n if (!customerId) return { since: null, sinceSource: 'no-subscription', at }\n const response = await fetch(\n `https://api.stripe.com/v1/subscriptions?customer=${encodeURIComponent(\n String(customerId),\n )}&status=all&limit=10`,\n { headers: { Authorization: `Bearer ${stripeKey}` } },\n )\n const payload = await response.json().catch(() => null)\n if (!response.ok) {\n console.error('[ai/admin/org] Stripe subscription lookup failed', {\n orgId,\n status: response.status,\n detail: payload?.error?.message ?? null,\n })\n return { since: null, sinceSource: 'unavailable', at }\n }\n const subscriptions = (payload?.data ?? []) as Array<{\n status?: string\n items?: { data?: Array<{ created?: number; price?: { id?: string } }> }\n }>\n // The LIVE subscription's item, when one exists: an add-on on a canceled\n // subscription is not an add-on the org has, and `hasAiAddon` already\n // says so — the date beside \"off\" would only confuse.\n const live = subscriptions.filter((subscription) =>\n isLiveSubscriptionStatus(subscription.status),\n )\n for (const subscription of live.length ? live : subscriptions) {\n for (const item of subscription.items?.data ?? []) {\n if (addonKindFromPriceId(item.price?.id) !== 'aiAddon') continue\n const created = Number(item.created)\n if (Number.isFinite(created) && created > 0) {\n return {\n since: new Date(created * 1000).toISOString(),\n sinceSource: 'stripe',\n at,\n }\n }\n }\n }\n return { since: null, sinceSource: 'stripe', at }\n } catch (error) {\n console.error('[ai/admin/org] Stripe subscription lookup threw', error)\n return { since: null, sinceSource: 'unavailable', at }\n }\n}\n\nconst toIso = (value: unknown): string | null => {\n const converted = (value as { toDate?: () => Date } | null)?.toDate?.()\n if (converted instanceof Date) return converted.toISOString()\n if (value instanceof Date) return value.toISOString()\n return null\n}\n\n/**\n * The month's generation jobs, summarized (AGL-2904's collection).\n *\n * Reads `orgs/{orgId}/aiJobs` by `createdAt` from the first instant of the\n * month, newest first, up to `JOBS_SCAN`. An org with no such collection\n * answers an empty snapshot, which is the honest \"no generation jobs yet\";\n * a read that FAILED answers `null`, which the card renders as a failure\n * rather than as an idle org. The field names are the ones `AiJob` declares\n * in `ai-jobs.types.ts`: `kind`, `status`, `creditsReserved`, `creditsSpent`,\n * `createdAt`, `createdBy`.\n */\nexport async function readOrgAiJobsSummary(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n month: string,\n): Promise<StaffOrgAiJobs | null> {\n try {\n const monthStart = new Date(`${month}-01T00:00:00.000Z`)\n const snapshot = await firestore\n .collection('orgs')\n .doc(orgId)\n .collection('aiJobs')\n .where('createdAt', '>=', monthStart)\n .orderBy('createdAt', 'desc')\n .limit(JOBS_SCAN + 1)\n .get()\n const truncated = snapshot.docs.length > JOBS_SCAN\n const docs = truncated ? snapshot.docs.slice(0, JOBS_SCAN) : snapshot.docs\n const counts = emptyJobCounts()\n const recent: StaffOrgAiJob[] = []\n for (const doc of docs) {\n const status = String(doc.get('status') ?? '')\n if ((STAFF_ORG_AI_JOB_STATUSES as readonly string[]).includes(status)) {\n counts[status as keyof typeof counts] += 1\n }\n if (recent.length < JOBS_RECENT) {\n recent.push({\n id: doc.id,\n kind: String(doc.get('kind') ?? ''),\n status,\n creditsReserved: Number(doc.get('creditsReserved') ?? 0) || 0,\n creditsSpent: Number(doc.get('creditsSpent') ?? 0) || 0,\n createdAt: toIso(doc.get('createdAt')),\n createdBy:\n typeof doc.get('createdBy') === 'string'\n ? String(doc.get('createdBy'))\n : null,\n })\n }\n }\n return { counts, recent, truncated }\n } catch (error) {\n console.error('[ai/admin/org] jobs read failed', error)\n return null\n }\n}\n\n/**\n * The month's per-user rollup, dearest first (AGL-2928).\n *\n * `readOrgAiUsageByUser` is the one reader of\n * `orgs/{orgId}/aiUsageByUser/{uid}/months/{month}` — the customer's Usage\n * table reads through it too — joined to the roster for names and carrying\n * each person's share of the org's measured spend. The card keeps the\n * dearest `TOP_USERS`.\n */\nasync function readOrgAiTopUsers(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n month: string,\n): Promise<StaffOrgAiUser[]> {\n try {\n const usage = await readOrgAiUsageByUser(firestore, orgId, month)\n return usage.rows.slice(0, TOP_USERS).map((row) => ({\n uid: row.uid,\n name: row.name,\n credits: row.credits,\n estCostUsd: row.estCostUsd,\n share: row.share,\n requests: row.requests,\n refusals: row.refusals,\n byKind: row.byKind,\n byHost: row.byHost,\n }))\n } catch (error) {\n console.error('[ai/admin/org] per-user read failed', error)\n return []\n }\n}\n\nasync function handler(request: Request): Promise<Response> {\n const {\n method,\n query,\n headers: rawHeaders,\n } = await pluginRequestFromWeb(request)\n const headers = rawHeaders as Partial<Record<string, string>>\n if (method !== 'GET') {\n return Response.json({ error: 'Method not allowed' }, { status: 405 })\n }\n const authorization = headers.authorization ?? ''\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n if (!idToken)\n return Response.json({ error: 'Unauthenticated' }, { status: 401 })\n const orgId = String(query.orgId ?? '')\n if (!orgId) return Response.json({ error: 'Missing orgId' }, { status: 400 })\n\n try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n if (!decoded.email_verified && !isImpersonationSession(decoded)) {\n return emailUnverifiedResponse()\n }\n if (!decoded['staff']) {\n return Response.json({ error: 'Staff only' }, { status: 403 })\n }\n\n const now = new Date()\n const month = assistUsageMonth(now)\n const db = firebaseAdmin.app().firestore()\n const orgRef = db.collection('orgs').doc(orgId)\n\n const [orgSnap, billingSnap, monthSnap, rollupSnap] = await Promise.all([\n orgRef.get(),\n orgRef.collection(ORG_BILLING_SUBCOLLECTION).doc(ORG_BILLING_DOC_ID).get(),\n orgRef.collection('assistUsage').doc(month).get(),\n orgRef.collection('usage').orderBy('month', 'desc').limit(1).get(),\n ])\n if (!orgSnap.exists) {\n return Response.json({ error: 'No such organization' }, { status: 404 })\n }\n // Org doc FIRST so a stale inline `subscription` loses to the billing\n // mirror — the same merge the margin route and the org detail read make.\n const org = {\n ...(orgSnap.data() ?? {}),\n ...(billingSnap.exists ? billingSnap.data() : {}),\n } as Record<string, unknown>\n const monthDoc = monthSnap.exists ? (monthSnap.data() ?? {}) : null\n\n const [jobs, users, since] = await Promise.all([\n readOrgAiJobsSummary(db, orgId, month),\n readOrgAiTopUsers(db, orgId, month),\n addonSinceFor(orgId),\n ])\n\n const pool = composeStaffOrgAiPool(org as never, monthDoc, now)\n // The overage line is an invoice, so it is priced off what the month\n // DREW; everything below it is our bill, so it reads what the month\n // COST (AGL-3015).\n const overage = composeStaffOrgAiOverage(org as never, pool.billedUsd)\n const addon = composeStaffOrgAiAddon(org as never, {\n since: since.since,\n sinceSource: since.sinceSource,\n })\n const thresholdUsd = assistCogsAlertThresholdUsd(\n process.env.ASSIST_ORG_MONTHLY_COGS_ALERT_USD,\n )\n const rollupDoc = rollupSnap.docs[0]\n // The whole-org contribution margin, on the same row the margin\n // utilization page builds — with THIS month's live assist spend in\n // place of the figure frozen onto the rollup, as that route does.\n const contributionRow = rollupDoc\n ? orgMarginRow({\n orgId,\n name: (org['name'] as string | undefined) ?? null,\n org: org as never,\n month: String(rollupDoc.get('month') ?? rollupDoc.id),\n rollup: {\n ...orgCogsInputFrom(rollupDoc.data()),\n assistCostUsd: pool.providerUsd,\n },\n })\n : null\n const margin = composeStaffOrgAiMargin({\n org: org as never,\n pool,\n overage,\n addon,\n thresholdUsd,\n multiple: assistMarginMultiple(pool.providerUsd, thresholdUsd),\n contribution: {\n month: contributionRow?.month ?? null,\n marginPct: contributionRow?.marginPct ?? null,\n rating: contributionRow?.rating ?? null,\n },\n })\n\n const body: StaffOrgAiResponse = {\n month,\n addon,\n pool,\n overage,\n refusals: assistRefusalCounts(monthDoc?.['refusals']),\n jobs,\n users,\n margin,\n tokens: composeStaffOrgAiTokens(monthDoc),\n }\n return Response.json(body, { status: 200 })\n } catch (error) {\n // An unverifiable credential is a 401, not a fault of ours\n // (AGL-1993). Null for anything else, so a real failure keeps its 500.\n const unauthenticated = invalidIdTokenResponse(error)\n if (unauthenticated) return unauthenticated\n console.error('[ai/admin/org]', error)\n return Response.json({ error: 'AI lookup failed' }, { status: 500 })\n }\n}\n\nexport { handler as GET }\n"],"names":["isLiveSubscriptionStatus","ORG_BILLING_DOC_ID","ORG_BILLING_SUBCOLLECTION","orgCogsInputFrom","pluginRequestFromWeb","emailUnverifiedResponse","firebaseAdmin","isImpersonationSession","readOrgBilling","assistUsageMonth","assistRefusalCounts","readOrgAiUsageByUser","invalidIdTokenResponse","addonKindFromPriceId","orgMarginRow","assistCogsAlertThresholdUsd","assistMarginMultiple","composeStaffOrgAiAddon","composeStaffOrgAiMargin","composeStaffOrgAiOverage","composeStaffOrgAiPool","composeStaffOrgAiTokens","emptyJobCounts","STAFF_ORG_AI_JOB_STATUSES","JOBS_SCAN","JOBS_RECENT","TOP_USERS","ADDON_SINCE_TTL_MS","addonSinceCache","Map","resetAddonSinceCache","clear","addonSinceFor","orgId","cached","get","Date","now","at","answer","readAddonSince","set","stripeKey","process","env","STRIPE_SECRET_KEY","since","sinceSource","customerId","stripeCustomerId","response","fetch","encodeURIComponent","String","headers","Authorization","payload","json","catch","ok","console","error","status","detail","message","subscriptions","data","live","filter","subscription","length","item","items","price","id","created","Number","isFinite","toISOString","toIso","value","converted","toDate","readOrgAiJobsSummary","firestore","month","monthStart","snapshot","collection","doc","where","orderBy","limit","truncated","docs","slice","counts","recent","includes","push","kind","creditsReserved","creditsSpent","createdAt","createdBy","readOrgAiTopUsers","usage","rows","map","row","uid","name","credits","estCostUsd","share","requests","refusals","byKind","byHost","handler","request","query","method","rawHeaders","Response","authorization","idToken","startsWith","undefined","orgSnap","monthSnap","org","rollupDoc","decoded","app","auth","verifyIdToken","email_verified","db","orgRef","billingSnap","rollupSnap","Promise","all","exists","monthDoc","jobs","users","pool","overage","billedUsd","addon","thresholdUsd","ASSIST_ORG_MONTHLY_COGS_ALERT_USD","contributionRow","rollup","assistCostUsd","providerUsd","margin","multiple","contribution","marginPct","rating","body","tokens","unauthenticated","GET"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/server/ai-admin-org.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n isLiveSubscriptionStatus,\n ORG_BILLING_DOC_ID,\n ORG_BILLING_SUBCOLLECTION,\n orgCogsInputFrom,\n pluginRequestFromWeb,\n} from '@aglyn/aglyn/server'\nimport {\n emailUnverifiedResponse,\n firebaseAdmin,\n isImpersonationSession,\n readOrgBilling,\n} from '@aglyn/tenant-data-admin'\nimport { assistUsageMonth } from '../usage/assist-usage'\nimport { assistRefusalCounts } from '../usage/assist-refusals'\nimport { readOrgAiUsageByUser } from '../usage/ai-usage-by-user'\nimport { invalidIdTokenResponse } from '@aglyn/tenant-data-admin/server/id-token-refusal'\nimport { addonKindFromPriceId } from '@aglyn/tenant-data-admin/server/billing-addons'\nimport { orgMarginRow } from '@aglyn/aglyn/app-utils/margin-utilization'\nimport {\n assistCogsAlertThresholdUsd,\n assistMarginMultiple,\n} from '../usage/assist-spend-guards'\nimport {\n composeStaffOrgAiAddon,\n composeStaffOrgAiMargin,\n composeStaffOrgAiOverage,\n composeStaffOrgAiPool,\n composeStaffOrgAiTokens,\n emptyJobCounts,\n STAFF_ORG_AI_JOB_STATUSES,\n type StaffOrgAiAddon,\n type StaffOrgAiJob,\n type StaffOrgAiJobs,\n type StaffOrgAiResponse,\n type StaffOrgAiUser,\n} from '../usage/staff-org-ai'\n\n/**\n * THE STAFF AI CARD'S BACKING READ (AGL-2930).\n *\n * One request composes everything the card on the staff org page shows: the\n * add-on and when it was bought, the credit pool and its parts, the month's\n * overage and the org's own ceilings, the refusal counter, the generation\n * jobs, the per-user rollup and the margin. Staff-gated exactly as\n * `/api/admin/org-usage` is, and read-only — every access below is a `get()`.\n *\n * ## The reads, stated\n *\n * The org document and its billing mirror; this month's `assistUsage`\n * document; the newest usage rollup (for the contribution margin, the same\n * read the margin route makes); a bounded page of `aiJobs`; the per-user\n * rollup for the month; and, when the add-on is on, ONE Stripe subscription\n * lookup that is cached in-process — see `addonSinceFor`.\n *\n * ## No audit row\n *\n * The card loads every time the staff org page opens, so an `adminAudit` row\n * per load recorded page views rather than decisions and buried the org's\n * real admin actions under them. The route only reads.\n */\n\n/** Generation jobs read per request; the counts are a floor past it. */\nconst JOBS_SCAN = 200\n\n/** How many jobs the card lists. */\nconst JOBS_RECENT = 10\n\n/** How many people the leaderboard names. */\nconst TOP_USERS = 10\n\n/**\n * How long a Stripe answer to \"when was the add-on bought\" is kept.\n *\n * The date does not move once the item exists, and the card is opened far\n * more often than an add-on is bought, so the only cost of a stale entry is\n * a card that says \"since —\" for up to this long after a purchase. The\n * alternative — a Stripe round trip on every render — is the thing the brief\n * forbade.\n */\nconst ADDON_SINCE_TTL_MS = 15 * 60 * 1000\n\ninterface AddonSince {\n since: string | null\n sinceSource: StaffOrgAiAddon['sinceSource']\n at: number\n}\n\nconst addonSinceCache = new Map<string, AddonSince>()\n\n/** Test seam: drop every cached Stripe answer. */\nexport function resetAddonSinceCache(): void {\n addonSinceCache.clear()\n}\n\n/**\n * The instant the add-on's subscription item was created, from Stripe.\n *\n * The org document carries the add-on as a quantity (`seatAddons.aiAddon`)\n * and nothing about WHEN — the webhook mirrors items, not their dates. Stripe\n * has it as `created` on the subscription item, found by the price id the\n * add-on sells at (`addonKindFromPriceId`), so a rename or a price change\n * does not lose it.\n */\nasync function addonSinceFor(orgId: string): Promise<AddonSince> {\n const cached = addonSinceCache.get(orgId)\n if (cached && Date.now() - cached.at < ADDON_SINCE_TTL_MS) return cached\n const answer = await readAddonSince(orgId)\n addonSinceCache.set(orgId, answer)\n return answer\n}\n\nasync function readAddonSince(orgId: string): Promise<AddonSince> {\n const at = Date.now()\n const stripeKey = process.env.STRIPE_SECRET_KEY\n if (!stripeKey) return { since: null, sinceSource: 'unavailable', at }\n try {\n const customerId = (await readOrgBilling(orgId)).stripeCustomerId\n if (!customerId) return { since: null, sinceSource: 'no-subscription', at }\n const response = await fetch(\n `https://api.stripe.com/v1/subscriptions?customer=${encodeURIComponent(\n String(customerId),\n )}&status=all&limit=10`,\n { headers: { Authorization: `Bearer ${stripeKey}` } },\n )\n const payload = await response.json().catch(() => null)\n if (!response.ok) {\n console.error('[ai/admin/org] Stripe subscription lookup failed', {\n orgId,\n status: response.status,\n detail: payload?.error?.message ?? null,\n })\n return { since: null, sinceSource: 'unavailable', at }\n }\n const subscriptions = (payload?.data ?? []) as Array<{\n status?: string\n items?: { data?: Array<{ created?: number; price?: { id?: string } }> }\n }>\n // The LIVE subscription's item, when one exists: an add-on on a canceled\n // subscription is not an add-on the org has, and `hasAiAddon` already\n // says so — the date beside \"off\" would only confuse.\n const live = subscriptions.filter((subscription) =>\n isLiveSubscriptionStatus(subscription.status),\n )\n for (const subscription of live.length ? live : subscriptions) {\n for (const item of subscription.items?.data ?? []) {\n if (addonKindFromPriceId(item.price?.id) !== 'aiAddon') continue\n const created = Number(item.created)\n if (Number.isFinite(created) && created > 0) {\n return {\n since: new Date(created * 1000).toISOString(),\n sinceSource: 'stripe',\n at,\n }\n }\n }\n }\n return { since: null, sinceSource: 'stripe', at }\n } catch (error) {\n console.error('[ai/admin/org] Stripe subscription lookup threw', error)\n return { since: null, sinceSource: 'unavailable', at }\n }\n}\n\nconst toIso = (value: unknown): string | null => {\n const converted = (value as { toDate?: () => Date } | null)?.toDate?.()\n if (converted instanceof Date) return converted.toISOString()\n if (value instanceof Date) return value.toISOString()\n return null\n}\n\n/**\n * The month's generation jobs, summarized (AGL-2904's collection).\n *\n * Reads `orgs/{orgId}/aiJobs` by `createdAt` from the first instant of the\n * month, newest first, up to `JOBS_SCAN`. An org with no such collection\n * answers an empty snapshot, which is the honest \"no generation jobs yet\";\n * a read that FAILED answers `null`, which the card renders as a failure\n * rather than as an idle org. The field names are the ones `AiJob` declares\n * in `ai-jobs.types.ts`: `kind`, `status`, `creditsReserved`, `creditsSpent`,\n * `createdAt`, `createdBy`.\n */\nexport async function readOrgAiJobsSummary(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n month: string,\n): Promise<StaffOrgAiJobs | null> {\n try {\n const monthStart = new Date(`${month}-01T00:00:00.000Z`)\n const snapshot = await firestore\n .collection('orgs')\n .doc(orgId)\n .collection('aiJobs')\n .where('createdAt', '>=', monthStart)\n .orderBy('createdAt', 'desc')\n .limit(JOBS_SCAN + 1)\n .get()\n const truncated = snapshot.docs.length > JOBS_SCAN\n const docs = truncated ? snapshot.docs.slice(0, JOBS_SCAN) : snapshot.docs\n const counts = emptyJobCounts()\n const recent: StaffOrgAiJob[] = []\n for (const doc of docs) {\n const status = String(doc.get('status') ?? '')\n if ((STAFF_ORG_AI_JOB_STATUSES as readonly string[]).includes(status)) {\n counts[status as keyof typeof counts] += 1\n }\n if (recent.length < JOBS_RECENT) {\n recent.push({\n id: doc.id,\n kind: String(doc.get('kind') ?? ''),\n status,\n creditsReserved: Number(doc.get('creditsReserved') ?? 0) || 0,\n creditsSpent: Number(doc.get('creditsSpent') ?? 0) || 0,\n createdAt: toIso(doc.get('createdAt')),\n createdBy:\n typeof doc.get('createdBy') === 'string'\n ? String(doc.get('createdBy'))\n : null,\n })\n }\n }\n return { counts, recent, truncated }\n } catch (error) {\n console.error('[ai/admin/org] jobs read failed', error)\n return null\n }\n}\n\n/**\n * The month's per-user rollup, dearest first (AGL-2928).\n *\n * `readOrgAiUsageByUser` is the one reader of\n * `orgs/{orgId}/aiUsageByUser/{uid}/months/{month}` — the customer's Usage\n * table reads through it too — joined to the roster for names and carrying\n * each person's share of the org's measured spend. The card keeps the\n * dearest `TOP_USERS`.\n */\nasync function readOrgAiTopUsers(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n month: string,\n): Promise<StaffOrgAiUser[]> {\n try {\n const usage = await readOrgAiUsageByUser(firestore, orgId, month)\n return usage.rows.slice(0, TOP_USERS).map((row) => ({\n uid: row.uid,\n name: row.name,\n credits: row.credits,\n estCostUsd: row.estCostUsd,\n share: row.share,\n requests: row.requests,\n refusals: row.refusals,\n byKind: row.byKind,\n byHost: row.byHost,\n }))\n } catch (error) {\n console.error('[ai/admin/org] per-user read failed', error)\n return []\n }\n}\n\nasync function handler(request: Request): Promise<Response> {\n const {\n method,\n query,\n headers: rawHeaders,\n } = await pluginRequestFromWeb(request)\n const headers = rawHeaders as Partial<Record<string, string>>\n if (method !== 'GET') {\n return Response.json({ error: 'Method not allowed' }, { status: 405 })\n }\n const authorization = headers.authorization ?? ''\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n if (!idToken)\n return Response.json({ error: 'Unauthenticated' }, { status: 401 })\n const orgId = String(query.orgId ?? '')\n if (!orgId) return Response.json({ error: 'Missing orgId' }, { status: 400 })\n\n try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n if (!decoded.email_verified && !isImpersonationSession(decoded)) {\n return emailUnverifiedResponse()\n }\n if (!decoded['staff']) {\n return Response.json({ error: 'Staff only' }, { status: 403 })\n }\n\n const now = new Date()\n const month = assistUsageMonth(now)\n const db = firebaseAdmin.app().firestore()\n const orgRef = db.collection('orgs').doc(orgId)\n\n const [orgSnap, billingSnap, monthSnap, rollupSnap] = await Promise.all([\n orgRef.get(),\n orgRef.collection(ORG_BILLING_SUBCOLLECTION).doc(ORG_BILLING_DOC_ID).get(),\n orgRef.collection('assistUsage').doc(month).get(),\n orgRef.collection('usage').orderBy('month', 'desc').limit(1).get(),\n ])\n if (!orgSnap.exists) {\n return Response.json({ error: 'No such organization' }, { status: 404 })\n }\n // Org doc FIRST so a stale inline `subscription` loses to the billing\n // mirror — the same merge the margin route and the org detail read make.\n const org = {\n ...(orgSnap.data() ?? {}),\n ...(billingSnap.exists ? billingSnap.data() : {}),\n } as Record<string, unknown>\n const monthDoc = monthSnap.exists ? (monthSnap.data() ?? {}) : null\n\n const [jobs, users, since] = await Promise.all([\n readOrgAiJobsSummary(db, orgId, month),\n readOrgAiTopUsers(db, orgId, month),\n addonSinceFor(orgId),\n ])\n\n const pool = composeStaffOrgAiPool(org as never, monthDoc, now)\n // The overage line is an invoice, so it is priced off what the month\n // DREW; everything below it is our bill, so it reads what the month\n // COST (AGL-3015).\n const overage = composeStaffOrgAiOverage(org as never, pool.billedUsd)\n const addon = composeStaffOrgAiAddon(org as never, {\n since: since.since,\n sinceSource: since.sinceSource,\n })\n const thresholdUsd = assistCogsAlertThresholdUsd(\n process.env.ASSIST_ORG_MONTHLY_COGS_ALERT_USD,\n )\n const rollupDoc = rollupSnap.docs[0]\n // The whole-org contribution margin, on the same row the margin\n // utilization page builds — with THIS month's live assist spend in\n // place of the figure frozen onto the rollup, as that route does.\n const contributionRow = rollupDoc\n ? orgMarginRow({\n orgId,\n name: (org['name'] as string | undefined) ?? null,\n org: org as never,\n month: String(rollupDoc.get('month') ?? rollupDoc.id),\n rollup: {\n ...orgCogsInputFrom(rollupDoc.data()),\n assistCostUsd: pool.providerUsd,\n },\n })\n : null\n const margin = composeStaffOrgAiMargin({\n org: org as never,\n pool,\n overage,\n addon,\n thresholdUsd,\n multiple: assistMarginMultiple(pool.providerUsd, thresholdUsd),\n contribution: {\n month: contributionRow?.month ?? null,\n marginPct: contributionRow?.marginPct ?? null,\n rating: contributionRow?.rating ?? null,\n },\n })\n\n const body: StaffOrgAiResponse = {\n month,\n addon,\n pool,\n overage,\n refusals: assistRefusalCounts(monthDoc?.['refusals']),\n jobs,\n users,\n margin,\n tokens: composeStaffOrgAiTokens(monthDoc),\n }\n return Response.json(body, { status: 200 })\n } catch (error) {\n // An unverifiable credential is a 401, not a fault of ours\n // (AGL-1993). Null for anything else, so a real failure keeps its 500.\n const unauthenticated = invalidIdTokenResponse(error)\n if (unauthenticated) return unauthenticated\n console.error('[ai/admin/org]', error)\n return Response.json({ error: 'AI lookup failed' }, { status: 500 })\n }\n}\n\nexport { handler as GET }\n"],"names":["isLiveSubscriptionStatus","ORG_BILLING_DOC_ID","ORG_BILLING_SUBCOLLECTION","orgCogsInputFrom","pluginRequestFromWeb","emailUnverifiedResponse","firebaseAdmin","isImpersonationSession","readOrgBilling","assistUsageMonth","assistRefusalCounts","readOrgAiUsageByUser","invalidIdTokenResponse","addonKindFromPriceId","orgMarginRow","assistCogsAlertThresholdUsd","assistMarginMultiple","composeStaffOrgAiAddon","composeStaffOrgAiMargin","composeStaffOrgAiOverage","composeStaffOrgAiPool","composeStaffOrgAiTokens","emptyJobCounts","STAFF_ORG_AI_JOB_STATUSES","JOBS_SCAN","JOBS_RECENT","TOP_USERS","ADDON_SINCE_TTL_MS","addonSinceCache","Map","resetAddonSinceCache","clear","addonSinceFor","orgId","cached","get","Date","now","at","answer","readAddonSince","set","stripeKey","process","env","STRIPE_SECRET_KEY","since","sinceSource","customerId","stripeCustomerId","response","fetch","encodeURIComponent","String","headers","Authorization","payload","json","catch","ok","console","error","status","detail","message","subscriptions","data","live","filter","subscription","length","item","items","price","id","created","Number","isFinite","toISOString","toIso","value","converted","toDate","readOrgAiJobsSummary","firestore","month","monthStart","snapshot","collection","doc","where","orderBy","limit","truncated","docs","slice","counts","recent","includes","push","kind","creditsReserved","creditsSpent","createdAt","createdBy","readOrgAiTopUsers","usage","rows","map","row","uid","name","credits","estCostUsd","share","requests","refusals","byKind","byHost","handler","request","query","method","rawHeaders","Response","authorization","idToken","startsWith","undefined","orgSnap","monthSnap","org","rollupDoc","decoded","app","auth","verifyIdToken","email_verified","db","orgRef","billingSnap","rollupSnap","Promise","all","exists","monthDoc","jobs","users","pool","overage","billedUsd","addon","thresholdUsd","ASSIST_ORG_MONTHLY_COGS_ALERT_USD","contributionRow","rollup","assistCostUsd","providerUsd","margin","multiple","contribution","marginPct","rating","body","tokens","unauthenticated","GET"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,wBAAwB,EACxBC,kBAAkB,EAClBC,yBAAyB,EACzBC,gBAAgB,EAChBC,oBAAoB,QACf,sBAAqB;AAC5B,SACEC,uBAAuB,EACvBC,aAAa,EACbC,sBAAsB,EACtBC,cAAc,QACT,2BAA0B;AACjC,SAASC,gBAAgB,QAAQ,2BAAuB;AACxD,SAASC,mBAAmB,QAAQ,8BAA0B;AAC9D,SAASC,oBAAoB,QAAQ,+BAA2B;AAChE,SAASC,sBAAsB,QAAQ,mDAAkD;AACzF,SAASC,oBAAoB,QAAQ,iDAAgD;AACrF,SAASC,YAAY,QAAQ,4CAA2C;AACxE,SACEC,2BAA2B,EAC3BC,oBAAoB,QACf,kCAA8B;AACrC,SACEC,sBAAsB,EACtBC,uBAAuB,EACvBC,wBAAwB,EACxBC,qBAAqB,EACrBC,uBAAuB,EACvBC,cAAc,EACdC,yBAAyB,QAMpB,2BAAuB;AAE9B;;;;;;;;;;;;;;;;;;;;;;CAsBC,GAED,sEAAsE,GACtE,MAAMC,YAAY;AAElB,kCAAkC,GAClC,MAAMC,cAAc;AAEpB,2CAA2C,GAC3C,MAAMC,YAAY;AAElB;;;;;;;;CAQC,GACD,MAAMC,qBAAqB,KAAK,KAAK;AAQrC,MAAMC,kBAAkB,IAAIC;AAE5B,gDAAgD,GAChD,OAAO,SAASC;IACdF,gBAAgBG,KAAK;AACvB;AAEA;;;;;;;;CAQC,GACD,eAAeC,cAAcC,KAAa;IACxC,MAAMC,SAASN,gBAAgBO,GAAG,CAACF;IACnC,IAAIC,UAAUE,KAAKC,GAAG,KAAKH,OAAOI,EAAE,GAAGX,oBAAoB,OAAOO;IAClE,MAAMK,SAAS,MAAMC,eAAeP;IACpCL,gBAAgBa,GAAG,CAACR,OAAOM;IAC3B,OAAOA;AACT;AAEA,eAAeC,eAAeP,KAAa;IACzC,MAAMK,KAAKF,KAAKC,GAAG;IACnB,MAAMK,YAAYC,QAAQC,GAAG,CAACC,iBAAiB;IAC/C,IAAI,CAACH,WAAW,OAAO;QAAEI,OAAO;QAAMC,aAAa;QAAeT;IAAG;IACrE,IAAI;;QACF,MAAMU,aAAa,AAAC,CAAA,MAAMxC,eAAeyB,MAAK,EAAGgB,gBAAgB;QACjE,IAAI,CAACD,YAAY,OAAO;YAAEF,OAAO;YAAMC,aAAa;YAAmBT;QAAG;QAC1E,MAAMY,WAAW,MAAMC,MACrB,CAAC,iDAAiD,EAAEC,mBAClDC,OAAOL,aACP,oBAAoB,CAAC,EACvB;YAAEM,SAAS;gBAAEC,eAAe,CAAC,OAAO,EAAEb,WAAW;YAAC;QAAE;QAEtD,MAAMc,UAAU,MAAMN,SAASO,IAAI,GAAGC,KAAK,CAAC,IAAM;QAClD,IAAI,CAACR,SAASS,EAAE,EAAE;;gBAINH;YAHVI,QAAQC,KAAK,CAAC,oDAAoD;gBAChE5B;gBACA6B,QAAQZ,SAASY,MAAM;gBACvBC,MAAM,WAAEP,4BAAAA,iBAAAA,QAASK,KAAK,qBAAdL,eAAgBQ,OAAO,oBAAI;YACrC;YACA,OAAO;gBAAElB,OAAO;gBAAMC,aAAa;gBAAeT;YAAG;QACvD;QACA,MAAM2B,wBAAiBT,2BAAAA,QAASU,IAAI,mBAAI,EAAE;QAI1C,yEAAyE;QACzE,sEAAsE;QACtE,sDAAsD;QACtD,MAAMC,OAAOF,cAAcG,MAAM,CAAC,CAACC,eACjCrE,yBAAyBqE,aAAaP,MAAM;QAE9C,KAAK,MAAMO,gBAAgBF,KAAKG,MAAM,GAAGH,OAAOF,cAAe;;gBAC1CI;YAAnB,KAAK,MAAME,kBAAQF,sBAAAA,aAAaG,KAAK,qBAAlBH,oBAAoBH,IAAI,oBAAI,EAAE,CAAE;oBACxBK;gBAAzB,IAAI1D,sBAAqB0D,cAAAA,KAAKE,KAAK,qBAAVF,YAAYG,EAAE,MAAM,WAAW;gBACxD,MAAMC,UAAUC,OAAOL,KAAKI,OAAO;gBACnC,IAAIC,OAAOC,QAAQ,CAACF,YAAYA,UAAU,GAAG;oBAC3C,OAAO;wBACL7B,OAAO,IAAIV,KAAKuC,UAAU,MAAMG,WAAW;wBAC3C/B,aAAa;wBACbT;oBACF;gBACF;YACF;QACF;QACA,OAAO;YAAEQ,OAAO;YAAMC,aAAa;YAAUT;QAAG;IAClD,EAAE,OAAOuB,OAAO;QACdD,QAAQC,KAAK,CAAC,mDAAmDA;QACjE,OAAO;YAAEf,OAAO;YAAMC,aAAa;YAAeT;QAAG;IACvD;AACF;AAEA,MAAMyC,QAAQ,CAACC;QACK;IAAlB,MAAMC,YAAaD,0BAAD,gBAAA,AAACA,MAA0CE,MAAM,qBAAjD,mBAACF;IACnB,IAAIC,qBAAqB7C,MAAM,OAAO6C,UAAUH,WAAW;IAC3D,IAAIE,iBAAiB5C,MAAM,OAAO4C,MAAMF,WAAW;IACnD,OAAO;AACT;AAEA;;;;;;;;;;CAUC,GACD,OAAO,eAAeK,qBACpBC,SAAsC,EACtCnD,KAAa,EACboD,KAAa;IAEb,IAAI;QACF,MAAMC,aAAa,IAAIlD,KAAK,GAAGiD,MAAM,iBAAiB,CAAC;QACvD,MAAME,WAAW,MAAMH,UACpBI,UAAU,CAAC,QACXC,GAAG,CAACxD,OACJuD,UAAU,CAAC,UACXE,KAAK,CAAC,aAAa,MAAMJ,YACzBK,OAAO,CAAC,aAAa,QACrBC,KAAK,CAACpE,YAAY,GAClBW,GAAG;QACN,MAAM0D,YAAYN,SAASO,IAAI,CAACxB,MAAM,GAAG9C;QACzC,MAAMsE,OAAOD,YAAYN,SAASO,IAAI,CAACC,KAAK,CAAC,GAAGvE,aAAa+D,SAASO,IAAI;QAC1E,MAAME,SAAS1E;QACf,MAAM2E,SAA0B,EAAE;QAClC,KAAK,MAAMR,OAAOK,KAAM;gBACAL;YAAtB,MAAM3B,SAAST,QAAOoC,WAAAA,IAAItD,GAAG,CAAC,qBAARsD,WAAqB;YAC3C,IAAI,AAAClE,0BAAgD2E,QAAQ,CAACpC,SAAS;gBACrEkC,MAAM,CAAClC,OAA8B,IAAI;YAC3C;YACA,IAAImC,OAAO3B,MAAM,GAAG7C,aAAa;oBAGhBgE,WAEWA,WACHA;gBALvBQ,OAAOE,IAAI,CAAC;oBACVzB,IAAIe,IAAIf,EAAE;oBACV0B,MAAM/C,QAAOoC,YAAAA,IAAItD,GAAG,CAAC,mBAARsD,YAAmB;oBAChC3B;oBACAuC,iBAAiBzB,QAAOa,YAAAA,IAAItD,GAAG,CAAC,8BAARsD,YAA8B,MAAM;oBAC5Da,cAAc1B,QAAOa,YAAAA,IAAItD,GAAG,CAAC,2BAARsD,YAA2B,MAAM;oBACtDc,WAAWxB,MAAMU,IAAItD,GAAG,CAAC;oBACzBqE,WACE,OAAOf,IAAItD,GAAG,CAAC,iBAAiB,WAC5BkB,OAAOoC,IAAItD,GAAG,CAAC,gBACf;gBACR;YACF;QACF;QACA,OAAO;YAAE6D;YAAQC;YAAQJ;QAAU;IACrC,EAAE,OAAOhC,OAAO;QACdD,QAAQC,KAAK,CAAC,mCAAmCA;QACjD,OAAO;IACT;AACF;AAEA;;;;;;;;CAQC,GACD,eAAe4C,kBACbrB,SAAsC,EACtCnD,KAAa,EACboD,KAAa;IAEb,IAAI;QACF,MAAMqB,QAAQ,MAAM/F,qBAAqByE,WAAWnD,OAAOoD;QAC3D,OAAOqB,MAAMC,IAAI,CAACZ,KAAK,CAAC,GAAGrE,WAAWkF,GAAG,CAAC,CAACC,MAAS,CAAA;gBAClDC,KAAKD,IAAIC,GAAG;gBACZC,MAAMF,IAAIE,IAAI;gBACdC,SAASH,IAAIG,OAAO;gBACpBC,YAAYJ,IAAII,UAAU;gBAC1BC,OAAOL,IAAIK,KAAK;gBAChBC,UAAUN,IAAIM,QAAQ;gBACtBC,UAAUP,IAAIO,QAAQ;gBACtBC,QAAQR,IAAIQ,MAAM;gBAClBC,QAAQT,IAAIS,MAAM;YACpB,CAAA;IACF,EAAE,OAAOzD,OAAO;QACdD,QAAQC,KAAK,CAAC,uCAAuCA;QACrD,OAAO,EAAE;IACX;AACF;AAEA,eAAe0D,QAAQC,OAAgB;QAUflE,wBAMDmE;IAfrB,MAAM,EACJC,MAAM,EACND,KAAK,EACLnE,SAASqE,UAAU,EACpB,GAAG,MAAMvH,qBAAqBoH;IAC/B,MAAMlE,UAAUqE;IAChB,IAAID,WAAW,OAAO;QACpB,OAAOE,SAASnE,IAAI,CAAC;YAAEI,OAAO;QAAqB,GAAG;YAAEC,QAAQ;QAAI;IACtE;IACA,MAAM+D,iBAAgBvE,yBAAAA,QAAQuE,aAAa,YAArBvE,yBAAyB;IAC/C,MAAMwE,UAAUD,cAAcE,UAAU,CAAC,aACrCF,cAAc9B,KAAK,CAAC,UAAUzB,MAAM,IACpC0D;IACJ,IAAI,CAACF,SACH,OAAOF,SAASnE,IAAI,CAAC;QAAEI,OAAO;IAAkB,GAAG;QAAEC,QAAQ;IAAI;IACnE,MAAM7B,QAAQoB,QAAOoE,eAAAA,MAAMxF,KAAK,YAAXwF,eAAe;IACpC,IAAI,CAACxF,OAAO,OAAO2F,SAASnE,IAAI,CAAC;QAAEI,OAAO;IAAgB,GAAG;QAAEC,QAAQ;IAAI;IAE3E,IAAI;YA0BImE,eAG+BC,iBA2BxBC,WAEOC;QAzDpB,MAAMC,UAAU,MAAM/H,cAAcgI,GAAG,GAAGC,IAAI,GAAGC,aAAa,CAACV;QAC/D,IAAI,CAACO,QAAQI,cAAc,IAAI,CAAClI,uBAAuB8H,UAAU;YAC/D,OAAOhI;QACT;QACA,IAAI,CAACgI,OAAO,CAAC,QAAQ,EAAE;YACrB,OAAOT,SAASnE,IAAI,CAAC;gBAAEI,OAAO;YAAa,GAAG;gBAAEC,QAAQ;YAAI;QAC9D;QAEA,MAAMzB,MAAM,IAAID;QAChB,MAAMiD,QAAQ5E,iBAAiB4B;QAC/B,MAAMqG,KAAKpI,cAAcgI,GAAG,GAAGlD,SAAS;QACxC,MAAMuD,SAASD,GAAGlD,UAAU,CAAC,QAAQC,GAAG,CAACxD;QAEzC,MAAM,CAACgG,SAASW,aAAaV,WAAWW,WAAW,GAAG,MAAMC,QAAQC,GAAG,CAAC;YACtEJ,OAAOxG,GAAG;YACVwG,OAAOnD,UAAU,CAACtF,2BAA2BuF,GAAG,CAACxF,oBAAoBkC,GAAG;YACxEwG,OAAOnD,UAAU,CAAC,eAAeC,GAAG,CAACJ,OAAOlD,GAAG;YAC/CwG,OAAOnD,UAAU,CAAC,SAASG,OAAO,CAAC,SAAS,QAAQC,KAAK,CAAC,GAAGzD,GAAG;SACjE;QACD,IAAI,CAAC8F,QAAQe,MAAM,EAAE;YACnB,OAAOpB,SAASnE,IAAI,CAAC;gBAAEI,OAAO;YAAuB,GAAG;gBAAEC,QAAQ;YAAI;QACxE;QACA,sEAAsE;QACtE,yEAAyE;QACzE,MAAMqE,MAAM,cACNF,gBAAAA,QAAQ/D,IAAI,cAAZ+D,gBAAkB,CAAC,GACnBW,YAAYI,MAAM,GAAGJ,YAAY1E,IAAI,KAAK,CAAC;QAEjD,MAAM+E,WAAWf,UAAUc,MAAM,IAAId,kBAAAA,UAAUhE,IAAI,cAAdgE,kBAAoB,CAAC,IAAK;QAE/D,MAAM,CAACgB,MAAMC,OAAOrG,MAAM,GAAG,MAAMgG,QAAQC,GAAG,CAAC;YAC7C5D,qBAAqBuD,IAAIzG,OAAOoD;YAChCoB,kBAAkBiC,IAAIzG,OAAOoD;YAC7BrD,cAAcC;SACf;QAED,MAAMmH,OAAOhI,sBAAsB+G,KAAcc,UAAU5G;QAC3D,qEAAqE;QACrE,oEAAoE;QACpE,mBAAmB;QACnB,MAAMgH,UAAUlI,yBAAyBgH,KAAciB,KAAKE,SAAS;QACrE,MAAMC,QAAQtI,uBAAuBkH,KAAc;YACjDrF,OAAOA,MAAMA,KAAK;YAClBC,aAAaD,MAAMC,WAAW;QAChC;QACA,MAAMyG,eAAezI,4BACnB4B,QAAQC,GAAG,CAAC6G,iCAAiC;QAE/C,MAAMrB,YAAYS,WAAW/C,IAAI,CAAC,EAAE;QACpC,gEAAgE;QAChE,mEAAmE;QACnE,kEAAkE;QAClE,MAAM4D,kBAAkBtB,YACpBtH,aAAa;YACXmB;YACA8E,IAAI,GAAGoB,YAAAA,GAAG,CAAC,OAAO,YAAXA,YAAsC;YAC7CA,KAAKA;YACL9C,OAAOhC,QAAO+E,iBAAAA,UAAUjG,GAAG,CAAC,oBAAdiG,iBAA0BA,UAAU1D,EAAE;YACpDiF,QAAQ,aACHxJ,iBAAiBiI,UAAUlE,IAAI;gBAClC0F,eAAeR,KAAKS,WAAW;;QAEnC,KACA;QACJ,MAAMC,SAAS5I,wBAAwB;YACrCiH,KAAKA;YACLiB;YACAC;YACAE;YACAC;YACAO,UAAU/I,qBAAqBoI,KAAKS,WAAW,EAAEL;YACjDQ,cAAc;gBACZ3E,KAAK,UAAEqE,mCAAAA,gBAAiBrE,KAAK,mBAAI;gBACjC4E,SAAS,WAAEP,mCAAAA,gBAAiBO,SAAS,oBAAI;gBACzCC,MAAM,WAAER,mCAAAA,gBAAiBQ,MAAM,oBAAI;YACrC;QACF;QAEA,MAAMC,OAA2B;YAC/B9E;YACAkE;YACAH;YACAC;YACAjC,UAAU1G,oBAAoBuI,4BAAAA,QAAU,CAAC,WAAW;YACpDC;YACAC;YACAW;YACAM,QAAQ/I,wBAAwB4H;QAClC;QACA,OAAOrB,SAASnE,IAAI,CAAC0G,MAAM;YAAErG,QAAQ;QAAI;IAC3C,EAAE,OAAOD,OAAO;QACd,2DAA2D;QAC3D,uEAAuE;QACvE,MAAMwG,kBAAkBzJ,uBAAuBiD;QAC/C,IAAIwG,iBAAiB,OAAOA;QAC5BzG,QAAQC,KAAK,CAAC,kBAAkBA;QAChC,OAAO+D,SAASnE,IAAI,CAAC;YAAEI,OAAO;QAAmB,GAAG;YAAEC,QAAQ;QAAI;IACpE;AACF;AAEA,SAASyD,WAAW+C,GAAG,GAAE"}
@@ -16,7 +16,7 @@
16
16
  */ import { pluginRequestFromWeb } from "@aglyn/aglyn/server";
17
17
  import { emailUnverifiedResponse, firebaseAdmin, isImpersonationSession } from "@aglyn/tenant-data-admin";
18
18
  import { invalidIdTokenResponse } from "@aglyn/tenant-data-admin/server/id-token-refusal";
19
- import { ASSIST_PROVIDER_COST_FIELD, assistProviderCostUsd } from "@aglyn/aglyn/app-utils/assist-credits";
19
+ import { ASSIST_PROVIDER_COST_FIELD, assistProviderCostUsd } from "../usage/assist-credits.js";
20
20
  import { assistUsageMonth } from "../usage/assist-usage.js";
21
21
  import { parseStaffOrgIds } from "../usage/staff-orgs-ai-spend.js";
22
22
  /**
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/server/ai-admin-orgs-spend.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 { pluginRequestFromWeb } from '@aglyn/aglyn/server'\nimport {\n emailUnverifiedResponse,\n firebaseAdmin,\n isImpersonationSession,\n} from '@aglyn/tenant-data-admin'\nimport { invalidIdTokenResponse } from '@aglyn/tenant-data-admin/server/id-token-refusal'\nimport {\n ASSIST_PROVIDER_COST_FIELD,\n assistProviderCostUsd,\n} from '@aglyn/aglyn/app-utils/assist-credits'\nimport { assistUsageMonth } from '../usage/assist-usage'\nimport {\n parseStaffOrgIds,\n type StaffOrgsAiSpendWire,\n} from '../usage/staff-orgs-ai-spend'\n\n/**\n * THE STAFF ORGANIZATIONS LIST'S AI SPEND (AGL-2984).\n *\n * `GET /api/ai/admin/orgs-spend?orgIds=a,b,…` answers this month's provider\n * spend for each org the list's page shows — the figures its AI spend column\n * draws and sorts by. Staff-gated exactly as `/api/ai/admin/org` is, and\n * read-only.\n *\n * ## The read\n *\n * ONE `getAll` over each named org's usage document for the current UTC\n * month, the key the usage writer files spend under — a single bounded\n * round trip for the page, never a read per row, and at most\n * `STAFF_ORGS_AI_SPEND_MAX_IDS` ids per request.\n *\n * An org with no document for the month answers `null`, never `0`, and a\n * read that FAILS answers `null` for every id: the list renders with the\n * column blank rather than failing because this read did.\n *\n * No `adminAudit` row: the answer is one dollar figure per workspace, with\n * nothing in it about a person.\n */\nasync function handler(request: Request): Promise<Response> {\n const {\n method,\n query,\n headers: rawHeaders,\n } = await pluginRequestFromWeb(request)\n const headers = rawHeaders as Partial<Record<string, string>>\n if (method !== 'GET') {\n return Response.json({ error: 'Method not allowed' }, { status: 405 })\n }\n const authorization = headers.authorization ?? ''\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n if (!idToken)\n return Response.json({ error: 'Unauthenticated' }, { status: 401 })\n const asked = parseStaffOrgIds(query['orgIds'])\n if (asked.error) {\n return Response.json({ error: asked.error }, { status: 400 })\n }\n\n try {\n const app = firebaseAdmin.app()\n const decoded = await app.auth().verifyIdToken(idToken)\n if (!decoded.email_verified && !isImpersonationSession(decoded)) {\n return emailUnverifiedResponse()\n }\n if (!decoded['staff']) {\n return Response.json({ error: 'Staff only' }, { status: 403 })\n }\n\n const month = assistUsageMonth()\n const db = app.firestore()\n const orgs = db.collection('orgs')\n const snapshots = await db\n .getAll(\n ...asked.orgIds.map((orgId) =>\n orgs.doc(orgId).collection('assistUsage').doc(month),\n ),\n )\n .catch((error: unknown) => {\n console.error('[ai/admin/orgs-spend] read failed', error)\n return null\n })\n const body: StaffOrgsAiSpendWire = {\n month,\n // `fromEntries`, so an id is always an own key of the answer.\n spendUsd: Object.fromEntries(\n asked.orgIds.map((orgId, index) => [\n orgId,\n spendOf(snapshots?.[index]),\n ]),\n ),\n }\n return Response.json(body, { status: 200 })\n } catch (error) {\n // An unverifiable credential is a 401, not a fault of ours\n // (AGL-1993). Null for anything else, so a real failure keeps its 500.\n const unauthenticated = invalidIdTokenResponse(error)\n if (unauthenticated) return unauthenticated\n console.error('[ai/admin/orgs-spend]', error)\n return Response.json({ error: 'AI spend lookup failed' }, { status: 500 })\n }\n}\n\n/**\n * A month document's PROVIDER spend, or `null` when there is no document to\n * read.\n *\n * The column this feeds is named for our money and is sorted on to find the\n * workspace spending it, so it is the provider figure and not what the\n * workspace drew (AGL-3015). A month closed before the two were split\n * answers with what it drew, which over-states the bill rather than hiding\n * it — the direction that still surfaces the workspace worth looking at.\n */\nfunction spendOf(\n snapshot: FirebaseFirestore.DocumentSnapshot | undefined,\n): number | null {\n if (!snapshot?.exists) return null\n const cost = assistProviderCostUsd(\n snapshot.get('estCostUsd'),\n snapshot.get(ASSIST_PROVIDER_COST_FIELD),\n )\n return Number.isFinite(cost) ? cost : null\n}\n\nexport { handler as GET }\n"],"names":["pluginRequestFromWeb","emailUnverifiedResponse","firebaseAdmin","isImpersonationSession","invalidIdTokenResponse","ASSIST_PROVIDER_COST_FIELD","assistProviderCostUsd","assistUsageMonth","parseStaffOrgIds","handler","request","headers","method","query","rawHeaders","Response","json","error","status","authorization","idToken","startsWith","slice","length","undefined","asked","app","decoded","auth","verifyIdToken","email_verified","month","db","firestore","orgs","collection","snapshots","getAll","orgIds","map","orgId","doc","catch","console","body","spendUsd","Object","fromEntries","index","spendOf","unauthenticated","snapshot","exists","cost","get","Number","isFinite","GET"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/server/ai-admin-orgs-spend.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 { pluginRequestFromWeb } from '@aglyn/aglyn/server'\nimport {\n emailUnverifiedResponse,\n firebaseAdmin,\n isImpersonationSession,\n} from '@aglyn/tenant-data-admin'\nimport { invalidIdTokenResponse } from '@aglyn/tenant-data-admin/server/id-token-refusal'\nimport {\n ASSIST_PROVIDER_COST_FIELD,\n assistProviderCostUsd,\n} from '../usage/assist-credits'\nimport { assistUsageMonth } from '../usage/assist-usage'\nimport {\n parseStaffOrgIds,\n type StaffOrgsAiSpendWire,\n} from '../usage/staff-orgs-ai-spend'\n\n/**\n * THE STAFF ORGANIZATIONS LIST'S AI SPEND (AGL-2984).\n *\n * `GET /api/ai/admin/orgs-spend?orgIds=a,b,…` answers this month's provider\n * spend for each org the list's page shows — the figures its AI spend column\n * draws and sorts by. Staff-gated exactly as `/api/ai/admin/org` is, and\n * read-only.\n *\n * ## The read\n *\n * ONE `getAll` over each named org's usage document for the current UTC\n * month, the key the usage writer files spend under — a single bounded\n * round trip for the page, never a read per row, and at most\n * `STAFF_ORGS_AI_SPEND_MAX_IDS` ids per request.\n *\n * An org with no document for the month answers `null`, never `0`, and a\n * read that FAILS answers `null` for every id: the list renders with the\n * column blank rather than failing because this read did.\n *\n * No `adminAudit` row: the answer is one dollar figure per workspace, with\n * nothing in it about a person.\n */\nasync function handler(request: Request): Promise<Response> {\n const {\n method,\n query,\n headers: rawHeaders,\n } = await pluginRequestFromWeb(request)\n const headers = rawHeaders as Partial<Record<string, string>>\n if (method !== 'GET') {\n return Response.json({ error: 'Method not allowed' }, { status: 405 })\n }\n const authorization = headers.authorization ?? ''\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n if (!idToken)\n return Response.json({ error: 'Unauthenticated' }, { status: 401 })\n const asked = parseStaffOrgIds(query['orgIds'])\n if (asked.error) {\n return Response.json({ error: asked.error }, { status: 400 })\n }\n\n try {\n const app = firebaseAdmin.app()\n const decoded = await app.auth().verifyIdToken(idToken)\n if (!decoded.email_verified && !isImpersonationSession(decoded)) {\n return emailUnverifiedResponse()\n }\n if (!decoded['staff']) {\n return Response.json({ error: 'Staff only' }, { status: 403 })\n }\n\n const month = assistUsageMonth()\n const db = app.firestore()\n const orgs = db.collection('orgs')\n const snapshots = await db\n .getAll(\n ...asked.orgIds.map((orgId) =>\n orgs.doc(orgId).collection('assistUsage').doc(month),\n ),\n )\n .catch((error: unknown) => {\n console.error('[ai/admin/orgs-spend] read failed', error)\n return null\n })\n const body: StaffOrgsAiSpendWire = {\n month,\n // `fromEntries`, so an id is always an own key of the answer.\n spendUsd: Object.fromEntries(\n asked.orgIds.map((orgId, index) => [\n orgId,\n spendOf(snapshots?.[index]),\n ]),\n ),\n }\n return Response.json(body, { status: 200 })\n } catch (error) {\n // An unverifiable credential is a 401, not a fault of ours\n // (AGL-1993). Null for anything else, so a real failure keeps its 500.\n const unauthenticated = invalidIdTokenResponse(error)\n if (unauthenticated) return unauthenticated\n console.error('[ai/admin/orgs-spend]', error)\n return Response.json({ error: 'AI spend lookup failed' }, { status: 500 })\n }\n}\n\n/**\n * A month document's PROVIDER spend, or `null` when there is no document to\n * read.\n *\n * The column this feeds is named for our money and is sorted on to find the\n * workspace spending it, so it is the provider figure and not what the\n * workspace drew (AGL-3015). A month closed before the two were split\n * answers with what it drew, which over-states the bill rather than hiding\n * it — the direction that still surfaces the workspace worth looking at.\n */\nfunction spendOf(\n snapshot: FirebaseFirestore.DocumentSnapshot | undefined,\n): number | null {\n if (!snapshot?.exists) return null\n const cost = assistProviderCostUsd(\n snapshot.get('estCostUsd'),\n snapshot.get(ASSIST_PROVIDER_COST_FIELD),\n )\n return Number.isFinite(cost) ? cost : null\n}\n\nexport { handler as GET }\n"],"names":["pluginRequestFromWeb","emailUnverifiedResponse","firebaseAdmin","isImpersonationSession","invalidIdTokenResponse","ASSIST_PROVIDER_COST_FIELD","assistProviderCostUsd","assistUsageMonth","parseStaffOrgIds","handler","request","headers","method","query","rawHeaders","Response","json","error","status","authorization","idToken","startsWith","slice","length","undefined","asked","app","decoded","auth","verifyIdToken","email_verified","month","db","firestore","orgs","collection","snapshots","getAll","orgIds","map","orgId","doc","catch","console","body","spendUsd","Object","fromEntries","index","spendOf","unauthenticated","snapshot","exists","cost","get","Number","isFinite","GET"],"mappings":"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"}
@@ -16,7 +16,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
16
16
  * limitations under the License.
17
17
  */ import { ORG_BILLING_DOC_ID, ORG_BILLING_SUBCOLLECTION, pluginRequestFromWeb } from "@aglyn/aglyn/server";
18
18
  import { emailUnverifiedResponse, firebaseAdmin, isImpersonationSession } from "@aglyn/tenant-data-admin";
19
- import { ASSIST_PROVIDER_COST_FIELD, assistCreditsFromUsd, assistProviderCostUsd } from "@aglyn/aglyn/app-utils/assist-credits";
19
+ import { ASSIST_PROVIDER_COST_FIELD, assistCreditsFromUsd, assistProviderCostUsd } from "../usage/assist-credits.js";
20
20
  import { hasAiAddon, resolveEffectivePlan } from "@aglyn/aglyn/app-utils/plan-entitlements";
21
21
  import { readPlatformFreeSpend } from "../usage/assist-free-taste.js";
22
22
  import { assistRefusalCounts } from "../usage/assist-refusals.js";