@aglyn/plugins-ai 1.0.0-beta.147 → 1.0.0-beta.150
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +17 -17
- package/src/lib/components/ai-brief-dialog.component.js +7 -1
- package/src/lib/components/ai-brief-dialog.component.js.map +1 -1
- package/src/lib/jobs/ai-job-page-sections.d.ts +32 -3
- package/src/lib/jobs/ai-job-page-sections.js +33 -3
- package/src/lib/jobs/ai-job-page-sections.js.map +1 -1
- package/src/lib/jobs/ai-job-plan-conformance.d.ts +33 -0
- package/src/lib/jobs/ai-job-plan-conformance.js +94 -1
- package/src/lib/jobs/ai-job-plan-conformance.js.map +1 -1
- package/src/lib/jobs/ai-job-plan-step.d.ts +14 -0
- package/src/lib/jobs/ai-job-plan-step.js +24 -0
- package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
- package/src/lib/jobs/ai-job-template-step.js +16 -2
- package/src/lib/jobs/ai-job-template-step.js.map +1 -1
- package/src/lib/model/ai-build-plan.d.ts +6 -3
- package/src/lib/model/ai-build-plan.js.map +1 -1
- package/src/lib/runtime/ai-products-generation.js +1 -1
- package/src/lib/runtime/ai-products-generation.js.map +1 -1
- package/src/lib/usage/ai-usage-by-user.d.ts +25 -3
- package/src/lib/usage/ai-usage-by-user.js +31 -7
- package/src/lib/usage/ai-usage-by-user.js.map +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/usage/ai-usage-by-user.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// Straight from the SDK, not the admin lib, for the reason `assist-usage.ts`\n// gives at its head: the statics need no app, and the admin lib would drag the\n// default-app initialization into every unit test that touches a counter.\nimport { FieldPath, FieldValue } from 'firebase-admin/firestore'\nimport {\n AI_USAGE_BY_USER_COLLECTION,\n AI_USAGE_BY_USER_MONTHS_COLLECTION,\n AI_USAGE_BY_USER_RETENTION_MONTHS,\n aiUsageByUserExpiry,\n aiUsageByUserMonthFrom,\n aiUsageShare,\n type AiUsageByUserMonth,\n type AiUsageKind,\n} from '../model/ai-usage-by-user'\nimport { AI_USAGE_TOKENS_FIELD } from '../model/ai-tokens'\nimport type { AiUsage } from '../providers/contract'\nimport { assistCreditsFromUsd } from '@aglyn/aglyn/app-utils/assist-credits'\n\n/**\n * PER-USER AI USAGE (AGL-2928): the writer and the readers.\n *\n * One document per person per month, under the org they spent in:\n *\n * orgs/{orgId}/aiUsageByUser/{uid}/months/{YYYY-MM}\n * { uid, month, credits, estCostUsd, requests, refusals,\n * tokens: { input, cached, cacheWrite, output },\n * byKind: { kind: credits }, byHost: { hostId: credits },\n * updatedAt, expiresAt }\n *\n * ## Written in the org rollup's own batch\n *\n * `recordUserAiUsage` takes the BATCH the org rollup is being written into\n * and adds this document to it, so the two cannot disagree: a batch commits\n * whole or not at all, and there is no path on which the workspace's month\n * moves and the person's does not. Both assist entrypoints hand it the batch\n * from inside `recordAssistCost` / `recordAssistExchange`, and the job\n * runner meters through the first of those, so a generation step lands here\n * with the job's kind and creator.\n *\n * ## No prose, no PII past the uid\n *\n * The exchange's verbatim half already expires at 180 days for the reason\n * `assist-usage.ts` gives; this rollup is integers keyed by an id, and it\n * is kept longer (`AI_USAGE_BY_USER_RETENTION_MONTHS`) because a workspace\n * admin's question is month-over-month. It is still a document ABOUT a\n * person, so it is readable only by the person and by members holding\n * `billing.view` or `org.auditLog` (the rules, and every route that serves\n * it), and an account erasure sweeps it (`eraseUserAiUsage`).\n */\n\n/** What one metered request attributes to a person. */\nexport interface RecordUserAiUsageInput {\n /** The person who asked. No uid, no attribution — the write is skipped. */\n uid: string | null | undefined\n /** The billing month the org rollup is being written into. */\n month: string\n /** The request's measured provider spend, as the org rollup counts it. */\n estCostUsd: number\n hostId: string | null | undefined\n kind: AiUsageKind\n /** The request's four token counts (AGL-2937), added to the month's `tokens`. */\n usage?: AiUsage\n}\n\n/**\n * A request's four token counts as increments, under the names every rollup\n * keeps them by (`model/ai-tokens.ts`).\n */\nexport function aiTokenIncrements(usage: AiUsage): Record<string, FirebaseFirestore.FieldValue> {\n const increment = FieldValue.increment\n return {\n input: increment(usage.inputTokens),\n cached: increment(usage.cacheReadTokens),\n cacheWrite: increment(usage.cacheWriteTokens),\n output: increment(usage.outputTokens),\n }\n}\n\nexport function userAiUsageMonthRef(\n orgRef: FirebaseFirestore.DocumentReference,\n uid: string,\n month: string,\n): FirebaseFirestore.DocumentReference {\n return orgRef\n .collection(AI_USAGE_BY_USER_COLLECTION)\n .doc(uid)\n .collection(AI_USAGE_BY_USER_MONTHS_COLLECTION)\n .doc(month)\n}\n\n/**\n * Fold one request into a person's month, on the caller's batch.\n *\n * Returns whether anything was queued: a request with no uid (a meter that\n * could not say who asked) attributes to nobody rather than to a placeholder\n * id that would rank on every table. `set(…, { merge: true })`, never\n * `update()`: the document does not exist before a person's first request\n * of the month, and a merge into a map field adds the key rather than\n * replacing the map.\n */\nexport function recordUserAiUsage(\n batch: FirebaseFirestore.WriteBatch,\n orgRef: FirebaseFirestore.DocumentReference,\n input: RecordUserAiUsageInput,\n): boolean {\n const uid = String(input.uid ?? '').trim()\n if (!uid) return false\n const estCostUsd =\n Number.isFinite(input.estCostUsd) && input.estCostUsd > 0\n ? input.estCostUsd\n : 0\n const credits = assistCreditsFromUsd(estCostUsd)\n const increment = FieldValue.increment\n const hostId = String(input.hostId ?? '').trim()\n batch.set(\n userAiUsageMonthRef(orgRef, uid, input.month),\n {\n uid,\n month: input.month,\n credits: increment(credits),\n estCostUsd: increment(estCostUsd),\n requests: increment(1),\n ...(input.usage ? { [AI_USAGE_TOKENS_FIELD]: aiTokenIncrements(input.usage) } : {}),\n byKind: { [input.kind]: increment(credits) },\n ...(hostId ? { byHost: { [hostId]: increment(credits) } } : {}),\n updatedAt: FieldValue.serverTimestamp(),\n // The retention window, as a Timestamp the TTL policy can act on.\n expiresAt: aiUsageByUserExpiry(input.month),\n },\n { merge: true },\n )\n return true\n}\n\n/**\n * The slice of a reservation a refusal is counted from: whether the gate\n * admitted the request, and the billing month it decided in. Structural, so\n * every door can hand over the reservation it already holds.\n */\nexport interface UserAiRefusalSource {\n allowed: boolean\n monthKey: string\n}\n\n/**\n * Count a refused reservation against the person who asked — the per-person\n * twin of `recordAssistRefusal`, and shaped the same way: fire-and-forget,\n * off the refusal's await path, swallowing every error, because a refusal\n * is the gate's cheap exit and a counter must not make it dearer than the\n * thing it counts.\n *\n * Takes the whole reservation rather than a month so every door calls it\n * the same way, once, right after reserving: an admitted reservation records\n * nothing, so the call needs no branch of its own and cannot drift from the\n * door's refusal branch. The month is the reservation's, not the clock's,\n * because that is the month the org's own refusal was counted in.\n */\nexport function recordUserAiRefusal(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n uid: string | null | undefined,\n reservation: UserAiRefusalSource,\n): void {\n if (reservation.allowed) return\n const subject = String(uid ?? '').trim()\n if (!subject) return\n try {\n const ref = userAiUsageMonthRef(\n firestore.collection('orgs').doc(orgId),\n subject,\n reservation.monthKey,\n )\n void Promise.resolve(\n ref.set(\n {\n uid: subject,\n month: reservation.monthKey,\n refusals: FieldValue.increment(1),\n updatedAt: FieldValue.serverTimestamp(),\n expiresAt: aiUsageByUserExpiry(reservation.monthKey),\n },\n { merge: true },\n ),\n ).catch(() => undefined)\n } catch {\n // A double without `set`, a ref that could not be built — the refusal\n // has already been decided and this must not change that.\n }\n}\n\n/** One roster row's month, with the person the roster names. */\nexport interface OrgAiUsageByUserRow extends AiUsageByUserMonth {\n /** The display name, else the email, else the uid — never blank. */\n name: string\n email: string | null\n role: string | null\n /** This person's share of the org's measured spend, in `[0, 1]`. */\n share: number\n}\n\nexport interface OrgAiUsageByUserMonth {\n month: string\n /** The org rollup's spend for the month — the denominator of every share. */\n orgEstCostUsd: number\n orgCredits: number\n /** Every current member with a document for the month, dearest first. */\n rows: OrgAiUsageByUserRow[]\n}\n\n/** How many references one `getAll` carries. */\nconst GET_ALL_CHUNK = 100\n\n/**\n * The month for every current member of the org, dearest first.\n *\n * Reads the ROSTER and then each member's month document by path, rather\n * than a collection-group query over `months`: the roster is the join the\n * table needs anyway (a uid is not a name), it is bounded by the seats the\n * plan sells, and a path read rides no index that has to be deployed before\n * a workspace can open its Usage page. A person who has left the org is not\n * listed — the roster is the set of people the reader may see — and their\n * documents expire on the retention window.\n */\nexport async function readOrgAiUsageByUser(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n month: string,\n): Promise<OrgAiUsageByUserMonth> {\n const orgRef = firestore.collection('orgs').doc(orgId)\n const [roster, rollup] = await Promise.all([\n orgRef.collection('members').get(),\n orgRef.collection('assistUsage').doc(month).get(),\n ])\n const orgEstCostUsd = Number(rollup.get('estCostUsd') ?? 0)\n const denominator =\n Number.isFinite(orgEstCostUsd) && orgEstCostUsd > 0 ? orgEstCostUsd : 0\n\n const members = roster.docs.map((doc) => ({\n uid: doc.id,\n email: typeof doc.get('email') === 'string' ? String(doc.get('email')) : null,\n displayName:\n typeof doc.get('displayName') === 'string' && String(doc.get('displayName')).trim()\n ? String(doc.get('displayName')).trim()\n : null,\n role: typeof doc.get('role') === 'string' ? String(doc.get('role')) : null,\n }))\n\n const rows: OrgAiUsageByUserRow[] = []\n for (let start = 0; start < members.length; start += GET_ALL_CHUNK) {\n const chunk = members.slice(start, start + GET_ALL_CHUNK)\n const snapshots = await firestore.getAll(\n ...chunk.map((member) => userAiUsageMonthRef(orgRef, member.uid, month)),\n )\n snapshots.forEach((snapshot, index) => {\n if (!snapshot.exists) return\n const member = chunk[index]\n const usage = aiUsageByUserMonthFrom(snapshot.data(), member.uid, month)\n rows.push({\n ...usage,\n name: member.displayName ?? member.email ?? member.uid,\n email: member.email,\n role: member.role,\n share: aiUsageShare(usage.estCostUsd, denominator),\n })\n })\n }\n rows.sort(\n (a, b) =>\n b.credits - a.credits ||\n b.requests - a.requests ||\n a.name.localeCompare(b.name),\n )\n return {\n month,\n orgEstCostUsd: denominator,\n orgCredits: assistCreditsFromUsd(denominator),\n rows,\n }\n}\n\n/**\n * One person's months in one org, newest first — the member card and the\n * staff page. Ordered on the document id, which IS the month key and so\n * needs no field every writer must remember.\n */\nexport async function readUserAiUsageMonths(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n uid: string,\n limit: number = AI_USAGE_BY_USER_RETENTION_MONTHS,\n): Promise<AiUsageByUserMonth[]> {\n const snapshot = await firestore\n .collection('orgs')\n .doc(orgId)\n .collection(AI_USAGE_BY_USER_COLLECTION)\n .doc(uid)\n .collection(AI_USAGE_BY_USER_MONTHS_COLLECTION)\n .orderBy(FieldPath.documentId(), 'desc')\n .limit(limit)\n .get()\n return snapshot.docs.map((doc) => aiUsageByUserMonthFrom(doc.data(), uid, doc.id))\n}\n\nexport interface EraseUserAiUsageResult {\n /** Orgs whose `aiUsageByUser/{uid}` subtree was deleted by path. */\n orgs: number\n /**\n * Month documents found by the collection-group sweep and deleted — the\n * ones in orgs the person had already left. `null` when the sweep could\n * not run (its index is not deployed), which is not zero.\n */\n sweptMonths: number | null\n}\n\n/**\n * Remove a person's usage documents on account erasure.\n *\n * Two passes, because the documents live under the ORG and are keyed by the\n * uid: `recursiveDelete(users/{uid})` cannot see them, and neither can a\n * walk of current memberships find the months left behind in an org the\n * person was removed from. So every org the erasure knows of is deleted by\n * path first — no index, cannot fail for want of one — and then a\n * collection-group query on `uid` catches the rest. The second pass is\n * reported as `null` rather than thrown when it cannot run: the erasure has\n * a legal clock, and the by-path pass has already removed everything a\n * current membership could reach.\n */\nexport async function eraseUserAiUsage(\n firestore: FirebaseFirestore.Firestore,\n uid: string,\n orgIds: readonly string[],\n): Promise<EraseUserAiUsageResult> {\n let orgs = 0\n for (const orgId of orgIds) {\n await firestore.recursiveDelete(\n firestore\n .collection('orgs')\n .doc(orgId)\n .collection(AI_USAGE_BY_USER_COLLECTION)\n .doc(uid),\n )\n orgs += 1\n }\n let sweptMonths: number | null = null\n try {\n const stray = await firestore\n .collectionGroup(AI_USAGE_BY_USER_MONTHS_COLLECTION)\n .where('uid', '==', uid)\n .get()\n sweptMonths = 0\n for (let start = 0; start < stray.docs.length; start += GET_ALL_CHUNK) {\n const batch = firestore.batch()\n for (const doc of stray.docs.slice(start, start + GET_ALL_CHUNK)) {\n batch.delete(doc.ref)\n sweptMonths += 1\n }\n await batch.commit()\n }\n } catch (error) {\n console.error(`eraseUserAiUsage: collection-group sweep failed for ${uid}`, error)\n }\n return { orgs, sweptMonths }\n}\n"],"names":["FieldPath","FieldValue","AI_USAGE_BY_USER_COLLECTION","AI_USAGE_BY_USER_MONTHS_COLLECTION","AI_USAGE_BY_USER_RETENTION_MONTHS","aiUsageByUserExpiry","aiUsageByUserMonthFrom","aiUsageShare","AI_USAGE_TOKENS_FIELD","assistCreditsFromUsd","aiTokenIncrements","usage","increment","input","inputTokens","cached","cacheReadTokens","cacheWrite","cacheWriteTokens","output","outputTokens","userAiUsageMonthRef","orgRef","uid","month","collection","doc","recordUserAiUsage","batch","String","trim","estCostUsd","Number","isFinite","credits","hostId","set","requests","byKind","kind","byHost","updatedAt","serverTimestamp","expiresAt","merge","recordUserAiRefusal","firestore","orgId","reservation","allowed","subject","ref","monthKey","Promise","resolve","refusals","catch","undefined","GET_ALL_CHUNK","readOrgAiUsageByUser","rollup","roster","all","get","orgEstCostUsd","denominator","members","docs","map","id","email","displayName","role","rows","start","length","chunk","slice","snapshots","getAll","member","forEach","snapshot","index","exists","data","push","name","share","sort","a","b","localeCompare","orgCredits","readUserAiUsageMonths","limit","orderBy","documentId","eraseUserAiUsage","orgIds","orgs","recursiveDelete","sweptMonths","stray","collectionGroup","where","delete","commit","error","console"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,6EAA6E;AAC7E,+EAA+E;AAC/E,0EAA0E;AAC1E,SAASA,SAAS,EAAEC,UAAU,QAAQ,2BAA0B;AAChE,SACEC,2BAA2B,EAC3BC,kCAAkC,EAClCC,iCAAiC,EACjCC,mBAAmB,EACnBC,sBAAsB,EACtBC,YAAY,QAGP,+BAA2B;AAClC,SAASC,qBAAqB,QAAQ,wBAAoB;AAE1D,SAASC,oBAAoB,QAAQ,wCAAuC;AAgD5E;;;CAGC,GACD,OAAO,SAASC,kBAAkBC,KAAc;IAC9C,MAAMC,YAAYX,WAAWW,SAAS;IACtC,OAAO;QACLC,OAAOD,UAAUD,MAAMG,WAAW;QAClCC,QAAQH,UAAUD,MAAMK,eAAe;QACvCC,YAAYL,UAAUD,MAAMO,gBAAgB;QAC5CC,QAAQP,UAAUD,MAAMS,YAAY;IACtC;AACF;AAEA,OAAO,SAASC,oBACdC,MAA2C,EAC3CC,GAAW,EACXC,KAAa;IAEb,OAAOF,OACJG,UAAU,CAACvB,6BACXwB,GAAG,CAACH,KACJE,UAAU,CAACtB,oCACXuB,GAAG,CAACF;AACT;AAEA;;;;;;;;;CASC,GACD,OAAO,SAASG,kBACdC,KAAmC,EACnCN,MAA2C,EAC3CT,KAA6B;QAEVA,YAQGA;IARtB,MAAMU,MAAMM,QAAOhB,aAAAA,MAAMU,GAAG,YAATV,aAAa,IAAIiB,IAAI;IACxC,IAAI,CAACP,KAAK,OAAO;IACjB,MAAMQ,aACJC,OAAOC,QAAQ,CAACpB,MAAMkB,UAAU,KAAKlB,MAAMkB,UAAU,GAAG,IACpDlB,MAAMkB,UAAU,GAChB;IACN,MAAMG,UAAUzB,qBAAqBsB;IACrC,MAAMnB,YAAYX,WAAWW,SAAS;IACtC,MAAMuB,SAASN,QAAOhB,gBAAAA,MAAMsB,MAAM,YAAZtB,gBAAgB,IAAIiB,IAAI;IAC9CF,MAAMQ,GAAG,CACPf,oBAAoBC,QAAQC,KAAKV,MAAMW,KAAK,GAC5C;QACED;QACAC,OAAOX,MAAMW,KAAK;QAClBU,SAAStB,UAAUsB;QACnBH,YAAYnB,UAAUmB;QACtBM,UAAUzB,UAAU;OAChBC,MAAMF,KAAK,GAAG;QAAE,CAACH,sBAAsB,EAAEE,kBAAkBG,MAAMF,KAAK;IAAE,IAAI,CAAC;QACjF2B,QAAQ;YAAE,CAACzB,MAAM0B,IAAI,CAAC,EAAE3B,UAAUsB;QAAS;OACvCC,SAAS;QAAEK,QAAQ;YAAE,CAACL,OAAO,EAAEvB,UAAUsB;QAAS;IAAE,IAAI,CAAC;QAC7DO,WAAWxC,WAAWyC,eAAe;QACrC,kEAAkE;QAClEC,WAAWtC,oBAAoBQ,MAAMW,KAAK;QAE5C;QAAEoB,OAAO;IAAK;IAEhB,OAAO;AACT;AAYA;;;;;;;;;;;;CAYC,GACD,OAAO,SAASC,oBACdC,SAAsC,EACtCC,KAAa,EACbxB,GAA8B,EAC9ByB,WAAgC;IAEhC,IAAIA,YAAYC,OAAO,EAAE;IACzB,MAAMC,UAAUrB,OAAON,cAAAA,MAAO,IAAIO,IAAI;IACtC,IAAI,CAACoB,SAAS;IACd,IAAI;QACF,MAAMC,MAAM9B,oBACVyB,UAAUrB,UAAU,CAAC,QAAQC,GAAG,CAACqB,QACjCG,SACAF,YAAYI,QAAQ;QAEtB,KAAKC,QAAQC,OAAO,CAClBH,IAAIf,GAAG,CACL;YACEb,KAAK2B;YACL1B,OAAOwB,YAAYI,QAAQ;YAC3BG,UAAUtD,WAAWW,SAAS,CAAC;YAC/B6B,WAAWxC,WAAWyC,eAAe;YACrCC,WAAWtC,oBAAoB2C,YAAYI,QAAQ;QACrD,GACA;YAAER,OAAO;QAAK,IAEhBY,KAAK,CAAC,IAAMC;IAChB,EAAE,eAAM;IACN,sEAAsE;IACtE,0DAA0D;IAC5D;AACF;AAqBA,8CAA8C,GAC9C,MAAMC,gBAAgB;AAEtB;;;;;;;;;;CAUC,GACD,OAAO,eAAeC,qBACpBb,SAAsC,EACtCC,KAAa,EACbvB,KAAa;QAOgBoC;IAL7B,MAAMtC,SAASwB,UAAUrB,UAAU,CAAC,QAAQC,GAAG,CAACqB;IAChD,MAAM,CAACc,QAAQD,OAAO,GAAG,MAAMP,QAAQS,GAAG,CAAC;QACzCxC,OAAOG,UAAU,CAAC,WAAWsC,GAAG;QAChCzC,OAAOG,UAAU,CAAC,eAAeC,GAAG,CAACF,OAAOuC,GAAG;KAChD;IACD,MAAMC,gBAAgBhC,QAAO4B,cAAAA,OAAOG,GAAG,CAAC,yBAAXH,cAA4B;IACzD,MAAMK,cACJjC,OAAOC,QAAQ,CAAC+B,kBAAkBA,gBAAgB,IAAIA,gBAAgB;IAExE,MAAME,UAAUL,OAAOM,IAAI,CAACC,GAAG,CAAC,CAAC1C,MAAS,CAAA;YACxCH,KAAKG,IAAI2C,EAAE;YACXC,OAAO,OAAO5C,IAAIqC,GAAG,CAAC,aAAa,WAAWlC,OAAOH,IAAIqC,GAAG,CAAC,YAAY;YACzEQ,aACE,OAAO7C,IAAIqC,GAAG,CAAC,mBAAmB,YAAYlC,OAAOH,IAAIqC,GAAG,CAAC,gBAAgBjC,IAAI,KAC7ED,OAAOH,IAAIqC,GAAG,CAAC,gBAAgBjC,IAAI,KACnC;YACN0C,MAAM,OAAO9C,IAAIqC,GAAG,CAAC,YAAY,WAAWlC,OAAOH,IAAIqC,GAAG,CAAC,WAAW;QACxE,CAAA;IAEA,MAAMU,OAA8B,EAAE;IACtC,IAAK,IAAIC,QAAQ,GAAGA,QAAQR,QAAQS,MAAM,EAAED,SAAShB,cAAe;QAClE,MAAMkB,QAAQV,QAAQW,KAAK,CAACH,OAAOA,QAAQhB;QAC3C,MAAMoB,YAAY,MAAMhC,UAAUiC,MAAM,IACnCH,MAAMR,GAAG,CAAC,CAACY,SAAW3D,oBAAoBC,QAAQ0D,OAAOzD,GAAG,EAAEC;QAEnEsD,UAAUG,OAAO,CAAC,CAACC,UAAUC;gBAMnBH,MAAAA;YALR,IAAI,CAACE,SAASE,MAAM,EAAE;YACtB,MAAMJ,SAASJ,KAAK,CAACO,MAAM;YAC3B,MAAMxE,QAAQL,uBAAuB4E,SAASG,IAAI,IAAIL,OAAOzD,GAAG,EAAEC;YAClEiD,KAAKa,IAAI,CAAC,aACL3E;gBACH4E,IAAI,GAAEP,QAAAA,sBAAAA,OAAOT,WAAW,YAAlBS,sBAAsBA,OAAOV,KAAK,YAAlCU,OAAsCA,OAAOzD,GAAG;gBACtD+C,OAAOU,OAAOV,KAAK;gBACnBE,MAAMQ,OAAOR,IAAI;gBACjBgB,OAAOjF,aAAaI,MAAMoB,UAAU,EAAEkC;;QAE1C;IACF;IACAQ,KAAKgB,IAAI,CACP,CAACC,GAAGC,IACFA,EAAEzD,OAAO,GAAGwD,EAAExD,OAAO,IACrByD,EAAEtD,QAAQ,GAAGqD,EAAErD,QAAQ,IACvBqD,EAAEH,IAAI,CAACK,aAAa,CAACD,EAAEJ,IAAI;IAE/B,OAAO;QACL/D;QACAwC,eAAeC;QACf4B,YAAYpF,qBAAqBwD;QACjCQ;IACF;AACF;AAEA;;;;CAIC,GACD,OAAO,eAAeqB,sBACpBhD,SAAsC,EACtCC,KAAa,EACbxB,GAAW,EACXwE,QAAgB3F,iCAAiC;IAEjD,MAAM8E,WAAW,MAAMpC,UACpBrB,UAAU,CAAC,QACXC,GAAG,CAACqB,OACJtB,UAAU,CAACvB,6BACXwB,GAAG,CAACH,KACJE,UAAU,CAACtB,oCACX6F,OAAO,CAAChG,UAAUiG,UAAU,IAAI,QAChCF,KAAK,CAACA,OACNhC,GAAG;IACN,OAAOmB,SAASf,IAAI,CAACC,GAAG,CAAC,CAAC1C,MAAQpB,uBAAuBoB,IAAI2D,IAAI,IAAI9D,KAAKG,IAAI2C,EAAE;AAClF;AAaA;;;;;;;;;;;;CAYC,GACD,OAAO,eAAe6B,iBACpBpD,SAAsC,EACtCvB,GAAW,EACX4E,MAAyB;IAEzB,IAAIC,OAAO;IACX,KAAK,MAAMrD,SAASoD,OAAQ;QAC1B,MAAMrD,UAAUuD,eAAe,CAC7BvD,UACGrB,UAAU,CAAC,QACXC,GAAG,CAACqB,OACJtB,UAAU,CAACvB,6BACXwB,GAAG,CAACH;QAET6E,QAAQ;IACV;IACA,IAAIE,cAA6B;IACjC,IAAI;QACF,MAAMC,QAAQ,MAAMzD,UACjB0D,eAAe,CAACrG,oCAChBsG,KAAK,CAAC,OAAO,MAAMlF,KACnBwC,GAAG;QACNuC,cAAc;QACd,IAAK,IAAI5B,QAAQ,GAAGA,QAAQ6B,MAAMpC,IAAI,CAACQ,MAAM,EAAED,SAAShB,cAAe;YACrE,MAAM9B,QAAQkB,UAAUlB,KAAK;YAC7B,KAAK,MAAMF,OAAO6E,MAAMpC,IAAI,CAACU,KAAK,CAACH,OAAOA,QAAQhB,eAAgB;gBAChE9B,MAAM8E,MAAM,CAAChF,IAAIyB,GAAG;gBACpBmD,eAAe;YACjB;YACA,MAAM1E,MAAM+E,MAAM;QACpB;IACF,EAAE,OAAOC,OAAO;QACdC,QAAQD,KAAK,CAAC,CAAC,oDAAoD,EAAErF,KAAK,EAAEqF;IAC9E;IACA,OAAO;QAAER;QAAME;IAAY;AAC7B"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/usage/ai-usage-by-user.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// Straight from the SDK, not the admin lib, for the reason `assist-usage.ts`\n// gives at its head: the statics need no app, and the admin lib would drag the\n// default-app initialization into every unit test that touches a counter.\nimport { FieldValue } from 'firebase-admin/firestore'\nimport {\n AI_USAGE_BY_USER_COLLECTION,\n AI_USAGE_BY_USER_MONTHS_COLLECTION,\n AI_USAGE_BY_USER_RETENTION_MONTHS,\n aiUsageByUserExpiry,\n aiUsageByUserMonthFrom,\n aiUsageMonthKeys,\n aiUsageShare,\n type AiUsageByUserMonth,\n type AiUsageKind,\n} from '../model/ai-usage-by-user'\nimport { AI_USAGE_TOKENS_FIELD } from '../model/ai-tokens'\nimport type { AiUsage } from '../providers/contract'\nimport { assistCreditsFromUsd } from '@aglyn/aglyn/app-utils/assist-credits'\n\n/**\n * PER-USER AI USAGE (AGL-2928): the writer and the readers.\n *\n * One document per person per month, under the org they spent in:\n *\n * orgs/{orgId}/aiUsageByUser/{uid}/months/{YYYY-MM}\n * { uid, month, credits, estCostUsd, requests, refusals,\n * tokens: { input, cached, cacheWrite, output },\n * byKind: { kind: credits }, byHost: { hostId: credits },\n * updatedAt, expiresAt }\n *\n * ## Written in the org rollup's own batch\n *\n * `recordUserAiUsage` takes the BATCH the org rollup is being written into\n * and adds this document to it, so the two cannot disagree: a batch commits\n * whole or not at all, and there is no path on which the workspace's month\n * moves and the person's does not. Both assist entrypoints hand it the batch\n * from inside `recordAssistCost` / `recordAssistExchange`, and the job\n * runner meters through the first of those, so a generation step lands here\n * with the job's kind and creator.\n *\n * ## No prose, no PII past the uid\n *\n * The exchange's verbatim half already expires at 180 days for the reason\n * `assist-usage.ts` gives; this rollup is integers keyed by an id, and it\n * is kept longer (`AI_USAGE_BY_USER_RETENTION_MONTHS`) because a workspace\n * admin's question is month-over-month. It is still a document ABOUT a\n * person, so it is readable only by the person and by members holding\n * `billing.view` or `org.auditLog` (the rules, and every route that serves\n * it), and an account erasure sweeps it (`eraseUserAiUsage`).\n */\n\n/** What one metered request attributes to a person. */\nexport interface RecordUserAiUsageInput {\n /** The person who asked. No uid, no attribution — the write is skipped. */\n uid: string | null | undefined\n /** The billing month the org rollup is being written into. */\n month: string\n /** The request's measured provider spend, as the org rollup counts it. */\n estCostUsd: number\n hostId: string | null | undefined\n kind: AiUsageKind\n /** The request's four token counts (AGL-2937), added to the month's `tokens`. */\n usage?: AiUsage\n}\n\n/**\n * A request's four token counts as increments, under the names every rollup\n * keeps them by (`model/ai-tokens.ts`).\n */\nexport function aiTokenIncrements(usage: AiUsage): Record<string, FirebaseFirestore.FieldValue> {\n const increment = FieldValue.increment\n return {\n input: increment(usage.inputTokens),\n cached: increment(usage.cacheReadTokens),\n cacheWrite: increment(usage.cacheWriteTokens),\n output: increment(usage.outputTokens),\n }\n}\n\nexport function userAiUsageMonthRef(\n orgRef: FirebaseFirestore.DocumentReference,\n uid: string,\n month: string,\n): FirebaseFirestore.DocumentReference {\n return orgRef\n .collection(AI_USAGE_BY_USER_COLLECTION)\n .doc(uid)\n .collection(AI_USAGE_BY_USER_MONTHS_COLLECTION)\n .doc(month)\n}\n\n/**\n * Fold one request into a person's month, on the caller's batch.\n *\n * Returns whether anything was queued: a request with no uid (a meter that\n * could not say who asked) attributes to nobody rather than to a placeholder\n * id that would rank on every table. `set(…, { merge: true })`, never\n * `update()`: the document does not exist before a person's first request\n * of the month, and a merge into a map field adds the key rather than\n * replacing the map.\n */\nexport function recordUserAiUsage(\n batch: FirebaseFirestore.WriteBatch,\n orgRef: FirebaseFirestore.DocumentReference,\n input: RecordUserAiUsageInput,\n): boolean {\n const uid = String(input.uid ?? '').trim()\n if (!uid) return false\n const estCostUsd =\n Number.isFinite(input.estCostUsd) && input.estCostUsd > 0\n ? input.estCostUsd\n : 0\n const credits = assistCreditsFromUsd(estCostUsd)\n const increment = FieldValue.increment\n const hostId = String(input.hostId ?? '').trim()\n batch.set(\n userAiUsageMonthRef(orgRef, uid, input.month),\n {\n uid,\n month: input.month,\n credits: increment(credits),\n estCostUsd: increment(estCostUsd),\n requests: increment(1),\n ...(input.usage ? { [AI_USAGE_TOKENS_FIELD]: aiTokenIncrements(input.usage) } : {}),\n byKind: { [input.kind]: increment(credits) },\n ...(hostId ? { byHost: { [hostId]: increment(credits) } } : {}),\n updatedAt: FieldValue.serverTimestamp(),\n // The retention window, as a Timestamp the TTL policy can act on.\n expiresAt: aiUsageByUserExpiry(input.month),\n },\n { merge: true },\n )\n return true\n}\n\n/**\n * The slice of a reservation a refusal is counted from: whether the gate\n * admitted the request, and the billing month it decided in. Structural, so\n * every door can hand over the reservation it already holds.\n */\nexport interface UserAiRefusalSource {\n allowed: boolean\n monthKey: string\n}\n\n/**\n * Count a refused reservation against the person who asked — the per-person\n * twin of `recordAssistRefusal`, and shaped the same way: fire-and-forget,\n * off the refusal's await path, swallowing every error, because a refusal\n * is the gate's cheap exit and a counter must not make it dearer than the\n * thing it counts.\n *\n * Takes the whole reservation rather than a month so every door calls it\n * the same way, once, right after reserving: an admitted reservation records\n * nothing, so the call needs no branch of its own and cannot drift from the\n * door's refusal branch. The month is the reservation's, not the clock's,\n * because that is the month the org's own refusal was counted in.\n */\nexport function recordUserAiRefusal(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n uid: string | null | undefined,\n reservation: UserAiRefusalSource,\n): void {\n if (reservation.allowed) return\n const subject = String(uid ?? '').trim()\n if (!subject) return\n try {\n const ref = userAiUsageMonthRef(\n firestore.collection('orgs').doc(orgId),\n subject,\n reservation.monthKey,\n )\n void Promise.resolve(\n ref.set(\n {\n uid: subject,\n month: reservation.monthKey,\n refusals: FieldValue.increment(1),\n updatedAt: FieldValue.serverTimestamp(),\n expiresAt: aiUsageByUserExpiry(reservation.monthKey),\n },\n { merge: true },\n ),\n ).catch(() => undefined)\n } catch {\n // A double without `set`, a ref that could not be built — the refusal\n // has already been decided and this must not change that.\n }\n}\n\n/** One roster row's month, with the person the roster names. */\nexport interface OrgAiUsageByUserRow extends AiUsageByUserMonth {\n /** The display name, else the email, else the uid — never blank. */\n name: string\n email: string | null\n role: string | null\n /** This person's share of the org's measured spend, in `[0, 1]`. */\n share: number\n}\n\nexport interface OrgAiUsageByUserMonth {\n month: string\n /** The org rollup's spend for the month — the denominator of every share. */\n orgEstCostUsd: number\n orgCredits: number\n /** Every current member with a document for the month, dearest first. */\n rows: OrgAiUsageByUserRow[]\n}\n\n/** How many references one `getAll` carries. */\nconst GET_ALL_CHUNK = 100\n\n/**\n * The month for every current member of the org, dearest first.\n *\n * Reads the ROSTER and then each member's month document by path, rather\n * than a collection-group query over `months`: the roster is the join the\n * table needs anyway (a uid is not a name), it is bounded by the seats the\n * plan sells, and a path read rides no index that has to be deployed before\n * a workspace can open its Usage page. A person who has left the org is not\n * listed — the roster is the set of people the reader may see — and their\n * documents expire on the retention window.\n */\nexport async function readOrgAiUsageByUser(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n month: string,\n): Promise<OrgAiUsageByUserMonth> {\n const orgRef = firestore.collection('orgs').doc(orgId)\n const [roster, rollup] = await Promise.all([\n orgRef.collection('members').get(),\n orgRef.collection('assistUsage').doc(month).get(),\n ])\n const orgEstCostUsd = Number(rollup.get('estCostUsd') ?? 0)\n const denominator =\n Number.isFinite(orgEstCostUsd) && orgEstCostUsd > 0 ? orgEstCostUsd : 0\n\n const members = roster.docs.map((doc) => ({\n uid: doc.id,\n email: typeof doc.get('email') === 'string' ? String(doc.get('email')) : null,\n displayName:\n typeof doc.get('displayName') === 'string' && String(doc.get('displayName')).trim()\n ? String(doc.get('displayName')).trim()\n : null,\n role: typeof doc.get('role') === 'string' ? String(doc.get('role')) : null,\n }))\n\n const rows: OrgAiUsageByUserRow[] = []\n for (let start = 0; start < members.length; start += GET_ALL_CHUNK) {\n const chunk = members.slice(start, start + GET_ALL_CHUNK)\n const snapshots = await firestore.getAll(\n ...chunk.map((member) => userAiUsageMonthRef(orgRef, member.uid, month)),\n )\n snapshots.forEach((snapshot, index) => {\n if (!snapshot.exists) return\n const member = chunk[index]\n const usage = aiUsageByUserMonthFrom(snapshot.data(), member.uid, month)\n rows.push({\n ...usage,\n name: member.displayName ?? member.email ?? member.uid,\n email: member.email,\n role: member.role,\n share: aiUsageShare(usage.estCostUsd, denominator),\n })\n })\n }\n rows.sort(\n (a, b) =>\n b.credits - a.credits ||\n b.requests - a.requests ||\n a.name.localeCompare(b.name),\n )\n return {\n month,\n orgEstCostUsd: denominator,\n orgCredits: assistCreditsFromUsd(denominator),\n rows,\n }\n}\n\n/**\n * One person's months in one org, newest first — the member card and the\n * staff page. The document id IS the month key, so no field every writer must\n * remember carries the order.\n *\n * ## Asked for by id, never ordered (AGL-3143 §13)\n *\n * This read `.orderBy(documentId(), 'desc').limit(n)`, which looks\n * like the obvious way to say \"newest first\" and **500s in production**:\n * Firestore indexes `__name__` ASCENDING automatically and descending NOT at\n * all, so the query answers `FAILED_PRECONDITION: the query requires an\n * index`. `GET /api/ai/usage?uid=…` returned 500 on every call and the member\n * card read \"Could not read this member's AI usage — a failed read, not zero\n * usage\", while the org-wide table beside it, which orders on a real field,\n * was fine.\n *\n * ⛔ The fix is deliberately NOT the index the error offers. A promotion ships\n * Vercel and nothing else — it deploys no rules and no indexes — so a fix that\n * needs one would land green, ship, and stay broken until someone remembered a\n * separate deploy. There is nothing to remember here.\n *\n * The whole key space is knowable without a query: `aiUsageMonthKeys` is the\n * retention window and the TTL reaps everything older, so the thirteen ids are\n * asked for directly and the ones that exist come back in the order they were\n * asked. Bounded at thirteen point reads, and it cannot return a month outside\n * retention, which is what the door promises its callers.\n */\nexport async function readUserAiUsageMonths(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n uid: string,\n limit: number = AI_USAGE_BY_USER_RETENTION_MONTHS,\n now: Date = new Date(),\n): Promise<AiUsageByUserMonth[]> {\n const months = firestore\n .collection('orgs')\n .doc(orgId)\n .collection(AI_USAGE_BY_USER_COLLECTION)\n .doc(uid)\n .collection(AI_USAGE_BY_USER_MONTHS_COLLECTION)\n const wanted = aiUsageMonthKeys(now)\n const snapshots = await firestore.getAll(...wanted.map((month) => months.doc(month)))\n return snapshots\n .filter((doc) => doc.exists)\n .slice(0, Math.max(1, limit))\n .map((doc) => aiUsageByUserMonthFrom(doc.data(), uid, doc.id))\n}\n\nexport interface EraseUserAiUsageResult {\n /** Orgs whose `aiUsageByUser/{uid}` subtree was deleted by path. */\n orgs: number\n /**\n * Month documents found by the collection-group sweep and deleted — the\n * ones in orgs the person had already left. `null` when the sweep could\n * not run (its index is not deployed), which is not zero.\n */\n sweptMonths: number | null\n}\n\n/**\n * Remove a person's usage documents on account erasure.\n *\n * Two passes, because the documents live under the ORG and are keyed by the\n * uid: `recursiveDelete(users/{uid})` cannot see them, and neither can a\n * walk of current memberships find the months left behind in an org the\n * person was removed from. So every org the erasure knows of is deleted by\n * path first — no index, cannot fail for want of one — and then a\n * collection-group query on `uid` catches the rest. The second pass is\n * reported as `null` rather than thrown when it cannot run: the erasure has\n * a legal clock, and the by-path pass has already removed everything a\n * current membership could reach.\n */\nexport async function eraseUserAiUsage(\n firestore: FirebaseFirestore.Firestore,\n uid: string,\n orgIds: readonly string[],\n): Promise<EraseUserAiUsageResult> {\n let orgs = 0\n for (const orgId of orgIds) {\n await firestore.recursiveDelete(\n firestore\n .collection('orgs')\n .doc(orgId)\n .collection(AI_USAGE_BY_USER_COLLECTION)\n .doc(uid),\n )\n orgs += 1\n }\n let sweptMonths: number | null = null\n try {\n const stray = await firestore\n .collectionGroup(AI_USAGE_BY_USER_MONTHS_COLLECTION)\n .where('uid', '==', uid)\n .get()\n sweptMonths = 0\n for (let start = 0; start < stray.docs.length; start += GET_ALL_CHUNK) {\n const batch = firestore.batch()\n for (const doc of stray.docs.slice(start, start + GET_ALL_CHUNK)) {\n batch.delete(doc.ref)\n sweptMonths += 1\n }\n await batch.commit()\n }\n } catch (error) {\n console.error(`eraseUserAiUsage: collection-group sweep failed for ${uid}`, error)\n }\n return { orgs, sweptMonths }\n}\n"],"names":["FieldValue","AI_USAGE_BY_USER_COLLECTION","AI_USAGE_BY_USER_MONTHS_COLLECTION","AI_USAGE_BY_USER_RETENTION_MONTHS","aiUsageByUserExpiry","aiUsageByUserMonthFrom","aiUsageMonthKeys","aiUsageShare","AI_USAGE_TOKENS_FIELD","assistCreditsFromUsd","aiTokenIncrements","usage","increment","input","inputTokens","cached","cacheReadTokens","cacheWrite","cacheWriteTokens","output","outputTokens","userAiUsageMonthRef","orgRef","uid","month","collection","doc","recordUserAiUsage","batch","String","trim","estCostUsd","Number","isFinite","credits","hostId","set","requests","byKind","kind","byHost","updatedAt","serverTimestamp","expiresAt","merge","recordUserAiRefusal","firestore","orgId","reservation","allowed","subject","ref","monthKey","Promise","resolve","refusals","catch","undefined","GET_ALL_CHUNK","readOrgAiUsageByUser","rollup","roster","all","get","orgEstCostUsd","denominator","members","docs","map","id","email","displayName","role","rows","start","length","chunk","slice","snapshots","getAll","member","forEach","snapshot","index","exists","data","push","name","share","sort","a","b","localeCompare","orgCredits","readUserAiUsageMonths","limit","now","Date","months","wanted","filter","Math","max","eraseUserAiUsage","orgIds","orgs","recursiveDelete","sweptMonths","stray","collectionGroup","where","delete","commit","error","console"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,6EAA6E;AAC7E,+EAA+E;AAC/E,0EAA0E;AAC1E,SAASA,UAAU,QAAQ,2BAA0B;AACrD,SACEC,2BAA2B,EAC3BC,kCAAkC,EAClCC,iCAAiC,EACjCC,mBAAmB,EACnBC,sBAAsB,EACtBC,gBAAgB,EAChBC,YAAY,QAGP,+BAA2B;AAClC,SAASC,qBAAqB,QAAQ,wBAAoB;AAE1D,SAASC,oBAAoB,QAAQ,wCAAuC;AAgD5E;;;CAGC,GACD,OAAO,SAASC,kBAAkBC,KAAc;IAC9C,MAAMC,YAAYZ,WAAWY,SAAS;IACtC,OAAO;QACLC,OAAOD,UAAUD,MAAMG,WAAW;QAClCC,QAAQH,UAAUD,MAAMK,eAAe;QACvCC,YAAYL,UAAUD,MAAMO,gBAAgB;QAC5CC,QAAQP,UAAUD,MAAMS,YAAY;IACtC;AACF;AAEA,OAAO,SAASC,oBACdC,MAA2C,EAC3CC,GAAW,EACXC,KAAa;IAEb,OAAOF,OACJG,UAAU,CAACxB,6BACXyB,GAAG,CAACH,KACJE,UAAU,CAACvB,oCACXwB,GAAG,CAACF;AACT;AAEA;;;;;;;;;CASC,GACD,OAAO,SAASG,kBACdC,KAAmC,EACnCN,MAA2C,EAC3CT,KAA6B;QAEVA,YAQGA;IARtB,MAAMU,MAAMM,QAAOhB,aAAAA,MAAMU,GAAG,YAATV,aAAa,IAAIiB,IAAI;IACxC,IAAI,CAACP,KAAK,OAAO;IACjB,MAAMQ,aACJC,OAAOC,QAAQ,CAACpB,MAAMkB,UAAU,KAAKlB,MAAMkB,UAAU,GAAG,IACpDlB,MAAMkB,UAAU,GAChB;IACN,MAAMG,UAAUzB,qBAAqBsB;IACrC,MAAMnB,YAAYZ,WAAWY,SAAS;IACtC,MAAMuB,SAASN,QAAOhB,gBAAAA,MAAMsB,MAAM,YAAZtB,gBAAgB,IAAIiB,IAAI;IAC9CF,MAAMQ,GAAG,CACPf,oBAAoBC,QAAQC,KAAKV,MAAMW,KAAK,GAC5C;QACED;QACAC,OAAOX,MAAMW,KAAK;QAClBU,SAAStB,UAAUsB;QACnBH,YAAYnB,UAAUmB;QACtBM,UAAUzB,UAAU;OAChBC,MAAMF,KAAK,GAAG;QAAE,CAACH,sBAAsB,EAAEE,kBAAkBG,MAAMF,KAAK;IAAE,IAAI,CAAC;QACjF2B,QAAQ;YAAE,CAACzB,MAAM0B,IAAI,CAAC,EAAE3B,UAAUsB;QAAS;OACvCC,SAAS;QAAEK,QAAQ;YAAE,CAACL,OAAO,EAAEvB,UAAUsB;QAAS;IAAE,IAAI,CAAC;QAC7DO,WAAWzC,WAAW0C,eAAe;QACrC,kEAAkE;QAClEC,WAAWvC,oBAAoBS,MAAMW,KAAK;QAE5C;QAAEoB,OAAO;IAAK;IAEhB,OAAO;AACT;AAYA;;;;;;;;;;;;CAYC,GACD,OAAO,SAASC,oBACdC,SAAsC,EACtCC,KAAa,EACbxB,GAA8B,EAC9ByB,WAAgC;IAEhC,IAAIA,YAAYC,OAAO,EAAE;IACzB,MAAMC,UAAUrB,OAAON,cAAAA,MAAO,IAAIO,IAAI;IACtC,IAAI,CAACoB,SAAS;IACd,IAAI;QACF,MAAMC,MAAM9B,oBACVyB,UAAUrB,UAAU,CAAC,QAAQC,GAAG,CAACqB,QACjCG,SACAF,YAAYI,QAAQ;QAEtB,KAAKC,QAAQC,OAAO,CAClBH,IAAIf,GAAG,CACL;YACEb,KAAK2B;YACL1B,OAAOwB,YAAYI,QAAQ;YAC3BG,UAAUvD,WAAWY,SAAS,CAAC;YAC/B6B,WAAWzC,WAAW0C,eAAe;YACrCC,WAAWvC,oBAAoB4C,YAAYI,QAAQ;QACrD,GACA;YAAER,OAAO;QAAK,IAEhBY,KAAK,CAAC,IAAMC;IAChB,EAAE,eAAM;IACN,sEAAsE;IACtE,0DAA0D;IAC5D;AACF;AAqBA,8CAA8C,GAC9C,MAAMC,gBAAgB;AAEtB;;;;;;;;;;CAUC,GACD,OAAO,eAAeC,qBACpBb,SAAsC,EACtCC,KAAa,EACbvB,KAAa;QAOgBoC;IAL7B,MAAMtC,SAASwB,UAAUrB,UAAU,CAAC,QAAQC,GAAG,CAACqB;IAChD,MAAM,CAACc,QAAQD,OAAO,GAAG,MAAMP,QAAQS,GAAG,CAAC;QACzCxC,OAAOG,UAAU,CAAC,WAAWsC,GAAG;QAChCzC,OAAOG,UAAU,CAAC,eAAeC,GAAG,CAACF,OAAOuC,GAAG;KAChD;IACD,MAAMC,gBAAgBhC,QAAO4B,cAAAA,OAAOG,GAAG,CAAC,yBAAXH,cAA4B;IACzD,MAAMK,cACJjC,OAAOC,QAAQ,CAAC+B,kBAAkBA,gBAAgB,IAAIA,gBAAgB;IAExE,MAAME,UAAUL,OAAOM,IAAI,CAACC,GAAG,CAAC,CAAC1C,MAAS,CAAA;YACxCH,KAAKG,IAAI2C,EAAE;YACXC,OAAO,OAAO5C,IAAIqC,GAAG,CAAC,aAAa,WAAWlC,OAAOH,IAAIqC,GAAG,CAAC,YAAY;YACzEQ,aACE,OAAO7C,IAAIqC,GAAG,CAAC,mBAAmB,YAAYlC,OAAOH,IAAIqC,GAAG,CAAC,gBAAgBjC,IAAI,KAC7ED,OAAOH,IAAIqC,GAAG,CAAC,gBAAgBjC,IAAI,KACnC;YACN0C,MAAM,OAAO9C,IAAIqC,GAAG,CAAC,YAAY,WAAWlC,OAAOH,IAAIqC,GAAG,CAAC,WAAW;QACxE,CAAA;IAEA,MAAMU,OAA8B,EAAE;IACtC,IAAK,IAAIC,QAAQ,GAAGA,QAAQR,QAAQS,MAAM,EAAED,SAAShB,cAAe;QAClE,MAAMkB,QAAQV,QAAQW,KAAK,CAACH,OAAOA,QAAQhB;QAC3C,MAAMoB,YAAY,MAAMhC,UAAUiC,MAAM,IACnCH,MAAMR,GAAG,CAAC,CAACY,SAAW3D,oBAAoBC,QAAQ0D,OAAOzD,GAAG,EAAEC;QAEnEsD,UAAUG,OAAO,CAAC,CAACC,UAAUC;gBAMnBH,MAAAA;YALR,IAAI,CAACE,SAASE,MAAM,EAAE;YACtB,MAAMJ,SAASJ,KAAK,CAACO,MAAM;YAC3B,MAAMxE,QAAQN,uBAAuB6E,SAASG,IAAI,IAAIL,OAAOzD,GAAG,EAAEC;YAClEiD,KAAKa,IAAI,CAAC,aACL3E;gBACH4E,IAAI,GAAEP,QAAAA,sBAAAA,OAAOT,WAAW,YAAlBS,sBAAsBA,OAAOV,KAAK,YAAlCU,OAAsCA,OAAOzD,GAAG;gBACtD+C,OAAOU,OAAOV,KAAK;gBACnBE,MAAMQ,OAAOR,IAAI;gBACjBgB,OAAOjF,aAAaI,MAAMoB,UAAU,EAAEkC;;QAE1C;IACF;IACAQ,KAAKgB,IAAI,CACP,CAACC,GAAGC,IACFA,EAAEzD,OAAO,GAAGwD,EAAExD,OAAO,IACrByD,EAAEtD,QAAQ,GAAGqD,EAAErD,QAAQ,IACvBqD,EAAEH,IAAI,CAACK,aAAa,CAACD,EAAEJ,IAAI;IAE/B,OAAO;QACL/D;QACAwC,eAAeC;QACf4B,YAAYpF,qBAAqBwD;QACjCQ;IACF;AACF;AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;CA0BC,GACD,OAAO,eAAeqB,sBACpBhD,SAAsC,EACtCC,KAAa,EACbxB,GAAW,EACXwE,QAAgB5F,iCAAiC,EACjD6F,MAAY,IAAIC,MAAM;IAEtB,MAAMC,SAASpD,UACZrB,UAAU,CAAC,QACXC,GAAG,CAACqB,OACJtB,UAAU,CAACxB,6BACXyB,GAAG,CAACH,KACJE,UAAU,CAACvB;IACd,MAAMiG,SAAS7F,iBAAiB0F;IAChC,MAAMlB,YAAY,MAAMhC,UAAUiC,MAAM,IAAIoB,OAAO/B,GAAG,CAAC,CAAC5C,QAAU0E,OAAOxE,GAAG,CAACF;IAC7E,OAAOsD,UACJsB,MAAM,CAAC,CAAC1E,MAAQA,IAAI0D,MAAM,EAC1BP,KAAK,CAAC,GAAGwB,KAAKC,GAAG,CAAC,GAAGP,QACrB3B,GAAG,CAAC,CAAC1C,MAAQrB,uBAAuBqB,IAAI2D,IAAI,IAAI9D,KAAKG,IAAI2C,EAAE;AAChE;AAaA;;;;;;;;;;;;CAYC,GACD,OAAO,eAAekC,iBACpBzD,SAAsC,EACtCvB,GAAW,EACXiF,MAAyB;IAEzB,IAAIC,OAAO;IACX,KAAK,MAAM1D,SAASyD,OAAQ;QAC1B,MAAM1D,UAAU4D,eAAe,CAC7B5D,UACGrB,UAAU,CAAC,QACXC,GAAG,CAACqB,OACJtB,UAAU,CAACxB,6BACXyB,GAAG,CAACH;QAETkF,QAAQ;IACV;IACA,IAAIE,cAA6B;IACjC,IAAI;QACF,MAAMC,QAAQ,MAAM9D,UACjB+D,eAAe,CAAC3G,oCAChB4G,KAAK,CAAC,OAAO,MAAMvF,KACnBwC,GAAG;QACN4C,cAAc;QACd,IAAK,IAAIjC,QAAQ,GAAGA,QAAQkC,MAAMzC,IAAI,CAACQ,MAAM,EAAED,SAAShB,cAAe;YACrE,MAAM9B,QAAQkB,UAAUlB,KAAK;YAC7B,KAAK,MAAMF,OAAOkF,MAAMzC,IAAI,CAACU,KAAK,CAACH,OAAOA,QAAQhB,eAAgB;gBAChE9B,MAAMmF,MAAM,CAACrF,IAAIyB,GAAG;gBACpBwD,eAAe;YACjB;YACA,MAAM/E,MAAMoF,MAAM;QACpB;IACF,EAAE,OAAOC,OAAO;QACdC,QAAQD,KAAK,CAAC,CAAC,oDAAoD,EAAE1F,KAAK,EAAE0F;IAC9E;IACA,OAAO;QAAER;QAAME;IAAY;AAC7B"}
|