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
@@ -0,0 +1,101 @@
1
+ import type { CommitRecord, TimeEntry } from "@estela/shared";
2
+ /**
3
+ * Métricas del panel compartido.
4
+ *
5
+ * Funciones puras: entran commits e imputaciones, salen cifras. Sin base de
6
+ * datos y sin interfaz, para que cada número tenga un test y se pueda defender
7
+ * cuando el líder técnico pregunte de dónde sale. Lo que arruina una demo no es
8
+ * una gráfica fea, es una cifra que no cuadra.
9
+ *
10
+ * Lo que deliberadamente NO se calcula: líneas de código como titular, número
11
+ * de commits, y cualquier índice compuesto de "productividad". Son métricas de
12
+ * vanidad; enseñarlas a quien dirige equipos descalifica la herramienta en el
13
+ * primer vistazo porque todo el oficio sabe que se inflan solas.
14
+ */
15
+ export interface FeatureEffort {
16
+ /** Nombre legible: la rama sin su prefijo de convención. */
17
+ readonly name: string;
18
+ readonly branch: string;
19
+ readonly seconds: number;
20
+ readonly days: number;
21
+ readonly firstDay: string;
22
+ readonly lastDay: string;
23
+ /** Lo entregado, en palabras de quien lo hizo. */
24
+ readonly delivered: readonly {
25
+ hash: string;
26
+ subject: string;
27
+ }[];
28
+ readonly linesAdded: number;
29
+ readonly linesDeleted: number;
30
+ /** `null` cuando no se sabe si está integrada. */
31
+ readonly merged: boolean | null;
32
+ }
33
+ /**
34
+ * Agrupa el trabajo por rama.
35
+ *
36
+ * Un gestor piensa en funcionalidades, no en martes: "18h en el informe de
37
+ * servicios" es una frase que puede contrastar con lo que pidió; "3h el día 14"
38
+ * no le dice nada.
39
+ */
40
+ export declare function effortByFeature(entries: readonly TimeEntry[], commits: readonly CommitRecord[], mergedBranches?: ReadonlySet<string> | null): FeatureEffort[];
41
+ /** Quita el prefijo de convención: `feature/informe-x` → `informe-x`. */
42
+ export declare function featureName(branch: string): string;
43
+ export interface Cadence {
44
+ readonly byDay: readonly {
45
+ date: string;
46
+ seconds: number;
47
+ }[];
48
+ readonly activeDays: number;
49
+ /** Días naturales entre el primero y el último con trabajo. */
50
+ readonly spanDays: number;
51
+ /** Hueco más largo sin actividad, en días. */
52
+ readonly longestGapDays: number;
53
+ readonly medianSecondsPerActiveDay: number;
54
+ }
55
+ /**
56
+ * Cómo se repartió el trabajo en el tiempo.
57
+ *
58
+ * Un líder técnico lo lee de un vistazo: trabajo repartido significa un
59
+ * proyecto que respira; todo amontonado la víspera significa otra cosa.
60
+ */
61
+ export declare function cadence(entries: readonly TimeEntry[]): Cadence;
62
+ export interface ChurnFile {
63
+ readonly file: string;
64
+ readonly touches: number;
65
+ readonly spanDays: number;
66
+ readonly lastDay: string;
67
+ }
68
+ /**
69
+ * Ficheros que se vuelven a tocar.
70
+ *
71
+ * Es la métrica que más respeto gana ante un líder técnico porque es
72
+ * diagnóstica y no autocomplaciente: mucha reescritura significa requisitos
73
+ * cambiando o diseño que se resistió. Enseñarla sin que la pida dice que no se
74
+ * oculta nada.
75
+ *
76
+ * Solo cuenta si los retoques están próximos en el tiempo: un fichero tocado en
77
+ * enero y en agosto no es reescritura, es mantenimiento normal.
78
+ */
79
+ export declare function churn(commits: readonly CommitRecord[], options?: {
80
+ withinDays?: number;
81
+ minTouches?: number;
82
+ }): ChurnFile[];
83
+ export interface OpenWork {
84
+ readonly branch: string;
85
+ readonly name: string;
86
+ readonly lastDay: string;
87
+ readonly ageDays: number;
88
+ readonly commits: number;
89
+ }
90
+ /**
91
+ * Ramas con trabajo sin integrar.
92
+ *
93
+ * Es lo que un jefe de proyecto más quiere y menos tiene: riesgo acumulado que
94
+ * no se ve hasta que estalla en la integración.
95
+ *
96
+ * Ojo al presentarlo: una rama lleva ocho días abierta tanto si el desarrollador
97
+ * va lento como si el cliente no la revisa. El número no distingue, así que la
98
+ * interfaz no debe insinuar culpa.
99
+ */
100
+ export declare function openWork(commits: readonly CommitRecord[], mergedBranches: ReadonlySet<string>, today?: Date): OpenWork[];
101
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/metrics/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAG9D;;;;;;;;;;;;GAYG;AAMH,MAAM,WAAW,aAAa;IAC5B,4DAA4D;IAC5D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,kDAAkD;IAClD,QAAQ,CAAC,SAAS,EAAE,SAAS;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACjE,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,kDAAkD;IAClD,QAAQ,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,CAAC;CACjC;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC7B,OAAO,EAAE,SAAS,SAAS,EAAE,EAC7B,OAAO,EAAE,SAAS,YAAY,EAAE,EAChC,cAAc,GAAE,WAAW,CAAC,MAAM,CAAC,GAAG,IAAW,GAChD,aAAa,EAAE,CAgDjB;AAgBD,yEAAyE;AACzE,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAQlD;AAMD,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,KAAK,EAAE,SAAS;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC7D,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,+DAA+D;IAC/D,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,8CAA8C;IAC9C,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,yBAAyB,EAAE,MAAM,CAAC;CAC5C;AAED;;;;;GAKG;AACH,wBAAgB,OAAO,CAAC,OAAO,EAAE,SAAS,SAAS,EAAE,GAAG,OAAO,CAsC9D;AAMD,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,KAAK,CACnB,OAAO,EAAE,SAAS,YAAY,EAAE,EAChC,OAAO,GAAE;IAAE,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAO,GACzD,SAAS,EAAE,CAyCb;AAMD,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;;;;;GASG;AACH,wBAAgB,QAAQ,CACtB,OAAO,EAAE,SAAS,YAAY,EAAE,EAChC,cAAc,EAAE,WAAW,CAAC,MAAM,CAAC,EACnC,KAAK,OAAa,GACjB,QAAQ,EAAE,CAuBZ"}
@@ -0,0 +1,221 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.effortByFeature = effortByFeature;
4
+ exports.featureName = featureName;
5
+ exports.cadence = cadence;
6
+ exports.churn = churn;
7
+ exports.openWork = openWork;
8
+ const shared_1 = require("@estela/shared");
9
+ /**
10
+ * Agrupa el trabajo por rama.
11
+ *
12
+ * Un gestor piensa en funcionalidades, no en martes: "18h en el informe de
13
+ * servicios" es una frase que puede contrastar con lo que pidió; "3h el día 14"
14
+ * no le dice nada.
15
+ */
16
+ function effortByFeature(entries, commits, mergedBranches = null) {
17
+ const byBranch = new Map();
18
+ const get = (branch) => {
19
+ let entry = byBranch.get(branch);
20
+ if (!entry) {
21
+ entry = { seconds: 0, days: new Set(), commits: [] };
22
+ byBranch.set(branch, entry);
23
+ }
24
+ return entry;
25
+ };
26
+ for (const entry of entries) {
27
+ if (!entry.billable)
28
+ continue;
29
+ const bucket = get(bucketFor(entry));
30
+ bucket.seconds += entry.seconds;
31
+ bucket.days.add((0, shared_1.localDate)(entry.startedAt));
32
+ }
33
+ for (const commit of commits) {
34
+ if (!commit.branch)
35
+ continue;
36
+ // Solo se cuentan commits de ramas donde además hubo trabajo imputado: si
37
+ // no, aparecerían funcionalidades con commits y cero horas, que confunden.
38
+ const bucket = byBranch.get(commit.branch);
39
+ if (bucket)
40
+ bucket.commits.push(commit);
41
+ }
42
+ return [...byBranch.entries()]
43
+ .map(([branch, b]) => {
44
+ const days = [...b.days].sort();
45
+ const sorted = [...b.commits].sort((x, y) => y.at.getTime() - x.at.getTime());
46
+ return {
47
+ name: featureName(branch),
48
+ branch,
49
+ seconds: b.seconds,
50
+ days: days.length,
51
+ firstDay: days[0] ?? "",
52
+ lastDay: days[days.length - 1] ?? "",
53
+ delivered: sorted.map((c) => ({ hash: c.hash.slice(0, 7), subject: c.subject })),
54
+ linesAdded: sorted.reduce((s, c) => s + c.linesAdded, 0),
55
+ linesDeleted: sorted.reduce((s, c) => s + c.linesDeleted, 0),
56
+ // Un grupo por tipo de trabajo no es una rama: preguntarle si está
57
+ // integrado no significa nada, y decir "en curso" sería confuso.
58
+ merged: branch.startsWith("kind:") ? null
59
+ : mergedBranches ? mergedBranches.has(branch) : null,
60
+ };
61
+ })
62
+ .sort((a, b) => b.seconds - a.seconds);
63
+ }
64
+ /**
65
+ * A qué grupo pertenece una imputación.
66
+ *
67
+ * El trabajo sin rama no es un hueco sin explicar: una reunión o un viaje no
68
+ * ocurren en ninguna rama. Agruparlo por su tipo da una línea que un gestor
69
+ * entiende —"1h 30m en reuniones"— en vez de un "sin rama" que solo genera la
70
+ * pregunta de qué falló.
71
+ */
72
+ function bucketFor(entry) {
73
+ if (entry.branch)
74
+ return entry.branch;
75
+ if (entry.kind !== "development")
76
+ return `kind:${entry.kind}`;
77
+ return "kind:other";
78
+ }
79
+ /** Quita el prefijo de convención: `feature/informe-x` → `informe-x`. */
80
+ function featureName(branch) {
81
+ if (branch.startsWith("kind:")) {
82
+ const kind = branch.slice(5);
83
+ return shared_1.WORK_KIND_LABELS[kind] ?? "Trabajo general";
84
+ }
85
+ return branch.replace(/^(feature|feat|fix|hotfix|chore|bugfix|release)\//i, "")
86
+ .replace(/[-_]+/g, " ")
87
+ .trim() || branch;
88
+ }
89
+ /**
90
+ * Cómo se repartió el trabajo en el tiempo.
91
+ *
92
+ * Un líder técnico lo lee de un vistazo: trabajo repartido significa un
93
+ * proyecto que respira; todo amontonado la víspera significa otra cosa.
94
+ */
95
+ function cadence(entries) {
96
+ const perDay = new Map();
97
+ for (const entry of entries) {
98
+ if (!entry.billable)
99
+ continue;
100
+ const day = (0, shared_1.localDate)(entry.startedAt);
101
+ perDay.set(day, (perDay.get(day) ?? 0) + entry.seconds);
102
+ }
103
+ const byDay = [...perDay.entries()]
104
+ .map(([date, seconds]) => ({ date, seconds }))
105
+ .sort((a, b) => a.date.localeCompare(b.date));
106
+ if (byDay.length === 0) {
107
+ return { byDay, activeDays: 0, spanDays: 0, longestGapDays: 0, medianSecondsPerActiveDay: 0 };
108
+ }
109
+ const first = byDay[0].date;
110
+ const last = byDay[byDay.length - 1].date;
111
+ const spanDays = daysBetween(first, last) + 1;
112
+ let longestGap = 0;
113
+ for (let i = 1; i < byDay.length; i++) {
114
+ longestGap = Math.max(longestGap, daysBetween(byDay[i - 1].date, byDay[i].date) - 1);
115
+ }
116
+ const sorted = byDay.map((d) => d.seconds).sort((a, b) => a - b);
117
+ const mid = Math.floor(sorted.length / 2);
118
+ const median = sorted.length % 2 === 0
119
+ ? Math.round(((sorted[mid - 1] ?? 0) + (sorted[mid] ?? 0)) / 2)
120
+ : sorted[mid] ?? 0;
121
+ return {
122
+ byDay,
123
+ activeDays: byDay.length,
124
+ spanDays,
125
+ longestGapDays: longestGap,
126
+ medianSecondsPerActiveDay: median,
127
+ };
128
+ }
129
+ /**
130
+ * Ficheros que se vuelven a tocar.
131
+ *
132
+ * Es la métrica que más respeto gana ante un líder técnico porque es
133
+ * diagnóstica y no autocomplaciente: mucha reescritura significa requisitos
134
+ * cambiando o diseño que se resistió. Enseñarla sin que la pida dice que no se
135
+ * oculta nada.
136
+ *
137
+ * Solo cuenta si los retoques están próximos en el tiempo: un fichero tocado en
138
+ * enero y en agosto no es reescritura, es mantenimiento normal.
139
+ */
140
+ function churn(commits, options = {}) {
141
+ const within = options.withinDays ?? 14;
142
+ const minTouches = options.minTouches ?? 3;
143
+ const byFile = new Map();
144
+ for (const commit of commits) {
145
+ for (const file of commit.files) {
146
+ const list = byFile.get(file);
147
+ if (list)
148
+ list.push(commit.at);
149
+ else
150
+ byFile.set(file, [commit.at]);
151
+ }
152
+ }
153
+ const result = [];
154
+ for (const [file, dates] of byFile) {
155
+ if (dates.length < minTouches)
156
+ continue;
157
+ dates.sort((a, b) => a.getTime() - b.getTime());
158
+ // Retoques encadenados: cada uno a menos de `within` días del anterior.
159
+ let runStart = 0;
160
+ let best = { count: 1, from: 0, to: 0 };
161
+ for (let i = 1; i < dates.length; i++) {
162
+ const gap = (dates[i].getTime() - dates[i - 1].getTime()) / 86_400_000;
163
+ if (gap > within)
164
+ runStart = i;
165
+ const count = i - runStart + 1;
166
+ if (count > best.count)
167
+ best = { count, from: runStart, to: i };
168
+ }
169
+ if (best.count < minTouches)
170
+ continue;
171
+ const from = dates[best.from];
172
+ const to = dates[best.to];
173
+ result.push({
174
+ file,
175
+ touches: best.count,
176
+ spanDays: Math.max(1, Math.round((to.getTime() - from.getTime()) / 86_400_000)),
177
+ lastDay: (0, shared_1.localDate)(to),
178
+ });
179
+ }
180
+ return result.sort((a, b) => b.touches - a.touches || a.spanDays - b.spanDays);
181
+ }
182
+ /**
183
+ * Ramas con trabajo sin integrar.
184
+ *
185
+ * Es lo que un jefe de proyecto más quiere y menos tiene: riesgo acumulado que
186
+ * no se ve hasta que estalla en la integración.
187
+ *
188
+ * Ojo al presentarlo: una rama lleva ocho días abierta tanto si el desarrollador
189
+ * va lento como si el cliente no la revisa. El número no distingue, así que la
190
+ * interfaz no debe insinuar culpa.
191
+ */
192
+ function openWork(commits, mergedBranches, today = new Date()) {
193
+ const byBranch = new Map();
194
+ for (const commit of commits) {
195
+ if (!commit.branch || mergedBranches.has(commit.branch))
196
+ continue;
197
+ const entry = byBranch.get(commit.branch);
198
+ if (entry) {
199
+ entry.count++;
200
+ if (commit.at > entry.last)
201
+ entry.last = commit.at;
202
+ }
203
+ else {
204
+ byBranch.set(commit.branch, { last: commit.at, count: 1 });
205
+ }
206
+ }
207
+ return [...byBranch.entries()]
208
+ .map(([branch, b]) => ({
209
+ branch,
210
+ name: featureName(branch),
211
+ lastDay: (0, shared_1.localDate)(b.last),
212
+ ageDays: Math.max(0, Math.round((today.getTime() - b.last.getTime()) / 86_400_000)),
213
+ commits: b.count,
214
+ }))
215
+ .sort((a, b) => b.ageDays - a.ageDays);
216
+ }
217
+ // ---------------------------------------------------------------------------
218
+ function daysBetween(a, b) {
219
+ return Math.round((new Date(`${b}T12:00:00Z`).getTime() - new Date(`${a}T12:00:00Z`).getTime()) / 86_400_000);
220
+ }
221
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/metrics/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=metrics.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metrics.test.d.ts","sourceRoot":"","sources":["../../src/metrics/metrics.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,162 @@
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 index_js_1 = require("./index.js");
9
+ function entry(day, hours, branch = "feature/x") {
10
+ const startedAt = new Date(`${day}T14:00:00Z`);
11
+ return {
12
+ id: `e-${day}-${branch}`, projectId: "p", startedAt,
13
+ endedAt: new Date(startedAt.getTime() + hours * 3600_000),
14
+ seconds: Math.round(hours * 3600), description: "Trabajo",
15
+ billable: true, invoiceId: null, aiCost: { microUsd: 0 },
16
+ agentSeconds: 0, commitHashes: [], agents: [],
17
+ source: "agent", kind: "development", branch,
18
+ };
19
+ }
20
+ function commit(day, branch, files, subject = "feat: algo") {
21
+ return {
22
+ repoPath: "/repo", hash: `${day.replace(/-/g, "")}${branch.length}`.padEnd(40, "0"),
23
+ at: new Date(`${day}T15:00:00Z`), authorEmail: "yo@ejemplo.com", authorName: "Yo",
24
+ branch, subject, linesAdded: 10, linesDeleted: 2, files,
25
+ };
26
+ }
27
+ // ── Esfuerzo por funcionalidad ─────────────────────────────────────────
28
+ (0, node_test_1.test)("agrupa las horas por rama, no por día", () => {
29
+ // Un gestor piensa en funcionalidades: "18h en el informe de servicios" es
30
+ // una frase que puede contrastar; "3h el martes" no le dice nada.
31
+ const features = (0, index_js_1.effortByFeature)([
32
+ entry("2026-08-10", 3, "feature/informe"),
33
+ entry("2026-08-11", 2, "feature/informe"),
34
+ entry("2026-08-11", 1, "feature/login"),
35
+ ], []);
36
+ strict_1.default.equal(features.length, 2);
37
+ strict_1.default.equal(features[0].branch, "feature/informe", "la de más horas primero");
38
+ strict_1.default.equal(features[0].seconds, 5 * 3600);
39
+ strict_1.default.equal(features[0].days, 2);
40
+ });
41
+ (0, node_test_1.test)("el nombre legible quita el prefijo de convención", () => {
42
+ strict_1.default.equal((0, index_js_1.featureName)("feature/servicios-socios-cartera"), "servicios socios cartera");
43
+ strict_1.default.equal((0, index_js_1.featureName)("fix/SavePreferences"), "SavePreferences");
44
+ strict_1.default.equal((0, index_js_1.featureName)("main"), "main");
45
+ });
46
+ (0, node_test_1.test)("adjunta lo entregado en palabras de quien lo hizo", () => {
47
+ const features = (0, index_js_1.effortByFeature)([entry("2026-08-10", 3, "feature/informe")], [commit("2026-08-10", "feature/informe", ["a.cs"], "feat: filtro de Canarias")]);
48
+ strict_1.default.equal(features[0].delivered.length, 1);
49
+ strict_1.default.equal(features[0].delivered[0].subject, "feat: filtro de Canarias");
50
+ });
51
+ (0, node_test_1.test)("no inventa funcionalidades con commits pero sin horas", () => {
52
+ // Si apareciera una rama con commits y cero horas, el lector se preguntaría
53
+ // por qué y perdería confianza en el resto de las cifras.
54
+ const features = (0, index_js_1.effortByFeature)([entry("2026-08-10", 3, "feature/informe")], [commit("2026-08-10", "feature/otra", ["b.cs"])]);
55
+ strict_1.default.equal(features.length, 1);
56
+ strict_1.default.equal(features[0].branch, "feature/informe");
57
+ });
58
+ (0, node_test_1.test)("lo no facturable no cuenta como esfuerzo del proyecto", () => {
59
+ const interno = { ...entry("2026-08-10", 5, "feature/x"), billable: false };
60
+ strict_1.default.equal((0, index_js_1.effortByFeature)([interno], []).length, 0);
61
+ });
62
+ (0, node_test_1.test)("marca si la funcionalidad está integrada", () => {
63
+ const merged = new Set(["feature/hecha"]);
64
+ const features = (0, index_js_1.effortByFeature)([entry("2026-08-10", 1, "feature/hecha"), entry("2026-08-11", 1, "feature/abierta")], [], merged);
65
+ strict_1.default.equal(features.find((f) => f.branch === "feature/hecha").merged, true);
66
+ strict_1.default.equal(features.find((f) => f.branch === "feature/abierta").merged, false);
67
+ });
68
+ (0, node_test_1.test)("sin información de integración, no se afirma nada", () => {
69
+ // null no es false: decir "sin integrar" cuando no se sabe sería mentir
70
+ // sobre el estado del proyecto delante del cliente.
71
+ const features = (0, index_js_1.effortByFeature)([entry("2026-08-10", 1)], []);
72
+ strict_1.default.equal(features[0].merged, null);
73
+ });
74
+ // ── Ritmo ──────────────────────────────────────────────────────────────
75
+ (0, node_test_1.test)("mide el hueco más largo sin actividad", () => {
76
+ const c = (0, index_js_1.cadence)([
77
+ entry("2026-08-01", 2), entry("2026-08-02", 2), entry("2026-08-10", 2),
78
+ ]);
79
+ strict_1.default.equal(c.activeDays, 3);
80
+ strict_1.default.equal(c.spanDays, 10);
81
+ strict_1.default.equal(c.longestGapDays, 7, "del 2 al 10 hay 7 días sin nada");
82
+ });
83
+ (0, node_test_1.test)("la mediana resiste un día atípico", () => {
84
+ // La media se dispara con una jornada de 12h y haría parecer que el ritmo
85
+ // habitual es otro. La mediana dice lo que pasa un día normal.
86
+ const c = (0, index_js_1.cadence)([
87
+ entry("2026-08-01", 2), entry("2026-08-02", 2),
88
+ entry("2026-08-03", 2), entry("2026-08-04", 12),
89
+ ]);
90
+ strict_1.default.equal(c.medianSecondsPerActiveDay, 2 * 3600);
91
+ });
92
+ (0, node_test_1.test)("sin trabajo, ritmo en cero y sin dividir por cero", () => {
93
+ const c = (0, index_js_1.cadence)([]);
94
+ strict_1.default.equal(c.activeDays, 0);
95
+ strict_1.default.equal(c.spanDays, 0);
96
+ strict_1.default.equal(c.medianSecondsPerActiveDay, 0);
97
+ });
98
+ // ── Reescritura ────────────────────────────────────────────────────────
99
+ (0, node_test_1.test)("detecta un fichero que se toca una y otra vez", () => {
100
+ const commits = [
101
+ commit("2026-08-10", "f/a", ["Servicio.cs"]),
102
+ commit("2026-08-11", "f/a", ["Servicio.cs"]),
103
+ commit("2026-08-13", "f/a", ["Servicio.cs", "Otro.cs"]),
104
+ ];
105
+ const result = (0, index_js_1.churn)(commits, { minTouches: 3 });
106
+ strict_1.default.equal(result.length, 1);
107
+ strict_1.default.equal(result[0].file, "Servicio.cs");
108
+ strict_1.default.equal(result[0].touches, 3);
109
+ });
110
+ (0, node_test_1.test)("tocar un fichero con meses de diferencia no es reescritura", () => {
111
+ // Enero y agosto es mantenimiento normal. Llamarlo reescritura sería acusar
112
+ // al proyecto de un problema que no tiene.
113
+ const commits = [
114
+ commit("2026-01-10", "f/a", ["Viejo.cs"]),
115
+ commit("2026-05-10", "f/a", ["Viejo.cs"]),
116
+ commit("2026-08-10", "f/a", ["Viejo.cs"]),
117
+ ];
118
+ strict_1.default.equal((0, index_js_1.churn)(commits, { withinDays: 14, minTouches: 3 }).length, 0);
119
+ });
120
+ (0, node_test_1.test)("un fichero tocado dos veces no llega al umbral", () => {
121
+ const commits = [
122
+ commit("2026-08-10", "f/a", ["A.cs"]),
123
+ commit("2026-08-11", "f/a", ["A.cs"]),
124
+ ];
125
+ strict_1.default.equal((0, index_js_1.churn)(commits, { minTouches: 3 }).length, 0);
126
+ });
127
+ // ── Trabajo abierto ────────────────────────────────────────────────────
128
+ (0, node_test_1.test)("lista las ramas sin integrar y su antigüedad", () => {
129
+ const hoy = new Date("2026-08-20T12:00:00Z");
130
+ const result = (0, index_js_1.openWork)([commit("2026-08-12", "feature/abierta", ["a.cs"]),
131
+ commit("2026-08-19", "feature/reciente", ["b.cs"]),
132
+ commit("2026-08-01", "feature/hecha", ["c.cs"])], new Set(["feature/hecha"]), hoy);
133
+ strict_1.default.equal(result.length, 2, "la integrada no aparece");
134
+ strict_1.default.equal(result[0].branch, "feature/abierta", "la más antigua primero");
135
+ strict_1.default.equal(result[0].ageDays, 8);
136
+ strict_1.default.equal(result[1].ageDays, 1);
137
+ });
138
+ (0, node_test_1.test)("sin ramas abiertas, lista vacía en vez de error", () => {
139
+ strict_1.default.deepEqual((0, index_js_1.openWork)([commit("2026-08-01", "main", ["a.cs"])], new Set(["main"])), []);
140
+ });
141
+ (0, node_test_1.test)("el trabajo sin rama se agrupa por su tipo, no como un hueco", () => {
142
+ // "sin rama" delante de un cliente es una pregunta incómoda sin buena
143
+ // respuesta. Una reunión no ocurre en ninguna rama: es una línea legítima.
144
+ const reunion = { ...entry("2026-08-10", 1.5, null), kind: "meeting" };
145
+ const viaje = { ...entry("2026-08-11", 2, null), id: "v", kind: "travel" };
146
+ const features = (0, index_js_1.effortByFeature)([reunion, viaje, entry("2026-08-12", 3)], []);
147
+ const nombres = features.map((f) => f.name);
148
+ strict_1.default.ok(nombres.includes("Reunión"));
149
+ strict_1.default.ok(nombres.includes("Desplazamiento"));
150
+ strict_1.default.ok(!nombres.some((n) => n.includes("sin rama")));
151
+ });
152
+ (0, node_test_1.test)("a un grupo por tipo no se le pregunta si está integrado", () => {
153
+ // "Reunión — en curso" no significa nada y confunde.
154
+ const reunion = { ...entry("2026-08-10", 1, null), kind: "meeting" };
155
+ const features = (0, index_js_1.effortByFeature)([reunion], [], new Set(["main"]));
156
+ strict_1.default.equal(features[0].merged, null);
157
+ });
158
+ (0, node_test_1.test)("desarrollo sin rama cae en un grupo general, no en 'otro'", () => {
159
+ const features = (0, index_js_1.effortByFeature)([entry("2026-08-10", 1, null)], []);
160
+ strict_1.default.equal(features[0].name, "Otro");
161
+ });
162
+ //# sourceMappingURL=metrics.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metrics.test.js","sourceRoot":"","sources":["../../src/metrics/metrics.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Quién es quién en un repositorio.
3
+ *
4
+ * Git no tiene identidades, tiene pares (nombre, correo) escritos a mano en
5
+ * cada máquina. En el repositorio de Accuro, una sola persona aparece así:
6
+ *
7
+ * Denys Fonseca <denys.fonseca@accuro.es> 79 commits
8
+ * Denys Fonseca <denys.fonseca@paipe.com> 134 commits
9
+ * dfonseca <denys.fonseca@accuro.es> 11 commits
10
+ *
11
+ * Agrupar por correo la parte en dos. Agrupar por nombre también. Una vista de
12
+ * equipo que enseñe a Denys tres veces no se la cree nadie, y con razón: si
13
+ * falla en lo que el lector puede comprobar de un vistazo, deja de creerse
14
+ * también lo que no puede comprobar.
15
+ *
16
+ * La regla: **dos identidades son la misma persona si comparten el correo, o si
17
+ * comparten un nombre que de verdad identifique a alguien**, y esa relación se
18
+ * propaga. Es el problema de las componentes conexas de un grafo, y resuelve el
19
+ * caso de arriba en un paso: `dfonseca` entra por el correo que comparte con
20
+ * `Denys Fonseca`, y arrastra consigo el segundo correo de esa persona.
21
+ *
22
+ * El matiz del nombre no es teórico. Unir por cualquier nombre fusionaba a dos
23
+ * compañeros distintos porque ambos habían commiteado alguna vez como "JD": un
24
+ * alias de dos letras hacía de puente entre `nx-jleon@` y `nx-jmoreira@`. Por
25
+ * eso solo une un nombre con apellido, y los alias sueltos se agrupan por
26
+ * correo, que es el vínculo fuerte.
27
+ *
28
+ * El sesgo es deliberado: ante la duda, partir antes que fundir. Una persona
29
+ * partida en dos filas se ve en pantalla y se entiende; dos personas fundidas
30
+ * atribuyen el trabajo de alguien a otro sin que nadie lo note.
31
+ */
32
+ export interface AuthorRow {
33
+ readonly name: string;
34
+ readonly email: string;
35
+ readonly commits: number;
36
+ }
37
+ export interface Person {
38
+ /** Nombre para enseñar: el más humano de los que usa. */
39
+ readonly name: string;
40
+ readonly emails: readonly string[];
41
+ readonly names: readonly string[];
42
+ readonly commits: number;
43
+ }
44
+ /** Une identidades de git que pertenecen a la misma persona. */
45
+ export declare function mergeIdentities(rows: readonly AuthorRow[]): Person[];
46
+ //# sourceMappingURL=people.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"people.d.ts","sourceRoot":"","sources":["../../src/metrics/people.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,MAAM;IACrB,yDAAyD;IACzD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,gEAAgE;AAChE,wBAAgB,eAAe,CAAC,IAAI,EAAE,SAAS,SAAS,EAAE,GAAG,MAAM,EAAE,CAmDpE"}