estela 0.1.0

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 (186) hide show
  1. package/README.md +138 -0
  2. package/dist/billing/amortize.d.ts +44 -0
  3. package/dist/billing/amortize.d.ts.map +1 -0
  4. package/dist/billing/amortize.js +83 -0
  5. package/dist/billing/amortize.js.map +1 -0
  6. package/dist/billing/amortize.test.d.ts +2 -0
  7. package/dist/billing/amortize.test.d.ts.map +1 -0
  8. package/dist/billing/amortize.test.js +121 -0
  9. package/dist/billing/amortize.test.js.map +1 -0
  10. package/dist/billing/format.test.d.ts +2 -0
  11. package/dist/billing/format.test.d.ts.map +1 -0
  12. package/dist/billing/format.test.js +110 -0
  13. package/dist/billing/format.test.js.map +1 -0
  14. package/dist/billing/invoice.d.ts +52 -0
  15. package/dist/billing/invoice.d.ts.map +1 -0
  16. package/dist/billing/invoice.js +148 -0
  17. package/dist/billing/invoice.js.map +1 -0
  18. package/dist/billing/invoice.test.d.ts +2 -0
  19. package/dist/billing/invoice.test.d.ts.map +1 -0
  20. package/dist/billing/invoice.test.js +200 -0
  21. package/dist/billing/invoice.test.js.map +1 -0
  22. package/dist/billing/sessionize.d.ts +104 -0
  23. package/dist/billing/sessionize.d.ts.map +1 -0
  24. package/dist/billing/sessionize.js +0 -0
  25. package/dist/billing/sessionize.js.map +1 -0
  26. package/dist/billing/sessionize.test.d.ts +2 -0
  27. package/dist/billing/sessionize.test.d.ts.map +1 -0
  28. package/dist/billing/sessionize.test.js +67 -0
  29. package/dist/billing/sessionize.test.js.map +1 -0
  30. package/dist/cli.d.ts +4 -0
  31. package/dist/cli.d.ts.map +1 -0
  32. package/dist/cli.js +1091 -0
  33. package/dist/cli.js.map +1 -0
  34. package/dist/cloud/auth.d.ts +4 -0
  35. package/dist/cloud/auth.d.ts.map +1 -0
  36. package/dist/cloud/auth.js +98 -0
  37. package/dist/cloud/auth.js.map +1 -0
  38. package/dist/cloud/client.d.ts +12 -0
  39. package/dist/cloud/client.d.ts.map +1 -0
  40. package/dist/cloud/client.js +34 -0
  41. package/dist/cloud/client.js.map +1 -0
  42. package/dist/db/schema.d.ts +11 -0
  43. package/dist/db/schema.d.ts.map +1 -0
  44. package/dist/db/schema.js +424 -0
  45. package/dist/db/schema.js.map +1 -0
  46. package/dist/db/schema.test.d.ts +2 -0
  47. package/dist/db/schema.test.d.ts.map +1 -0
  48. package/dist/db/schema.test.js +335 -0
  49. package/dist/db/schema.test.js.map +1 -0
  50. package/dist/db/store.d.ts +113 -0
  51. package/dist/db/store.d.ts.map +1 -0
  52. package/dist/db/store.js +550 -0
  53. package/dist/db/store.js.map +1 -0
  54. package/dist/doctor.d.ts +24 -0
  55. package/dist/doctor.d.ts.map +1 -0
  56. package/dist/doctor.js +229 -0
  57. package/dist/doctor.js.map +1 -0
  58. package/dist/export/csv.d.ts +9 -0
  59. package/dist/export/csv.d.ts.map +1 -0
  60. package/dist/export/csv.js +81 -0
  61. package/dist/export/csv.js.map +1 -0
  62. package/dist/export/invoice-pdf.d.ts +21 -0
  63. package/dist/export/invoice-pdf.d.ts.map +1 -0
  64. package/dist/export/invoice-pdf.js +135 -0
  65. package/dist/export/invoice-pdf.js.map +1 -0
  66. package/dist/export/panel.d.ts +78 -0
  67. package/dist/export/panel.d.ts.map +1 -0
  68. package/dist/export/panel.js +693 -0
  69. package/dist/export/panel.js.map +1 -0
  70. package/dist/export/panel.test.d.ts +2 -0
  71. package/dist/export/panel.test.d.ts.map +1 -0
  72. package/dist/export/panel.test.js +273 -0
  73. package/dist/export/panel.test.js.map +1 -0
  74. package/dist/export/pdf.d.ts +32 -0
  75. package/dist/export/pdf.d.ts.map +1 -0
  76. package/dist/export/pdf.js +229 -0
  77. package/dist/export/pdf.js.map +1 -0
  78. package/dist/export/pdf.test.d.ts +2 -0
  79. package/dist/export/pdf.test.d.ts.map +1 -0
  80. package/dist/export/pdf.test.js +151 -0
  81. package/dist/export/pdf.test.js.map +1 -0
  82. package/dist/export/share.d.ts +42 -0
  83. package/dist/export/share.d.ts.map +1 -0
  84. package/dist/export/share.js +181 -0
  85. package/dist/export/share.js.map +1 -0
  86. package/dist/export/share.test.d.ts +2 -0
  87. package/dist/export/share.test.d.ts.map +1 -0
  88. package/dist/export/share.test.js +116 -0
  89. package/dist/export/share.test.js.map +1 -0
  90. package/dist/metrics/index.d.ts +101 -0
  91. package/dist/metrics/index.d.ts.map +1 -0
  92. package/dist/metrics/index.js +221 -0
  93. package/dist/metrics/index.js.map +1 -0
  94. package/dist/metrics/metrics.test.d.ts +2 -0
  95. package/dist/metrics/metrics.test.d.ts.map +1 -0
  96. package/dist/metrics/metrics.test.js +162 -0
  97. package/dist/metrics/metrics.test.js.map +1 -0
  98. package/dist/metrics/people.d.ts +46 -0
  99. package/dist/metrics/people.d.ts.map +1 -0
  100. package/dist/metrics/people.js +126 -0
  101. package/dist/metrics/people.js.map +1 -0
  102. package/dist/metrics/people.test.d.ts +2 -0
  103. package/dist/metrics/people.test.d.ts.map +1 -0
  104. package/dist/metrics/people.test.js +84 -0
  105. package/dist/metrics/people.test.js.map +1 -0
  106. package/dist/metrics/team.d.ts +41 -0
  107. package/dist/metrics/team.d.ts.map +1 -0
  108. package/dist/metrics/team.js +0 -0
  109. package/dist/metrics/team.js.map +1 -0
  110. package/dist/metrics/team.test.d.ts +2 -0
  111. package/dist/metrics/team.test.d.ts.map +1 -0
  112. package/dist/metrics/team.test.js +90 -0
  113. package/dist/metrics/team.test.js.map +1 -0
  114. package/dist/pricing/catalog.d.ts +39 -0
  115. package/dist/pricing/catalog.d.ts.map +1 -0
  116. package/dist/pricing/catalog.js +59 -0
  117. package/dist/pricing/catalog.js.map +1 -0
  118. package/dist/pricing/cost.d.ts +25 -0
  119. package/dist/pricing/cost.d.ts.map +1 -0
  120. package/dist/pricing/cost.js +50 -0
  121. package/dist/pricing/cost.js.map +1 -0
  122. package/dist/pricing/cost.test.d.ts +2 -0
  123. package/dist/pricing/cost.test.d.ts.map +1 -0
  124. package/dist/pricing/cost.test.js +74 -0
  125. package/dist/pricing/cost.test.js.map +1 -0
  126. package/dist/server.d.ts +15 -0
  127. package/dist/server.d.ts.map +1 -0
  128. package/dist/server.js +1141 -0
  129. package/dist/server.js.map +1 -0
  130. package/dist/setup.d.ts +70 -0
  131. package/dist/setup.d.ts.map +1 -0
  132. package/dist/setup.js +223 -0
  133. package/dist/setup.js.map +1 -0
  134. package/dist/setup.test.d.ts +2 -0
  135. package/dist/setup.test.d.ts.map +1 -0
  136. package/dist/setup.test.js +131 -0
  137. package/dist/setup.test.js.map +1 -0
  138. package/dist/watchers/claude.d.ts +34 -0
  139. package/dist/watchers/claude.d.ts.map +1 -0
  140. package/dist/watchers/claude.js +235 -0
  141. package/dist/watchers/claude.js.map +1 -0
  142. package/dist/watchers/claude.test.d.ts +2 -0
  143. package/dist/watchers/claude.test.d.ts.map +1 -0
  144. package/dist/watchers/claude.test.js +180 -0
  145. package/dist/watchers/claude.test.js.map +1 -0
  146. package/dist/watchers/git.d.ts +42 -0
  147. package/dist/watchers/git.d.ts.map +1 -0
  148. package/dist/watchers/git.js +218 -0
  149. package/dist/watchers/git.js.map +1 -0
  150. package/dist/watchers/git.test.d.ts +2 -0
  151. package/dist/watchers/git.test.d.ts.map +1 -0
  152. package/dist/watchers/git.test.js +73 -0
  153. package/dist/watchers/git.test.js.map +1 -0
  154. package/dist/watchers/identity.d.ts +34 -0
  155. package/dist/watchers/identity.d.ts.map +1 -0
  156. package/dist/watchers/identity.js +85 -0
  157. package/dist/watchers/identity.js.map +1 -0
  158. package/dist/watchers/identity.test.d.ts +2 -0
  159. package/dist/watchers/identity.test.d.ts.map +1 -0
  160. package/dist/watchers/identity.test.js +48 -0
  161. package/dist/watchers/identity.test.js.map +1 -0
  162. package/dist/web-ui.test.d.ts +2 -0
  163. package/dist/web-ui.test.d.ts.map +1 -0
  164. package/dist/web-ui.test.js +312 -0
  165. package/dist/web-ui.test.js.map +1 -0
  166. package/node_modules/@estela/shared/dist/index.d.ts +3 -0
  167. package/node_modules/@estela/shared/dist/index.d.ts.map +1 -0
  168. package/node_modules/@estela/shared/dist/index.js +19 -0
  169. package/node_modules/@estela/shared/dist/index.js.map +1 -0
  170. package/node_modules/@estela/shared/dist/money.d.ts +71 -0
  171. package/node_modules/@estela/shared/dist/money.d.ts.map +1 -0
  172. package/node_modules/@estela/shared/dist/money.js +171 -0
  173. package/node_modules/@estela/shared/dist/money.js.map +1 -0
  174. package/node_modules/@estela/shared/dist/types.d.ts +365 -0
  175. package/node_modules/@estela/shared/dist/types.d.ts.map +1 -0
  176. package/node_modules/@estela/shared/dist/types.js +24 -0
  177. package/node_modules/@estela/shared/dist/types.js.map +1 -0
  178. package/node_modules/@estela/shared/package.json +5 -0
  179. package/package.json +41 -0
  180. package/web/app.css +749 -0
  181. package/web/app.js +1257 -0
  182. package/web/apple-touch-icon.png +0 -0
  183. package/web/favicon.png +0 -0
  184. package/web/index.html +132 -0
  185. package/web/lib.js +94 -0
  186. package/web/logo-mark.png +0 -0
package/README.md ADDED
@@ -0,0 +1,138 @@
1
+ # Estela
2
+
3
+ Registro de horas de desarrollo que **no hay que rellenar**. Lee lo que tus
4
+ agentes de IA y tu Git ya escribieron en disco y reconstruye en qué se te fue el
5
+ tiempo, con lo que costó de IA.
6
+
7
+ ```sh
8
+ npx estela setup
9
+ ```
10
+
11
+ Veinte segundos después tienes tu historial de los últimos meses. Sin cuenta,
12
+ sin tarjeta y sin que nada salga de tu máquina.
13
+
14
+ ---
15
+
16
+ ## Por qué existe
17
+
18
+ Un cronómetro que hay que acordarse de arrancar siempre falla. Y con un agente
19
+ de IA el tiempo ya no se mide tecleando: se va en escribir el prompt, leer lo
20
+ que devuelve y probarlo.
21
+
22
+ Pero ese trabajo **deja rastro**. Claude Code guarda cada sesión en
23
+ `~/.claude/projects/` con su modelo y sus tokens exactos. Git guarda cuándo
24
+ commiteaste y qué. Estela lee ambas cosas y las cruza.
25
+
26
+ Lo que no hace:
27
+
28
+ - **No instala hooks.** Tu `husky` y tu `lefthook` siguen intactos, y tus
29
+ mensajes de commit no se tocan. Un identificador metido en el historial es
30
+ irreversible una vez subido.
31
+ - **No inspecciona procesos ni la terminal.** Lee ficheros que ya existen.
32
+ - **No guarda el contenido de tus prompts.** Solo cuándo, cuánto y con qué
33
+ modelo.
34
+ - **No manda nada a ningún sitio.** El plan gratuito es local entero.
35
+
36
+ ## Empezar
37
+
38
+ Necesitas **Node 22.5 o superior** (por `node:sqlite`).
39
+
40
+ ```sh
41
+ npx estela setup # detecta agentes y repositorios, reconstruye tu historial
42
+ npx estela web # abre el panel en http://localhost:4319
43
+ npx estela doctor # revisa los datos y avisa de lo que está mal
44
+ ```
45
+
46
+ `setup` no pregunta nada y no pisa lo que hayas configurado a mano: se puede
47
+ volver a ejecutar.
48
+
49
+ ## Facturar a un cliente
50
+
51
+ Los proyectos se crean como internos y sin tarifa, porque inventarla daría
52
+ cifras falsas el primer minuto. Cuando uno sea de un cliente de verdad:
53
+
54
+ ```sh
55
+ estela client add --id acme --name "ACME" --currency EUR
56
+ estela project add --id acme-web --client acme --name "Web de ACME" --repo ~/dev/acme-web
57
+ estela rate set --project acme-web --rate 50
58
+
59
+ estela author --project acme-web # con qué correo commiteas ahí
60
+ estela report --project acme-web --cutoff 2026-08-31 --dry-run
61
+ ```
62
+
63
+ **`estela author` importa más de lo que parece.** En el repositorio de un
64
+ cliente casi nunca commiteas con tu correo global, y sin decirlo se capturan
65
+ tres commits de mil setecientos.
66
+
67
+ ## Compartir el avance con tu cliente
68
+
69
+ ```sh
70
+ estela publish --project acme-web --base-url https://tu-dominio.com
71
+ ```
72
+
73
+ Genera un HTML autocontenido con un enlace no adivinable. Enseña horas y
74
+ commits; **nunca tu tarifa ni tu consumo de IA**, porque mientras la pagues tú
75
+ ese gasto es tuyo y un cliente que sabe qué parte generó una IA tiene un
76
+ argumento nuevo para negociar tu tarifa.
77
+
78
+ Al republicar, pasa el mismo `--token` o tu cliente se queda con un enlace
79
+ muerto.
80
+
81
+ ## El coste de la IA
82
+
83
+ Con una cuota plana el gasto real no es la suma de los tokens: es la cuota
84
+ repartida entre lo que consumiste.
85
+
86
+ ```sh
87
+ estela subscription add --id max --name "Claude Max" --fee 100
88
+ estela ai-cost
89
+ ```
90
+
91
+ La caché se cuenta aparte porque suele ser la mayor parte de la factura —
92
+ ignorarla subestimaba el gasto cinco veces.
93
+
94
+ ## Horas que ningún import va a deducir
95
+
96
+ Reuniones, desplazamientos, investigación, y desarrollo sin agente que tampoco
97
+ dejó commits:
98
+
99
+ ```sh
100
+ estela log --project acme-web --hours 1.5 --kind meeting --what "Seguimiento semanal"
101
+ ```
102
+
103
+ Un import nunca las toca.
104
+
105
+ ## Trabajo sin agente
106
+
107
+ Si programaste a mano, tus commits siguen siendo un rastro: Estela deduce el
108
+ tiempo de ellos y lo marca como **estimado**, para que sepas qué parte de tu
109
+ parte está medida y cuál supuesta. La estimación se queda corta a propósito:
110
+ estas horas acaban en un informe que alguien paga.
111
+
112
+ ## Planes
113
+
114
+ | | Free | Pro | Teams |
115
+ |---|---|---|---|
116
+ | Todo lo de arriba, en local | ✓ | ✓ | ✓ |
117
+ | Sincronizar varias máquinas | — | ✓ | ✓ |
118
+ | Paneles ilimitados y alojados | 1 | ✓ | ✓ |
119
+ | Horas del equipo **medidas** | — | — | ✓ |
120
+ | Presupuesto de IA por proyecto | — | — | ✓ |
121
+
122
+ Pro es una persona en varias máquinas; Teams son varias personas. El coste de
123
+ IA se informa **por proyecto, nunca por persona**: lo que cada cual gasta de su
124
+ bolsillo es suyo.
125
+
126
+ Más en [getestela.dev](https://getestela.dev).
127
+
128
+ ## Desarrollo
129
+
130
+ ```sh
131
+ npm install
132
+ npm run build
133
+ node --test packages/daemon/dist/**/*.test.js
134
+ ```
135
+
136
+ Node 22 y **cero dependencias de runtime**, a propósito: nadie instala un
137
+ programa que lee sus transcripts si no puede auditarlo, y una lista de
138
+ dependencias vacía se audita en una tarde.
@@ -0,0 +1,44 @@
1
+ import type { AiCost, AmortizedShare, Money, Subscription } from "@estela/shared";
2
+ /**
3
+ * Reparto de una cuota fija entre proyectos.
4
+ *
5
+ * Con una suscripción de precio fijo, sumar el coste por token da un número que
6
+ * no salió de tu bolsillo. Lo que gastaste de verdad es la cuota. Para saber
7
+ * cuánto de esa cuota "se comió" un proyecto, se reparte en proporción a lo que
8
+ * consumió, usando la tarifa API equivalente como unidad de medida.
9
+ *
10
+ * parte_del_proyecto = cuota_mensual x (consumo_proyecto / consumo_total_mes)
11
+ *
12
+ * El denominador es **todo** tu consumo del mes, incluido el de proyectos no
13
+ * facturables. Si tus experimentos personales se comieron la mitad de la cuota,
14
+ * a los clientes solo les corresponde la otra mitad. Repartir únicamente entre
15
+ * lo facturable les cargaría un gasto que no generaron.
16
+ */
17
+ export interface ConsumptionRow {
18
+ readonly projectId: string;
19
+ /** Mes en formato YYYY-MM. */
20
+ readonly month: string;
21
+ readonly consumption: AiCost;
22
+ }
23
+ /**
24
+ * @param monthTotals Consumo total del mes en **toda** tu máquina, incluido el
25
+ * trabajo que aún no has asignado a ningún proyecto. Es el denominador
26
+ * correcto: la cuota la pagaste entera, la usaras en lo que la usaras.
27
+ *
28
+ * Sin este dato, el denominador sería solo la suma de los proyectos
29
+ * configurados y el primero que registres cargaría con toda la cuota, aunque
30
+ * la mayor parte de tu consumo estuviera en repos sin configurar.
31
+ */
32
+ export declare function amortize(rows: readonly ConsumptionRow[], subscriptions: readonly Subscription[], monthTotals?: ReadonlyMap<string, AiCost>): AmortizedShare[];
33
+ /**
34
+ * Suma de las cuotas vigentes en un mes.
35
+ *
36
+ * Se suman porque es habitual pagar varias a la vez (Claude Max y Cursor, por
37
+ * ejemplo) y todas alimentan el mismo trabajo.
38
+ */
39
+ export declare function feeForMonth(subscriptions: readonly Subscription[], month: string): Money | null;
40
+ /** Mes (YYYY-MM, en UTC) de una fecha. */
41
+ export declare function monthOf(date: Date): string;
42
+ /** Lo que le corresponde a un proyecto en un rango de meses. */
43
+ export declare function shareForProject(shares: readonly AmortizedShare[], projectId: string, months: readonly string[]): Money | null;
44
+ //# sourceMappingURL=amortize.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"amortize.d.ts","sourceRoot":"","sources":["../../src/billing/amortize.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,cAAc,EAAE,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAGlF;;;;;;;;;;;;;;GAcG;AAEH,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,8BAA8B;IAC9B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B;AAED;;;;;;;;GAQG;AACH,wBAAgB,QAAQ,CACtB,IAAI,EAAE,SAAS,cAAc,EAAE,EAC/B,aAAa,EAAE,SAAS,YAAY,EAAE,EACtC,WAAW,CAAC,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,GACxC,cAAc,EAAE,CAiClB;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,aAAa,EAAE,SAAS,YAAY,EAAE,EAAE,KAAK,EAAE,MAAM,GAAG,KAAK,GAAG,IAAI,CAkB/F;AAED,0CAA0C;AAC1C,wBAAgB,OAAO,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,CAE1C;AAED,gEAAgE;AAChE,wBAAgB,eAAe,CAC7B,MAAM,EAAE,SAAS,cAAc,EAAE,EACjC,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,SAAS,MAAM,EAAE,GACxB,KAAK,GAAG,IAAI,CAMd"}
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.amortize = amortize;
4
+ exports.feeForMonth = feeForMonth;
5
+ exports.monthOf = monthOf;
6
+ exports.shareForProject = shareForProject;
7
+ const shared_1 = require("@estela/shared");
8
+ /**
9
+ * @param monthTotals Consumo total del mes en **toda** tu máquina, incluido el
10
+ * trabajo que aún no has asignado a ningún proyecto. Es el denominador
11
+ * correcto: la cuota la pagaste entera, la usaras en lo que la usaras.
12
+ *
13
+ * Sin este dato, el denominador sería solo la suma de los proyectos
14
+ * configurados y el primero que registres cargaría con toda la cuota, aunque
15
+ * la mayor parte de tu consumo estuviera en repos sin configurar.
16
+ */
17
+ function amortize(rows, subscriptions, monthTotals) {
18
+ const byMonth = new Map();
19
+ for (const row of rows) {
20
+ const bucket = byMonth.get(row.month);
21
+ if (bucket)
22
+ bucket.push(row);
23
+ else
24
+ byMonth.set(row.month, [row]);
25
+ }
26
+ const shares = [];
27
+ for (const [month, monthRows] of byMonth) {
28
+ const fee = feeForMonth(subscriptions, month);
29
+ const assigned = monthRows.reduce((sum, r) => sum + r.consumption.microUsd, 0);
30
+ // Nunca por debajo de lo ya asignado: un total incoherente no puede hacer
31
+ // que las partes sumen más del 100%.
32
+ const total = Math.max(monthTotals?.get(month)?.microUsd ?? 0, assigned);
33
+ for (const row of monthRows) {
34
+ const share = total === 0 ? 0 : row.consumption.microUsd / total;
35
+ shares.push({
36
+ projectId: row.projectId,
37
+ month,
38
+ consumption: row.consumption,
39
+ monthTotal: { microUsd: total },
40
+ share,
41
+ amount: fee
42
+ ? (0, shared_1.money)(Math.round(fee.amount * share), fee.currency)
43
+ : (0, shared_1.money)(0, "USD"),
44
+ });
45
+ }
46
+ }
47
+ return shares.sort((a, b) => a.month.localeCompare(b.month) || a.projectId.localeCompare(b.projectId));
48
+ }
49
+ /**
50
+ * Suma de las cuotas vigentes en un mes.
51
+ *
52
+ * Se suman porque es habitual pagar varias a la vez (Claude Max y Cursor, por
53
+ * ejemplo) y todas alimentan el mismo trabajo.
54
+ */
55
+ function feeForMonth(subscriptions, month) {
56
+ const start = new Date(`${month}-01T00:00:00Z`);
57
+ const active = subscriptions.filter((s) => s.effectiveFrom <= start && (s.effectiveTo === null || start < s.effectiveTo));
58
+ if (active.length === 0)
59
+ return null;
60
+ const currency = active[0].monthlyFee.currency;
61
+ let total = 0;
62
+ for (const s of active) {
63
+ if (s.monthlyFee.currency !== currency) {
64
+ throw new TypeError(`Suscripciones en monedas distintas en ${month} (${currency} y ${s.monthlyFee.currency}). ` +
65
+ `Únifica la moneda de las suscripciones: no se convierten automáticamente.`);
66
+ }
67
+ total += s.monthlyFee.amount;
68
+ }
69
+ return (0, shared_1.money)(total, currency);
70
+ }
71
+ /** Mes (YYYY-MM, en UTC) de una fecha. */
72
+ function monthOf(date) {
73
+ return date.toISOString().slice(0, 7);
74
+ }
75
+ /** Lo que le corresponde a un proyecto en un rango de meses. */
76
+ function shareForProject(shares, projectId, months) {
77
+ const relevant = shares.filter((s) => s.projectId === projectId && months.includes(s.month));
78
+ if (relevant.length === 0)
79
+ return null;
80
+ const currency = relevant[0].amount.currency;
81
+ return (0, shared_1.money)(relevant.reduce((sum, s) => sum + s.amount.amount, 0), currency);
82
+ }
83
+ //# sourceMappingURL=amortize.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"amortize.js","sourceRoot":"","sources":["../../src/billing/amortize.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=amortize.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"amortize.test.d.ts","sourceRoot":"","sources":["../../src/billing/amortize.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,121 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const strict_1 = __importDefault(require("node:assert/strict"));
7
+ const node_test_1 = require("node:test");
8
+ const shared_1 = require("@estela/shared");
9
+ const amortize_js_1 = require("./amortize.js");
10
+ const CLAUDE_MAX = {
11
+ id: "claude-max", name: "Claude Max",
12
+ monthlyFee: (0, shared_1.parseMoney)("200", "USD"),
13
+ effectiveFrom: new Date("2025-01-01T00:00:00Z"), effectiveTo: null,
14
+ };
15
+ (0, node_test_1.test)("reparte la cuota en proporción al consumo", () => {
16
+ const shares = (0, amortize_js_1.amortize)([
17
+ { projectId: "a", month: "2026-08", consumption: { microUsd: 75_000_000 } }, // 75%
18
+ { projectId: "b", month: "2026-08", consumption: { microUsd: 25_000_000 } }, // 25%
19
+ ], [CLAUDE_MAX]);
20
+ const a = shares.find((s) => s.projectId === "a");
21
+ const b = shares.find((s) => s.projectId === "b");
22
+ strict_1.default.equal(a.share, 0.75);
23
+ strict_1.default.equal((0, shared_1.formatMoney)(a.amount), "$150,00");
24
+ strict_1.default.equal((0, shared_1.formatMoney)(b.amount), "$50,00");
25
+ strict_1.default.equal(a.amount.amount + b.amount.amount, 20_000, "el reparto suma la cuota entera");
26
+ });
27
+ (0, node_test_1.test)("lo no facturable también consume cuota, y por eso entra en el denominador", () => {
28
+ // Si tus experimentos se comen la mitad, al cliente solo le toca la otra mitad.
29
+ const shares = (0, amortize_js_1.amortize)([
30
+ { projectId: "cliente", month: "2026-08", consumption: { microUsd: 50_000_000 } },
31
+ { projectId: "experimentos", month: "2026-08", consumption: { microUsd: 50_000_000 } },
32
+ ], [CLAUDE_MAX]);
33
+ const cliente = shares.find((s) => s.projectId === "cliente");
34
+ strict_1.default.equal((0, shared_1.formatMoney)(cliente.amount), "$100,00");
35
+ strict_1.default.equal(cliente.share, 0.5);
36
+ });
37
+ (0, node_test_1.test)("el trabajo sin asignar a proyecto también diluye la cuota", () => {
38
+ // El fallo que esto previene: con un solo proyecto configurado y el 90% del
39
+ // consumo en repos sin registrar, ese proyecto cargaba con la cuota entera.
40
+ const shares = (0, amortize_js_1.amortize)([{ projectId: "eurosistema", month: "2026-08", consumption: { microUsd: 10_000_000 } }], [CLAUDE_MAX], new Map([["2026-08", { microUsd: 100_000_000 }]])); // total real de la máquina
41
+ strict_1.default.equal(shares[0].share, 0.1);
42
+ strict_1.default.equal((0, shared_1.formatMoney)(shares[0].amount), "$20,00");
43
+ strict_1.default.equal(shares[0].monthTotal.microUsd, 100_000_000, "el denominador es el consumo real de la máquina, no la suma de proyectos");
44
+ });
45
+ (0, node_test_1.test)("un total incoherente nunca hace que las partes pasen del 100%", () => {
46
+ const shares = (0, amortize_js_1.amortize)([{ projectId: "a", month: "2026-08", consumption: { microUsd: 50_000_000 } }], [CLAUDE_MAX], new Map([["2026-08", { microUsd: 10_000_000 }]])); // menor que lo asignado
47
+ strict_1.default.equal(shares[0].share, 1, "se limita al 100%, no al 500%");
48
+ strict_1.default.equal((0, shared_1.formatMoney)(shares[0].amount), "$200,00");
49
+ });
50
+ (0, node_test_1.test)("el reparto es por mes, no acumulado", () => {
51
+ const shares = (0, amortize_js_1.amortize)([
52
+ { projectId: "a", month: "2026-07", consumption: { microUsd: 10_000_000 } },
53
+ { projectId: "a", month: "2026-08", consumption: { microUsd: 30_000_000 } },
54
+ { projectId: "b", month: "2026-08", consumption: { microUsd: 10_000_000 } },
55
+ ], [CLAUDE_MAX]);
56
+ const julio = shares.find((s) => s.projectId === "a" && s.month === "2026-07");
57
+ const agosto = shares.find((s) => s.projectId === "a" && s.month === "2026-08");
58
+ strict_1.default.equal((0, shared_1.formatMoney)(julio.amount), "$200,00", "único proyecto en julio: cuota entera");
59
+ strict_1.default.equal((0, shared_1.formatMoney)(agosto.amount), "$150,00", "75% de agosto");
60
+ });
61
+ (0, node_test_1.test)("varias suscripciones a la vez se suman", () => {
62
+ const cursor = {
63
+ id: "cursor", name: "Cursor", monthlyFee: (0, shared_1.parseMoney)("20", "USD"),
64
+ effectiveFrom: new Date("2025-01-01T00:00:00Z"), effectiveTo: null,
65
+ };
66
+ strict_1.default.equal((0, shared_1.formatMoney)((0, amortize_js_1.feeForMonth)([CLAUDE_MAX, cursor], "2026-08")), "$220,00");
67
+ });
68
+ (0, node_test_1.test)("una suscripción no vigente no cuenta", () => {
69
+ const antigua = {
70
+ ...CLAUDE_MAX, id: "vieja",
71
+ effectiveTo: new Date("2026-01-01T00:00:00Z"),
72
+ };
73
+ strict_1.default.equal((0, amortize_js_1.feeForMonth)([antigua], "2026-08"), null);
74
+ strict_1.default.equal((0, shared_1.formatMoney)((0, amortize_js_1.feeForMonth)([antigua], "2025-06")), "$200,00");
75
+ });
76
+ (0, node_test_1.test)("suscripciones en monedas distintas se rechazan en vez de convertirse", () => {
77
+ const enEuros = {
78
+ id: "x", name: "X", monthlyFee: (0, shared_1.money)(2000, "EUR"),
79
+ effectiveFrom: new Date("2025-01-01T00:00:00Z"), effectiveTo: null,
80
+ };
81
+ strict_1.default.throws(() => (0, amortize_js_1.feeForMonth)([CLAUDE_MAX, enEuros], "2026-08"), /monedas distintas/);
82
+ });
83
+ (0, node_test_1.test)("sin suscripción, el reparto sale a cero en vez de inventar un coste", () => {
84
+ const shares = (0, amortize_js_1.amortize)([{ projectId: "a", month: "2026-08", consumption: { microUsd: 8_016_655 } }], []);
85
+ strict_1.default.equal(shares[0].amount.amount, 0);
86
+ strict_1.default.equal(shares[0].consumption.microUsd, 8_016_655, "el consumo sí se conserva");
87
+ });
88
+ (0, node_test_1.test)("consumo cero no divide por cero", () => {
89
+ const shares = (0, amortize_js_1.amortize)([{ projectId: "a", month: "2026-08", consumption: { microUsd: 0 } }], [CLAUDE_MAX]);
90
+ strict_1.default.equal(shares[0].share, 0);
91
+ strict_1.default.equal(shares[0].amount.amount, 0);
92
+ });
93
+ (0, node_test_1.test)("suma de un proyecto a lo largo de varios meses", () => {
94
+ const shares = (0, amortize_js_1.amortize)([
95
+ { projectId: "a", month: "2026-07", consumption: { microUsd: 100_000_000 } },
96
+ { projectId: "a", month: "2026-08", consumption: { microUsd: 100_000_000 } },
97
+ ], [CLAUDE_MAX]);
98
+ const total = (0, amortize_js_1.shareForProject)(shares, "a", ["2026-07", "2026-08"]);
99
+ strict_1.default.equal((0, shared_1.formatMoney)(total), "$400,00");
100
+ const soloJulio = (0, amortize_js_1.shareForProject)(shares, "a", ["2026-07"]);
101
+ strict_1.default.equal((0, shared_1.formatMoney)(soloJulio), "$200,00");
102
+ });
103
+ (0, node_test_1.test)("monthOf usa UTC de forma consistente", () => {
104
+ strict_1.default.equal((0, amortize_js_1.monthOf)(new Date("2026-08-26T23:30:00Z")), "2026-08");
105
+ strict_1.default.equal((0, amortize_js_1.monthOf)(new Date("2026-09-01T00:00:00Z")), "2026-09");
106
+ });
107
+ (0, node_test_1.test)("el reparto de la cuota se congela en el informe", () => {
108
+ // El fallo que esto previene: el reparto es un valor derivado que cambia al
109
+ // entrar más consumo del mismo mes. Sin congelarlo, reimprimir un informe de
110
+ // agosto en diciembre daría otra cifra, y un documento que cambia solo no es
111
+ // un documento.
112
+ const enAgosto = (0, amortize_js_1.amortize)([{ projectId: "a", month: "2026-08", consumption: { microUsd: 50_000_000 } }], [CLAUDE_MAX], new Map([["2026-08", { microUsd: 100_000_000 }]]));
113
+ const congelado = (0, amortize_js_1.shareForProject)(enAgosto, "a", ["2026-08"]);
114
+ strict_1.default.equal((0, shared_1.formatMoney)(congelado), "$100,00");
115
+ // En diciembre se conoce el doble de consumo de ese mismo mes.
116
+ const enDiciembre = (0, amortize_js_1.amortize)([{ projectId: "a", month: "2026-08", consumption: { microUsd: 50_000_000 } }], [CLAUDE_MAX], new Map([["2026-08", { microUsd: 200_000_000 }]]));
117
+ const recalculado = (0, amortize_js_1.shareForProject)(enDiciembre, "a", ["2026-08"]);
118
+ strict_1.default.equal((0, shared_1.formatMoney)(recalculado), "$50,00");
119
+ strict_1.default.notEqual(congelado.amount, recalculado.amount, "el recálculo cambia: por eso hay que guardar el de su día");
120
+ });
121
+ //# sourceMappingURL=amortize.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"amortize.test.js","sourceRoot":"","sources":["../../src/billing/amortize.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=format.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"format.test.d.ts","sourceRoot":"","sources":["../../src/billing/format.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,110 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const strict_1 = __importDefault(require("node:assert/strict"));
7
+ const node_test_1 = require("node:test");
8
+ const shared_1 = require("@estela/shared");
9
+ const sessionize_js_1 = require("./sessionize.js");
10
+ // ── Duraciones ─────────────────────────────────────────────────────────
11
+ (0, node_test_1.test)("nunca imprime 60 minutos", () => {
12
+ // El fallo que esto previene: redondear los minutos después de haber
13
+ // separado las horas. 2h 59m 50s salía como "2h 60m", y en una factura.
14
+ strict_1.default.equal((0, shared_1.formatDuration)(2 * 3600 + 59 * 60 + 50), "3h");
15
+ strict_1.default.equal((0, shared_1.formatDuration)(3599), "1h");
16
+ strict_1.default.equal((0, shared_1.formatDuration)(59 * 60 + 55), "1h");
17
+ });
18
+ (0, node_test_1.test)("duraciones normales", () => {
19
+ strict_1.default.equal((0, shared_1.formatDuration)(0), "0m");
20
+ strict_1.default.equal((0, shared_1.formatDuration)(90), "2m");
21
+ strict_1.default.equal((0, shared_1.formatDuration)(3600), "1h");
22
+ strict_1.default.equal((0, shared_1.formatDuration)(3660), "1h 01m");
23
+ strict_1.default.equal((0, shared_1.formatDuration)(4 * 3600 + 16 * 60), "4h 16m");
24
+ });
25
+ (0, node_test_1.test)("ninguna duración razonable produce 60m", () => {
26
+ for (let s = 0; s < 12 * 3600; s += 7) {
27
+ const out = (0, shared_1.formatDuration)(s);
28
+ strict_1.default.ok(!out.includes("60m"), `formatDuration(${s}) = "${out}"`);
29
+ }
30
+ });
31
+ // ── Fecha local ────────────────────────────────────────────────────────
32
+ (0, node_test_1.test)("el día de trabajo es el del reloj de quien trabaja", () => {
33
+ // En UTC-5, las 22:00 del día 25 son las 03:00 UTC del 26. Usar la fecha UTC
34
+ // partiría la jornada de tarde-noche entre dos días.
35
+ const tarde = new Date("2026-08-26T03:19:00Z"); // 22:19 del 25 en Ecuador
36
+ const esperado = new Date(tarde.getTime() - tarde.getTimezoneOffset() * 60_000)
37
+ .toISOString().slice(0, 10);
38
+ strict_1.default.equal((0, shared_1.localDate)(tarde), esperado);
39
+ if (tarde.getTimezoneOffset() > 0) {
40
+ strict_1.default.notEqual((0, shared_1.localDate)(tarde), tarde.toISOString().slice(0, 10), "al oeste de Greenwich, la fecha local y la UTC difieren en esta hora");
41
+ }
42
+ });
43
+ // ── Agrupación ─────────────────────────────────────────────────────────
44
+ function block(startIso, minutes, branch = "main") {
45
+ const startedAt = new Date(startIso);
46
+ return {
47
+ startedAt,
48
+ endedAt: new Date(startedAt.getTime() + minutes * 60_000),
49
+ seconds: minutes * 60,
50
+ repoPath: "/repo", branch, turnCount: 1,
51
+ aiCost: { microUsd: 1_000_000 },
52
+ models: ["claude-opus-5"], sessionIds: ["s"], commits: [], unpricedModels: [],
53
+ };
54
+ }
55
+ (0, node_test_1.test)("suma el tiempo trabajado, no el lapso entre el primero y el último", () => {
56
+ // Dos bloques de 30 min con tres horas de comida en medio son 1h, no 4h.
57
+ // Facturar el hueco sería cobrarle de más al cliente.
58
+ const merged = (0, sessionize_js_1.groupByBranchAndDay)([
59
+ block("2026-08-26T14:00:00Z", 30),
60
+ block("2026-08-26T21:00:00Z", 30),
61
+ ]);
62
+ strict_1.default.equal(merged.length, 1);
63
+ strict_1.default.equal(merged[0].seconds, 3600, "1h de trabajo real");
64
+ strict_1.default.equal((0, shared_1.formatDuration)(merged[0].seconds), "1h");
65
+ });
66
+ (0, node_test_1.test)("ramas distintas no se fusionan", () => {
67
+ const merged = (0, sessionize_js_1.groupByBranchAndDay)([
68
+ block("2026-08-26T14:00:00Z", 30, "main"),
69
+ block("2026-08-26T15:00:00Z", 30, "feature/auth"),
70
+ ]);
71
+ strict_1.default.equal(merged.length, 2);
72
+ });
73
+ (0, node_test_1.test)("se acumulan commits, modelos y coste al fusionar", () => {
74
+ const a = block("2026-08-26T14:00:00Z", 30);
75
+ const b = block("2026-08-26T15:00:00Z", 30);
76
+ const merged = (0, sessionize_js_1.groupByBranchAndDay)([a, b]);
77
+ strict_1.default.equal(merged[0].aiCost.microUsd, 2_000_000);
78
+ strict_1.default.equal(merged[0].turnCount, 2);
79
+ });
80
+ // ── Identificador determinista de una imputación ───────────────────────
81
+ /** La misma fórmula que usan la CLI y el servidor para nombrar un bloque. */
82
+ function entryId(projectId, startedAt, branch) {
83
+ const day = (0, shared_1.localDate)(startedAt);
84
+ const slug = (branch ?? "sin-rama").replace(/[^a-zA-Z0-9]+/g, "-");
85
+ return `te_${projectId}_${day}_${slug}`;
86
+ }
87
+ (0, node_test_1.test)("el id usa la fecha local, no la UTC", () => {
88
+ // El fallo que esto previene: la CLI construía el id con toISOString() (UTC)
89
+ // y el servidor con localDate(). Para el mismo bloque salían dos ids, así que
90
+ // en vez de actualizar una fila se creaba otra y el día se contaba doble.
91
+ const tarde = new Date("2026-08-27T02:49:59Z"); // 21:49 del 26 en UTC-5
92
+ const id = entryId("euro", tarde, "feature/x");
93
+ strict_1.default.ok(id.includes((0, shared_1.localDate)(tarde)), "el id lleva la fecha local");
94
+ if (tarde.getTimezoneOffset() > 0) {
95
+ strict_1.default.ok(!id.includes(tarde.toISOString().slice(0, 10)), "y no la UTC, que aquí es el día siguiente");
96
+ }
97
+ });
98
+ (0, node_test_1.test)("el mismo bloque siempre produce el mismo id", () => {
99
+ const at = new Date("2026-08-26T14:00:00Z");
100
+ strict_1.default.equal(entryId("p", at, "main"), entryId("p", new Date(at), "main"));
101
+ });
102
+ (0, node_test_1.test)("el id coincide con el local_date de la fila", () => {
103
+ // La invariante que hace posible detectar filas huérfanas: si un id lleva una
104
+ // fecha distinta a su local_date, esa fila la creó una versión con el bug.
105
+ for (const iso of ["2026-08-27T02:49:00Z", "2026-08-26T14:00:00Z", "2026-01-01T03:00:00Z"]) {
106
+ const at = new Date(iso);
107
+ strict_1.default.ok(entryId("p", at, "main").includes(`_${(0, shared_1.localDate)(at)}_`));
108
+ }
109
+ });
110
+ //# sourceMappingURL=format.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"format.test.js","sourceRoot":"","sources":["../../src/billing/format.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,52 @@
1
+ import type { Client, Invoice, Money, Project, RatePeriod, TimeEntry } from "@estela/shared";
2
+ /**
3
+ * Emisión de facturas.
4
+ *
5
+ * Tres reglas que no se negocian, porque son las que hacen que una factura de
6
+ * hace seis meses siga cuadrando hoy:
7
+ *
8
+ * 1. La tarifa se busca por la fecha del trabajo, no por la de hoy. Si subes
9
+ * el precio a mitad de proyecto, lo ya trabajado mantiene el suyo.
10
+ * 2. El tipo de cambio se captura al emitir y se guarda en la factura. Jamás
11
+ * se recalcula con el del día.
12
+ * 3. El coste de IA está en USD porque es la moneda en la que factura el
13
+ * proveedor. Nunca se suma a un importe en otra moneda sin un tipo
14
+ * explícito.
15
+ */
16
+ export interface IssueInvoiceInput {
17
+ readonly client: Client;
18
+ readonly project: Project;
19
+ readonly rates: readonly RatePeriod[];
20
+ readonly entries: readonly TimeEntry[];
21
+ /** Fecha de corte, inclusive. Se factura todo lo anterior o igual. */
22
+ readonly cutoffAt: Date;
23
+ readonly number: string;
24
+ readonly issuedAt?: Date;
25
+ /**
26
+ * Unidades de la moneda del cliente por 1 USD. Obligatorio solo cuando el
27
+ * proyecto repercute el coste de IA y la moneda no es USD.
28
+ */
29
+ readonly usdFxRate?: number;
30
+ /**
31
+ * Parte de la cuota de suscripción que le toca a este periodo. Se calcula
32
+ * fuera y se guarda tal cual: es un valor derivado que cambia cuando entran
33
+ * más datos, así que el informe tiene que quedarse con el de su día.
34
+ */
35
+ readonly aiAmortized?: Money;
36
+ readonly notes?: string;
37
+ }
38
+ export declare class InvoiceError extends Error {
39
+ }
40
+ export declare function issueInvoice(input: IssueInvoiceInput): Invoice;
41
+ /** Tarifa vigente en una fecha. Las tarifas se apilan, no se sobrescriben. */
42
+ export declare function rateAt(rates: readonly RatePeriod[], projectId: string, at: Date): Money | null;
43
+ /** Renderiza la factura como texto plano para revisarla antes de enviarla. */
44
+ export declare function renderInvoice(invoice: Invoice, client: Client, project: Project): string;
45
+ /** Margen real del periodo: lo cobrado menos lo que costó la IA. */
46
+ export declare function marginOf(invoice: Invoice, usdFxRate: number): {
47
+ revenue: Money;
48
+ aiCost: Money;
49
+ margin: Money;
50
+ marginPct: number;
51
+ };
52
+ //# sourceMappingURL=invoice.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"invoice.d.ts","sourceRoot":"","sources":["../../src/billing/invoice.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACF,MAAM,EAAE,OAAO,EAAe,KAAK,EAAE,OAAO,EAAE,UAAU,EAAE,SAAS,EAC5E,MAAM,gBAAgB,CAAC;AAMxB;;;;;;;;;;;;;GAaG;AAEH,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,SAAS,UAAU,EAAE,CAAC;IACtC,QAAQ,CAAC,OAAO,EAAE,SAAS,SAAS,EAAE,CAAC;IACvC,sEAAsE;IACtE,QAAQ,CAAC,QAAQ,EAAE,IAAI,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,QAAQ,CAAC,EAAE,IAAI,CAAC;IACzB;;;OAGG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B;;;;OAIG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,KAAK,CAAC;IAC7B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,qBAAa,YAAa,SAAQ,KAAK;CAAG;AAE1C,wBAAgB,YAAY,CAAC,KAAK,EAAE,iBAAiB,GAAG,OAAO,CAwF9D;AAED,8EAA8E;AAC9E,wBAAgB,MAAM,CACpB,KAAK,EAAE,SAAS,UAAU,EAAE,EAC5B,SAAS,EAAE,MAAM,EACjB,EAAE,EAAE,IAAI,GACP,KAAK,GAAG,IAAI,CAOd;AAED,8EAA8E;AAC9E,wBAAgB,aAAa,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,MAAM,CAgDxF;AAED,oEAAoE;AACpE,wBAAgB,QAAQ,CAAC,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,GAAG;IAC7D,OAAO,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,KAAK,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;CACjE,CAUA"}