@aida-dev/metrics 0.5.0 → 0.7.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.
package/dist/index.d.ts CHANGED
@@ -5,6 +5,7 @@ declare const Attribution: z.ZodObject<{
5
5
  commitsTotal: z.ZodNumber;
6
6
  ai: z.ZodNumber;
7
7
  human: z.ZodNumber;
8
+ automated: z.ZodNumber;
8
9
  unknown: z.ZodNumber;
9
10
  coverage: z.ZodNumber;
10
11
  defaultAttribution: z.ZodEnum<["ai", "human", "unknown"]>;
@@ -47,6 +48,7 @@ declare const Attribution: z.ZodObject<{
47
48
  human: number;
48
49
  unknown: number;
49
50
  commitsTotal: number;
51
+ automated: number;
50
52
  coverage: number;
51
53
  defaultAttribution: "ai" | "human" | "unknown";
52
54
  coverageThreshold: number;
@@ -68,6 +70,7 @@ declare const Attribution: z.ZodObject<{
68
70
  human: number;
69
71
  unknown: number;
70
72
  commitsTotal: number;
73
+ automated: number;
71
74
  coverage: number;
72
75
  defaultAttribution: "ai" | "human" | "unknown";
73
76
  coverageThreshold: number;
@@ -207,6 +210,9 @@ declare const MergeRatio: z.ZodObject<{
207
210
  type MergeRatio = z.infer<typeof MergeRatio>;
208
211
  declare const Persistence: z.ZodObject<{
209
212
  commitsConsidered: z.ZodNumber;
213
+ filesConsidered: z.ZodNumber;
214
+ filesExcluded: z.ZodNumber;
215
+ censored: z.ZodNumber;
210
216
  avgDays: z.ZodNumber;
211
217
  medianDays: z.ZodNumber;
212
218
  buckets: z.ZodObject<{
@@ -230,6 +236,9 @@ declare const Persistence: z.ZodObject<{
230
236
  }>;
231
237
  }, "strip", z.ZodTypeAny, {
232
238
  commitsConsidered: number;
239
+ filesConsidered: number;
240
+ filesExcluded: number;
241
+ censored: number;
233
242
  avgDays: number;
234
243
  medianDays: number;
235
244
  buckets: {
@@ -241,6 +250,9 @@ declare const Persistence: z.ZodObject<{
241
250
  };
242
251
  }, {
243
252
  commitsConsidered: number;
253
+ filesConsidered: number;
254
+ filesExcluded: number;
255
+ censored: number;
244
256
  avgDays: number;
245
257
  medianDays: number;
246
258
  buckets: {
@@ -283,6 +295,9 @@ declare const Baseline: z.ZodObject<{
283
295
  }>;
284
296
  persistence: z.ZodObject<{
285
297
  commitsConsidered: z.ZodNumber;
298
+ filesConsidered: z.ZodNumber;
299
+ filesExcluded: z.ZodNumber;
300
+ censored: z.ZodNumber;
286
301
  avgDays: z.ZodNumber;
287
302
  medianDays: z.ZodNumber;
288
303
  buckets: z.ZodObject<{
@@ -306,6 +321,9 @@ declare const Baseline: z.ZodObject<{
306
321
  }>;
307
322
  }, "strip", z.ZodTypeAny, {
308
323
  commitsConsidered: number;
324
+ filesConsidered: number;
325
+ filesExcluded: number;
326
+ censored: number;
309
327
  avgDays: number;
310
328
  medianDays: number;
311
329
  buckets: {
@@ -317,6 +335,9 @@ declare const Baseline: z.ZodObject<{
317
335
  };
318
336
  }, {
319
337
  commitsConsidered: number;
338
+ filesConsidered: number;
339
+ filesExcluded: number;
340
+ censored: number;
320
341
  avgDays: number;
321
342
  medianDays: number;
322
343
  buckets: {
@@ -335,6 +356,9 @@ declare const Baseline: z.ZodObject<{
335
356
  };
336
357
  persistence: {
337
358
  commitsConsidered: number;
359
+ filesConsidered: number;
360
+ filesExcluded: number;
361
+ censored: number;
338
362
  avgDays: number;
339
363
  medianDays: number;
340
364
  buckets: {
@@ -354,6 +378,9 @@ declare const Baseline: z.ZodObject<{
354
378
  };
355
379
  persistence: {
356
380
  commitsConsidered: number;
381
+ filesConsidered: number;
382
+ filesExcluded: number;
383
+ censored: number;
357
384
  avgDays: number;
358
385
  medianDays: number;
359
386
  buckets: {
@@ -399,6 +426,7 @@ declare const Metrics: z.ZodObject<{
399
426
  commitsTotal: z.ZodNumber;
400
427
  ai: z.ZodNumber;
401
428
  human: z.ZodNumber;
429
+ automated: z.ZodNumber;
402
430
  unknown: z.ZodNumber;
403
431
  coverage: z.ZodNumber;
404
432
  defaultAttribution: z.ZodEnum<["ai", "human", "unknown"]>;
@@ -441,6 +469,7 @@ declare const Metrics: z.ZodObject<{
441
469
  human: number;
442
470
  unknown: number;
443
471
  commitsTotal: number;
472
+ automated: number;
444
473
  coverage: number;
445
474
  defaultAttribution: "ai" | "human" | "unknown";
446
475
  coverageThreshold: number;
@@ -462,6 +491,7 @@ declare const Metrics: z.ZodObject<{
462
491
  human: number;
463
492
  unknown: number;
464
493
  commitsTotal: number;
494
+ automated: number;
465
495
  coverage: number;
466
496
  defaultAttribution: "ai" | "human" | "unknown";
467
497
  coverageThreshold: number;
@@ -494,6 +524,9 @@ declare const Metrics: z.ZodObject<{
494
524
  }>;
495
525
  persistence: z.ZodObject<{
496
526
  commitsConsidered: z.ZodNumber;
527
+ filesConsidered: z.ZodNumber;
528
+ filesExcluded: z.ZodNumber;
529
+ censored: z.ZodNumber;
497
530
  avgDays: z.ZodNumber;
498
531
  medianDays: z.ZodNumber;
499
532
  buckets: z.ZodObject<{
@@ -517,6 +550,9 @@ declare const Metrics: z.ZodObject<{
517
550
  }>;
518
551
  }, "strip", z.ZodTypeAny, {
519
552
  commitsConsidered: number;
553
+ filesConsidered: number;
554
+ filesExcluded: number;
555
+ censored: number;
520
556
  avgDays: number;
521
557
  medianDays: number;
522
558
  buckets: {
@@ -528,6 +564,9 @@ declare const Metrics: z.ZodObject<{
528
564
  };
529
565
  }, {
530
566
  commitsConsidered: number;
567
+ filesConsidered: number;
568
+ filesExcluded: number;
569
+ censored: number;
531
570
  avgDays: number;
532
571
  medianDays: number;
533
572
  buckets: {
@@ -749,6 +788,9 @@ declare const Metrics: z.ZodObject<{
749
788
  }>;
750
789
  persistence: z.ZodObject<{
751
790
  commitsConsidered: z.ZodNumber;
791
+ filesConsidered: z.ZodNumber;
792
+ filesExcluded: z.ZodNumber;
793
+ censored: z.ZodNumber;
752
794
  avgDays: z.ZodNumber;
753
795
  medianDays: z.ZodNumber;
754
796
  buckets: z.ZodObject<{
@@ -772,6 +814,9 @@ declare const Metrics: z.ZodObject<{
772
814
  }>;
773
815
  }, "strip", z.ZodTypeAny, {
774
816
  commitsConsidered: number;
817
+ filesConsidered: number;
818
+ filesExcluded: number;
819
+ censored: number;
775
820
  avgDays: number;
776
821
  medianDays: number;
777
822
  buckets: {
@@ -783,6 +828,9 @@ declare const Metrics: z.ZodObject<{
783
828
  };
784
829
  }, {
785
830
  commitsConsidered: number;
831
+ filesConsidered: number;
832
+ filesExcluded: number;
833
+ censored: number;
786
834
  avgDays: number;
787
835
  medianDays: number;
788
836
  buckets: {
@@ -801,6 +849,9 @@ declare const Metrics: z.ZodObject<{
801
849
  };
802
850
  persistence: {
803
851
  commitsConsidered: number;
852
+ filesConsidered: number;
853
+ filesExcluded: number;
854
+ censored: number;
804
855
  avgDays: number;
805
856
  medianDays: number;
806
857
  buckets: {
@@ -820,6 +871,9 @@ declare const Metrics: z.ZodObject<{
820
871
  };
821
872
  persistence: {
822
873
  commitsConsidered: number;
874
+ filesConsidered: number;
875
+ filesExcluded: number;
876
+ censored: number;
823
877
  avgDays: number;
824
878
  medianDays: number;
825
879
  buckets: {
@@ -859,6 +913,7 @@ declare const Metrics: z.ZodObject<{
859
913
  human: number;
860
914
  unknown: number;
861
915
  commitsTotal: number;
916
+ automated: number;
862
917
  coverage: number;
863
918
  defaultAttribution: "ai" | "human" | "unknown";
864
919
  coverageThreshold: number;
@@ -883,6 +938,9 @@ declare const Metrics: z.ZodObject<{
883
938
  };
884
939
  persistence: {
885
940
  commitsConsidered: number;
941
+ filesConsidered: number;
942
+ filesExcluded: number;
943
+ censored: number;
886
944
  avgDays: number;
887
945
  medianDays: number;
888
946
  buckets: {
@@ -933,6 +991,9 @@ declare const Metrics: z.ZodObject<{
933
991
  };
934
992
  persistence: {
935
993
  commitsConsidered: number;
994
+ filesConsidered: number;
995
+ filesExcluded: number;
996
+ censored: number;
936
997
  avgDays: number;
937
998
  medianDays: number;
938
999
  buckets: {
@@ -964,6 +1025,7 @@ declare const Metrics: z.ZodObject<{
964
1025
  human: number;
965
1026
  unknown: number;
966
1027
  commitsTotal: number;
1028
+ automated: number;
967
1029
  coverage: number;
968
1030
  defaultAttribution: "ai" | "human" | "unknown";
969
1031
  coverageThreshold: number;
@@ -988,6 +1050,9 @@ declare const Metrics: z.ZodObject<{
988
1050
  };
989
1051
  persistence: {
990
1052
  commitsConsidered: number;
1053
+ filesConsidered: number;
1054
+ filesExcluded: number;
1055
+ censored: number;
991
1056
  avgDays: number;
992
1057
  medianDays: number;
993
1058
  buckets: {
@@ -1038,6 +1103,9 @@ declare const Metrics: z.ZodObject<{
1038
1103
  };
1039
1104
  persistence: {
1040
1105
  commitsConsidered: number;
1106
+ filesConsidered: number;
1107
+ filesExcluded: number;
1108
+ censored: number;
1041
1109
  avgDays: number;
1042
1110
  medianDays: number;
1043
1111
  buckets: {
@@ -1066,8 +1134,13 @@ declare function calculateCategoryCounts(commits: Commit[]): CategoryCounts | nu
1066
1134
  declare function calculateMergeRatio(commitStream: CommitStream, isTarget?: (commit: Commit) => boolean): MergeRatio;
1067
1135
  declare function calculateBaselineMergeRatio(commitStream: CommitStream, isTarget?: (commit: Commit) => boolean): CohortMergeRatio;
1068
1136
 
1069
- declare function calculatePersistence(commitStream: CommitStream, isTarget?: (commit: Commit) => boolean): Persistence;
1070
- declare function calculateBaselinePersistence(commitStream: CommitStream, isTarget?: (commit: Commit) => boolean): Persistence;
1137
+ declare const DEFAULT_PERSISTENCE_EXCLUDED_CATEGORIES: FileCategory[];
1138
+ interface PersistenceOptions {
1139
+ excludeCategories?: FileCategory[];
1140
+ observationEnd?: Date;
1141
+ }
1142
+ declare function calculatePersistence(commitStream: CommitStream, isTarget?: (commit: Commit) => boolean, options?: PersistenceOptions): Persistence;
1143
+ declare function calculateBaselinePersistence(commitStream: CommitStream, isTarget?: (commit: Commit) => boolean, options?: PersistenceOptions): Persistence;
1071
1144
 
1072
1145
  interface MetricsOptions {
1073
1146
  defaultAttribution?: 'ai' | 'human' | 'unknown';
@@ -1075,4 +1148,4 @@ interface MetricsOptions {
1075
1148
  }
1076
1149
  declare function calculateMetrics(commitStream: CommitStream, options?: MetricsOptions): Metrics;
1077
1150
 
1078
- export { AgeStats, Attribution, Baseline, CategoryCounts, CohortContext, CohortMergeRatio, Delta, FileCategory, MergeRatio, Metrics, type MetricsOptions, Persistence, calculateAgeStats, calculateBaselineMergeRatio, calculateBaselinePersistence, calculateCategoryCounts, calculateMergeRatio, calculateMetrics, calculatePersistence, categorizeFile };
1151
+ export { AgeStats, Attribution, Baseline, CategoryCounts, CohortContext, CohortMergeRatio, DEFAULT_PERSISTENCE_EXCLUDED_CATEGORIES, Delta, FileCategory, MergeRatio, Metrics, type MetricsOptions, Persistence, type PersistenceOptions, calculateAgeStats, calculateBaselineMergeRatio, calculateBaselinePersistence, calculateCategoryCounts, calculateMergeRatio, calculateMetrics, calculatePersistence, categorizeFile };
package/dist/index.js CHANGED
@@ -83,77 +83,95 @@ function calculateBaselineMergeRatio(commitStream, isTarget = (commit) => commit
83
83
 
84
84
  // src/persistence.ts
85
85
  import { daysBetween as daysBetween2 } from "@aida-dev/core";
86
- function calculatePersistence(commitStream, isTarget = (commit) => commit.tags.attribution === "ai") {
86
+ var DEFAULT_PERSISTENCE_EXCLUDED_CATEGORIES = [
87
+ "migrations",
88
+ "generated"
89
+ ];
90
+ function calculatePersistence(commitStream, isTarget = (commit) => commit.tags.attribution === "ai", options = {}) {
91
+ const {
92
+ excludeCategories = DEFAULT_PERSISTENCE_EXCLUDED_CATEGORIES,
93
+ observationEnd = new Date(commitStream.generatedAt)
94
+ } = options;
95
+ const excluded = new Set(excludeCategories);
87
96
  const targetCommits = commitStream.commits.filter(isTarget);
97
+ const empty = {
98
+ commitsConsidered: targetCommits.length,
99
+ filesConsidered: 0,
100
+ filesExcluded: 0,
101
+ censored: 0,
102
+ avgDays: 0,
103
+ medianDays: 0,
104
+ buckets: { d0_1: 0, d2_7: 0, d8_30: 0, d31_90: 0, d90_plus: 0 }
105
+ };
88
106
  if (targetCommits.length === 0) {
89
- return {
90
- commitsConsidered: 0,
91
- avgDays: 0,
92
- medianDays: 0,
93
- buckets: {
94
- d0_1: 0,
95
- d2_7: 0,
96
- d8_30: 0,
97
- d31_90: 0,
98
- d90_plus: 0
99
- }
100
- };
107
+ return { ...empty, commitsConsidered: 0 };
101
108
  }
102
- const fileLifecycles = /* @__PURE__ */ new Map();
103
109
  const sortedCommits = [...commitStream.commits].sort(
104
110
  (a, b) => new Date(a.authorDate).getTime() - new Date(b.authorDate).getTime()
105
111
  );
106
- for (const commit of sortedCommits) {
107
- if (isTarget(commit)) {
108
- const commitDate = new Date(commit.authorDate);
109
- for (const file of commit.stats.files) {
110
- if (!fileLifecycles.has(file.path)) {
111
- fileLifecycles.set(file.path, {
112
- path: file.path,
113
- firstTargetCommitDate: commitDate,
114
- lastSeenDate: commitDate,
115
- persistenceDays: 0
116
- });
117
- }
112
+ const lifecycles = /* @__PURE__ */ new Map();
113
+ let filesExcluded = 0;
114
+ sortedCommits.forEach((commit, index) => {
115
+ if (!isTarget(commit)) return;
116
+ for (const file of commit.stats.files) {
117
+ if (lifecycles.has(file.path)) continue;
118
+ if (excluded.has(categorizeFile(file.path))) {
119
+ filesExcluded++;
120
+ lifecycles.set(file.path, { firstTargetIndex: -1, firstTargetDate: /* @__PURE__ */ new Date(0), eventDate: null });
121
+ continue;
118
122
  }
123
+ lifecycles.set(file.path, {
124
+ firstTargetIndex: index,
125
+ firstTargetDate: new Date(commit.authorDate),
126
+ eventDate: null
127
+ });
119
128
  }
120
- }
121
- for (const commit of sortedCommits) {
122
- const commitDate = new Date(commit.authorDate);
129
+ });
130
+ sortedCommits.forEach((commit, index) => {
123
131
  for (const file of commit.stats.files) {
124
- const lifecycle = fileLifecycles.get(file.path);
125
- if (lifecycle) {
126
- if (file.status !== "deleted") {
127
- lifecycle.lastSeenDate = commitDate;
128
- }
132
+ const lifecycle = lifecycles.get(file.path);
133
+ if (!lifecycle || lifecycle.firstTargetIndex < 0) continue;
134
+ if (index <= lifecycle.firstTargetIndex) continue;
135
+ if (lifecycle.eventDate === null) {
136
+ lifecycle.eventDate = new Date(commit.authorDate);
129
137
  }
130
138
  }
139
+ });
140
+ const survivalDays = [];
141
+ let censored = 0;
142
+ for (const lifecycle of lifecycles.values()) {
143
+ if (lifecycle.firstTargetIndex < 0) continue;
144
+ if (lifecycle.eventDate) {
145
+ survivalDays.push(daysBetween2(lifecycle.firstTargetDate, lifecycle.eventDate));
146
+ } else {
147
+ censored++;
148
+ survivalDays.push(Math.max(0, daysBetween2(lifecycle.firstTargetDate, observationEnd)));
149
+ }
131
150
  }
132
- const persistenceDays = [];
133
- for (const lifecycle of fileLifecycles.values()) {
134
- lifecycle.persistenceDays = daysBetween2(lifecycle.firstTargetCommitDate, lifecycle.lastSeenDate);
135
- persistenceDays.push(lifecycle.persistenceDays);
151
+ if (survivalDays.length === 0) {
152
+ return { ...empty, filesExcluded };
136
153
  }
137
- const avgDays = persistenceDays.length > 0 ? persistenceDays.reduce((sum, days) => sum + days, 0) / persistenceDays.length : 0;
138
- const sortedDays = [...persistenceDays].sort((a, b) => a - b);
139
- const medianDays = sortedDays.length > 0 ? sortedDays.length % 2 === 0 ? (sortedDays[sortedDays.length / 2 - 1] + sortedDays[sortedDays.length / 2]) / 2 : sortedDays[Math.floor(sortedDays.length / 2)] : 0;
140
- const buckets = {
141
- d0_1: persistenceDays.filter((days) => days <= 1).length,
142
- d2_7: persistenceDays.filter((days) => days >= 2 && days <= 7).length,
143
- d8_30: persistenceDays.filter((days) => days >= 8 && days <= 30).length,
144
- d31_90: persistenceDays.filter((days) => days >= 31 && days <= 90).length,
145
- d90_plus: persistenceDays.filter((days) => days > 90).length
146
- };
154
+ const avgDays = survivalDays.reduce((sum, days) => sum + days, 0) / survivalDays.length;
155
+ const sortedDays = [...survivalDays].sort((a, b) => a - b);
156
+ const medianDays = sortedDays.length % 2 === 0 ? (sortedDays[sortedDays.length / 2 - 1] + sortedDays[sortedDays.length / 2]) / 2 : sortedDays[Math.floor(sortedDays.length / 2)];
147
157
  return {
148
158
  commitsConsidered: targetCommits.length,
159
+ filesConsidered: survivalDays.length,
160
+ filesExcluded,
161
+ censored,
149
162
  avgDays: Math.round(avgDays * 100) / 100,
150
- // Round to 2 decimal places
151
163
  medianDays: Math.round(medianDays * 100) / 100,
152
- buckets
164
+ buckets: {
165
+ d0_1: survivalDays.filter((days) => days <= 1).length,
166
+ d2_7: survivalDays.filter((days) => days >= 2 && days <= 7).length,
167
+ d8_30: survivalDays.filter((days) => days >= 8 && days <= 30).length,
168
+ d31_90: survivalDays.filter((days) => days >= 31 && days <= 90).length,
169
+ d90_plus: survivalDays.filter((days) => days > 90).length
170
+ }
153
171
  };
154
172
  }
155
- function calculateBaselinePersistence(commitStream, isTarget = (commit) => commit.tags.attribution === "human") {
156
- return calculatePersistence(commitStream, isTarget);
173
+ function calculateBaselinePersistence(commitStream, isTarget = (commit) => commit.tags.attribution === "human", options = {}) {
174
+ return calculatePersistence(commitStream, isTarget, options);
157
175
  }
158
176
 
159
177
  // src/schema/metrics.ts
@@ -162,9 +180,12 @@ var Attribution = z.object({
162
180
  commitsTotal: z.number().int().nonnegative(),
163
181
  ai: z.number().int().nonnegative(),
164
182
  human: z.number().int().nonnegative(),
183
+ // Provenance-known automation (#39): merge commits, bots, manifest-excluded.
184
+ // Counts toward coverage, joins no cohort, untouched by priors.
185
+ automated: z.number().int().nonnegative(),
165
186
  unknown: z.number().int().nonnegative(),
166
187
  coverage: z.number().min(0).max(1),
167
- // (ai + human) / total
188
+ // (ai + human + automated) / total
168
189
  defaultAttribution: z.enum(["ai", "human", "unknown"]),
169
190
  // prior applied to unknown commits
170
191
  coverageThreshold: z.number().min(0).max(1),
@@ -190,6 +211,10 @@ var MergeRatio = z.object({
190
211
  });
191
212
  var Persistence = z.object({
192
213
  commitsConsidered: z.number().int().nonnegative(),
214
+ filesConsidered: z.number().int().nonnegative(),
215
+ filesExcluded: z.number().int().nonnegative(),
216
+ censored: z.number().int().nonnegative(),
217
+ // files that survived the whole window
193
218
  avgDays: z.number().nonnegative(),
194
219
  medianDays: z.number().nonnegative(),
195
220
  buckets: z.object({
@@ -273,7 +298,7 @@ function round(value, decimals) {
273
298
  }
274
299
  function calculateMetrics(commitStream, options = {}) {
275
300
  const { defaultAttribution = "unknown", coverageThreshold = 0.7 } = options;
276
- const counts = { ai: 0, human: 0, unknown: 0 };
301
+ const counts = { ai: 0, human: 0, automated: 0, unknown: 0 };
277
302
  const modes = { none: 0, autocomplete: 0, assisted: 0, agent: 0, unknown: 0 };
278
303
  const modeEvidence = { declared: 0, inferred: 0, none: 0 };
279
304
  for (const commit of commitStream.commits) {
@@ -282,11 +307,12 @@ function calculateMetrics(commitStream, options = {}) {
282
307
  modeEvidence[commit.tags.modeEvidence]++;
283
308
  }
284
309
  const total = commitStream.commits.length;
285
- const coverage = total > 0 ? (counts.ai + counts.human) / total : 0;
310
+ const coverage = total > 0 ? (counts.ai + counts.human + counts.automated) / total : 0;
286
311
  const attribution = {
287
312
  commitsTotal: total,
288
313
  ai: counts.ai,
289
314
  human: counts.human,
315
+ automated: counts.automated,
290
316
  unknown: counts.unknown,
291
317
  coverage: round(coverage, 4),
292
318
  defaultAttribution,
@@ -295,9 +321,8 @@ function calculateMetrics(commitStream, options = {}) {
295
321
  modes,
296
322
  modeEvidence
297
323
  };
298
- const isExcluded = (commit) => commit.tags.sources.includes("manifest:excluded");
299
- const isAI = (commit) => commit.tags.attribution === "ai" || defaultAttribution === "ai" && commit.tags.attribution === "unknown" && !isExcluded(commit);
300
- const isBaseline = (commit) => commit.tags.attribution === "human" || defaultAttribution === "human" && commit.tags.attribution === "unknown" && !isExcluded(commit);
324
+ const isAI = (commit) => commit.tags.attribution === "ai" || defaultAttribution === "ai" && commit.tags.attribution === "unknown";
325
+ const isBaseline = (commit) => commit.tags.attribution === "human" || defaultAttribution === "human" && commit.tags.attribution === "unknown";
301
326
  const mergeRatio = calculateMergeRatio(commitStream, isAI);
302
327
  const persistence = calculatePersistence(commitStream, isAI);
303
328
  const now = /* @__PURE__ */ new Date();
@@ -331,6 +356,7 @@ function calculateMetrics(commitStream, options = {}) {
331
356
  const caveats = [
332
357
  `Attribution coverage is ${(coverage * 100).toFixed(1)}%: metrics only describe commits whose provenance is known.`,
333
358
  "Persistence is file-level, not line-level.",
359
+ "Persistence is survival: days until the first subsequent modification. Files never modified again are censored at collection time. Migrations and generated files (convention-driven lifecycles) are excluded.",
334
360
  "Persistence comparisons are only meaningful between cohorts of similar age and task mix \u2014 check the cohorts section before reading the delta.",
335
361
  "Merge ratio: commits from all branches checked against default branch ancestry. Squash merges may undercount unmerged commits.",
336
362
  "Time-windowed collection (--since) also windows the ancestry check: commits merged into the default branch before the window may appear unmerged.",
@@ -370,6 +396,7 @@ export {
370
396
  CategoryCounts,
371
397
  CohortContext,
372
398
  CohortMergeRatio,
399
+ DEFAULT_PERSISTENCE_EXCLUDED_CATEGORIES,
373
400
  Delta,
374
401
  FileCategory,
375
402
  MergeRatio,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/cohort.ts","../src/merge-ratio.ts","../src/persistence.ts","../src/schema/metrics.ts"],"sourcesContent":["import { Commit, CommitStream, formatISODate } from '@aida-dev/core';\nimport { calculateAgeStats, calculateCategoryCounts } from './cohort.js';\nimport { calculateBaselineMergeRatio, calculateMergeRatio } from './merge-ratio.js';\nimport { calculateBaselinePersistence, calculatePersistence } from './persistence.js';\nimport { Attribution, Metrics } from './schema/metrics.js';\n\nexport * from './schema/metrics.js';\nexport * from './cohort.js';\nexport * from './merge-ratio.js';\nexport * from './persistence.js';\n\nexport interface MetricsOptions {\n // Prior applied to 'unknown' commits: which cohort (if any) they join.\n defaultAttribution?: 'ai' | 'human' | 'unknown';\n coverageThreshold?: number;\n}\n\nfunction round(value: number, decimals: number): number {\n const factor = 10 ** decimals;\n return Math.round(value * factor) / factor;\n}\n\nexport function calculateMetrics(\n commitStream: CommitStream,\n options: MetricsOptions = {}\n): Metrics {\n const { defaultAttribution = 'unknown', coverageThreshold = 0.7 } = options;\n\n const counts = { ai: 0, human: 0, unknown: 0 };\n const modes = { none: 0, autocomplete: 0, assisted: 0, agent: 0, unknown: 0 };\n const modeEvidence = { declared: 0, inferred: 0, none: 0 };\n for (const commit of commitStream.commits) {\n counts[commit.tags.attribution]++;\n modes[commit.tags.mode]++;\n modeEvidence[commit.tags.modeEvidence]++;\n }\n const total = commitStream.commits.length;\n const coverage = total > 0 ? (counts.ai + counts.human) / total : 0;\n\n const attribution: Attribution = {\n commitsTotal: total,\n ai: counts.ai,\n human: counts.human,\n unknown: counts.unknown,\n coverage: round(coverage, 4),\n defaultAttribution,\n coverageThreshold,\n belowThreshold: coverage < coverageThreshold,\n modes,\n modeEvidence,\n };\n\n // Cohort membership: unknown commits join a cohort only via the prior.\n // Manifest-excluded commits (release bots, merges) never do — they were\n // excluded precisely to stay out of both cohorts.\n const isExcluded = (commit: Commit) => commit.tags.sources.includes('manifest:excluded');\n const isAI = (commit: Commit) =>\n commit.tags.attribution === 'ai' ||\n (defaultAttribution === 'ai' && commit.tags.attribution === 'unknown' && !isExcluded(commit));\n const isBaseline = (commit: Commit) =>\n commit.tags.attribution === 'human' ||\n (defaultAttribution === 'human' &&\n commit.tags.attribution === 'unknown' &&\n !isExcluded(commit));\n\n const mergeRatio = calculateMergeRatio(commitStream, isAI);\n const persistence = calculatePersistence(commitStream, isAI);\n\n // Fairness context (#29, #36): cohort age and task mix\n const now = new Date();\n const aiCommits = commitStream.commits.filter(isAI);\n const baselineCommits = commitStream.commits.filter(isBaseline);\n const cohorts = {\n ai: {\n age: calculateAgeStats(aiCommits, now),\n taskMix: calculateCategoryCounts(aiCommits),\n },\n baseline: {\n age: calculateAgeStats(baselineCommits, now),\n taskMix: calculateCategoryCounts(baselineCommits),\n },\n };\n\n // No baseline cohort → no baseline, no delta. AIDA does not invent a\n // comparison out of unattributed commits.\n const baselineSize = baselineCommits.length;\n const baselineAssumed = defaultAttribution === 'human' && counts.unknown > 0;\n\n const baseline =\n baselineSize > 0\n ? {\n assumed: baselineAssumed,\n mergeRatio: calculateBaselineMergeRatio(commitStream, isBaseline),\n persistence: calculateBaselinePersistence(commitStream, isBaseline),\n }\n : null;\n\n const delta = baseline\n ? {\n mergeRatio: round(mergeRatio.mergeRatio - baseline.mergeRatio.mergeRatio, 4),\n avgPersistenceDays: round(persistence.avgDays - baseline.persistence.avgDays, 2),\n medianPersistenceDays: round(\n persistence.medianDays - baseline.persistence.medianDays,\n 2\n ),\n }\n : null;\n\n const caveats = [\n `Attribution coverage is ${(coverage * 100).toFixed(1)}%: metrics only describe commits whose provenance is known.`,\n 'Persistence is file-level, not line-level.',\n 'Persistence comparisons are only meaningful between cohorts of similar age and task mix — check the cohorts section before reading the delta.',\n 'Merge ratio: commits from all branches checked against default branch ancestry. Squash merges may undercount unmerged commits.',\n 'Time-windowed collection (--since) also windows the ancestry check: commits merged into the default branch before the window may appear unmerged.',\n 'AI tagging uses heuristic patterns; false positives/negatives possible.',\n ];\n if (baseline?.assumed) {\n caveats.push(\n `Baseline includes ${counts.unknown} unattributed commit(s) assumed human via defaultAttribution — undetected AI usage may leak into it.`\n );\n }\n if (!baseline) {\n caveats.push(\n 'No baseline: no commits are attributed as human. Set defaultAttribution to \"human\" in .aida.json if unattributed commits in this repo are human-authored.'\n );\n }\n\n return {\n generatedAt: formatISODate(new Date()),\n window: {\n since: commitStream.since,\n until: commitStream.until,\n },\n repoPath: commitStream.repoPath,\n defaultBranch: commitStream.defaultBranch,\n attribution,\n mergeRatio,\n persistence,\n cohorts,\n baseline,\n delta,\n caveats,\n };\n}\n","import { Commit, daysBetween } from '@aida-dev/core';\nimport { AgeStats, CategoryCounts, FileCategory } from './schema/metrics.js';\n\n// Cohort age (#29): persistence comparisons between cohorts of different\n// ages are misleading — old commits have had more time to accumulate\n// survival. Reporting each cohort's age lets consumers judge fairness.\nexport function calculateAgeStats(commits: Commit[], now: Date): AgeStats | null {\n if (commits.length === 0) {\n return null;\n }\n\n const ages = commits\n .map((commit) => daysBetween(new Date(commit.authorDate), now))\n .sort((a, b) => a - b);\n\n const avg = ages.reduce((sum, days) => sum + days, 0) / ages.length;\n const median =\n ages.length % 2 === 0\n ? (ages[ages.length / 2 - 1] + ages[ages.length / 2]) / 2\n : ages[Math.floor(ages.length / 2)];\n\n return {\n commits: commits.length,\n avgAgeDays: Math.round(avg * 100) / 100,\n medianAgeDays: Math.round(median * 100) / 100,\n };\n}\n\n// File categorization (#36, step 1): AI is often pointed at boilerplate,\n// tests, and migrations, which survive longer because nobody touches them.\n// Reporting each cohort's category mix shows when an AI-vs-baseline\n// comparison is apples-to-oranges. Order matters: first match wins.\nexport function categorizeFile(path: string): FileCategory {\n const lower = path.toLowerCase();\n const base = lower.split('/').pop() ?? lower;\n\n // Generated artifacts (lockfiles, changelogs, snapshots, build output)\n if (\n /^(pnpm-lock\\.yaml|package-lock\\.json|yarn\\.lock|bun\\.lockb?|composer\\.lock|cargo\\.lock|gemfile\\.lock|poetry\\.lock|changelog(\\.[a-z]+)?)$/.test(\n base\n ) ||\n /\\.(snap|min\\.js|min\\.css)$/.test(base) ||\n /(^|\\/)(dist|build|out|coverage|\\.next|node_modules)\\//.test(lower)\n ) {\n return 'generated';\n }\n\n if (\n /(^|\\/)(__tests__|__mocks__|tests?|spec|e2e|cypress)\\//.test(lower) ||\n /\\.(test|spec)\\.[a-z]+$/.test(base)\n ) {\n return 'tests';\n }\n\n if (/(^|\\/)migrations?\\//.test(lower)) {\n return 'migrations';\n }\n\n if (/\\.(md|mdx|rst|adoc|txt)$/.test(base) || /(^|\\/)docs?\\//.test(lower)) {\n return 'docs';\n }\n\n if (\n base.startsWith('.') ||\n /\\.(json|ya?ml|toml|ini|cfg|conf|env|properties)$/.test(base) ||\n /\\.config\\.[a-z]+$/.test(base) ||\n /(^|\\/)(\\.github|\\.circleci|\\.vscode|config)\\//.test(lower) ||\n /^(dockerfile|makefile|tsconfig.*|eslint.*|prettier.*)/.test(base)\n ) {\n return 'config';\n }\n\n return 'source';\n}\n\nexport function calculateCategoryCounts(commits: Commit[]): CategoryCounts | null {\n if (commits.length === 0) {\n return null;\n }\n\n const counts: CategoryCounts = {\n source: 0,\n tests: 0,\n migrations: 0,\n config: 0,\n docs: 0,\n generated: 0,\n };\n\n for (const commit of commits) {\n for (const file of commit.stats.files) {\n counts[categorizeFile(file.path)]++;\n }\n }\n\n return counts;\n}\n","import { Commit, CommitStream } from '@aida-dev/core';\nimport { CohortMergeRatio, MergeRatio } from './schema/metrics.js';\n\nfunction computeMergeRatio(commits: Commit[]): CohortMergeRatio {\n const merged = commits.filter((commit) => commit.inDefaultBranchAncestry);\n\n return {\n commitsTotal: commits.length,\n commitsMerged: merged.length,\n mergeRatio: commits.length > 0 ? merged.length / commits.length : 0,\n };\n}\n\nexport function calculateMergeRatio(\n commitStream: CommitStream,\n isTarget: (commit: Commit) => boolean = (commit) => commit.tags.attribution === 'ai'\n): MergeRatio {\n const aiCommits = commitStream.commits.filter(isTarget);\n const result = computeMergeRatio(aiCommits);\n\n return {\n aiCommitsTotal: result.commitsTotal,\n aiCommitsMerged: result.commitsMerged,\n mergeRatio: result.mergeRatio,\n };\n}\n\nexport function calculateBaselineMergeRatio(\n commitStream: CommitStream,\n isTarget: (commit: Commit) => boolean = (commit) => commit.tags.attribution === 'human'\n): CohortMergeRatio {\n const baselineCommits = commitStream.commits.filter(isTarget);\n return computeMergeRatio(baselineCommits);\n}\n","import { Commit, CommitStream, daysBetween } from '@aida-dev/core';\nimport { Persistence } from './schema/metrics.js';\n\ninterface FileLifecycle {\n path: string;\n firstTargetCommitDate: Date;\n lastSeenDate: Date;\n persistenceDays: number;\n}\n\nexport function calculatePersistence(\n commitStream: CommitStream,\n isTarget: (commit: Commit) => boolean = (commit) => commit.tags.attribution === 'ai'\n): Persistence {\n const targetCommits = commitStream.commits.filter(isTarget);\n\n if (targetCommits.length === 0) {\n return {\n commitsConsidered: 0,\n avgDays: 0,\n medianDays: 0,\n buckets: {\n d0_1: 0,\n d2_7: 0,\n d8_30: 0,\n d31_90: 0,\n d90_plus: 0,\n },\n };\n }\n\n // Group files by path and track their lifecycle\n const fileLifecycles = new Map<string, FileLifecycle>();\n\n // Sort commits by date (oldest first)\n const sortedCommits = [...commitStream.commits].sort(\n (a, b) => new Date(a.authorDate).getTime() - new Date(b.authorDate).getTime()\n );\n\n // Track first target commit per file\n for (const commit of sortedCommits) {\n if (isTarget(commit)) {\n const commitDate = new Date(commit.authorDate);\n\n for (const file of commit.stats.files) {\n if (!fileLifecycles.has(file.path)) {\n fileLifecycles.set(file.path, {\n path: file.path,\n firstTargetCommitDate: commitDate,\n lastSeenDate: commitDate,\n persistenceDays: 0,\n });\n }\n }\n }\n }\n\n // Track last seen date for each file\n for (const commit of sortedCommits) {\n const commitDate = new Date(commit.authorDate);\n\n for (const file of commit.stats.files) {\n const lifecycle = fileLifecycles.get(file.path);\n if (lifecycle) {\n // Update last seen date if file is not deleted\n if (file.status !== 'deleted') {\n lifecycle.lastSeenDate = commitDate;\n }\n }\n }\n }\n\n // Calculate persistence days for each file\n const persistenceDays: number[] = [];\n\n for (const lifecycle of fileLifecycles.values()) {\n lifecycle.persistenceDays = daysBetween(lifecycle.firstTargetCommitDate, lifecycle.lastSeenDate);\n persistenceDays.push(lifecycle.persistenceDays);\n }\n\n // Calculate statistics\n const avgDays =\n persistenceDays.length > 0\n ? persistenceDays.reduce((sum, days) => sum + days, 0) / persistenceDays.length\n : 0;\n\n const sortedDays = [...persistenceDays].sort((a, b) => a - b);\n const medianDays =\n sortedDays.length > 0\n ? sortedDays.length % 2 === 0\n ? (sortedDays[sortedDays.length / 2 - 1] + sortedDays[sortedDays.length / 2]) / 2\n : sortedDays[Math.floor(sortedDays.length / 2)]\n : 0;\n\n // Calculate buckets\n const buckets = {\n d0_1: persistenceDays.filter((days) => days <= 1).length,\n d2_7: persistenceDays.filter((days) => days >= 2 && days <= 7).length,\n d8_30: persistenceDays.filter((days) => days >= 8 && days <= 30).length,\n d31_90: persistenceDays.filter((days) => days >= 31 && days <= 90).length,\n d90_plus: persistenceDays.filter((days) => days > 90).length,\n };\n\n return {\n commitsConsidered: targetCommits.length,\n avgDays: Math.round(avgDays * 100) / 100, // Round to 2 decimal places\n medianDays: Math.round(medianDays * 100) / 100,\n buckets,\n };\n}\n\nexport function calculateBaselinePersistence(\n commitStream: CommitStream,\n isTarget: (commit: Commit) => boolean = (commit) => commit.tags.attribution === 'human'\n): Persistence {\n return calculatePersistence(commitStream, isTarget);\n}\n","import { z } from 'zod';\n\n// Attribution coverage (#34): the headline metric. Every other number in this\n// file is only as trustworthy as this block says it is.\nexport const Attribution = z.object({\n commitsTotal: z.number().int().nonnegative(),\n ai: z.number().int().nonnegative(),\n human: z.number().int().nonnegative(),\n unknown: z.number().int().nonnegative(),\n coverage: z.number().min(0).max(1), // (ai + human) / total\n defaultAttribution: z.enum(['ai', 'human', 'unknown']), // prior applied to unknown commits\n coverageThreshold: z.number().min(0).max(1),\n belowThreshold: z.boolean(),\n // Autonomy axis (#25): commit counts per mode and per mode-evidence level\n modes: z.object({\n none: z.number().int().nonnegative(),\n autocomplete: z.number().int().nonnegative(),\n assisted: z.number().int().nonnegative(),\n agent: z.number().int().nonnegative(),\n unknown: z.number().int().nonnegative(),\n }),\n modeEvidence: z.object({\n declared: z.number().int().nonnegative(),\n inferred: z.number().int().nonnegative(),\n none: z.number().int().nonnegative(),\n }),\n});\n\nexport const MergeRatio = z.object({\n aiCommitsTotal: z.number().int().nonnegative(),\n aiCommitsMerged: z.number().int().nonnegative(),\n mergeRatio: z.number().min(0).max(1),\n});\n\nexport const Persistence = z.object({\n commitsConsidered: z.number().int().nonnegative(),\n avgDays: z.number().nonnegative(),\n medianDays: z.number().nonnegative(),\n buckets: z.object({\n d0_1: z.number().int().nonnegative(),\n d2_7: z.number().int().nonnegative(),\n d8_30: z.number().int().nonnegative(),\n d31_90: z.number().int().nonnegative(),\n d90_plus: z.number().int().nonnegative(),\n }),\n});\n\n// Cohort age (#29): context for judging whether a persistence comparison\n// between cohorts is fair — older cohorts accumulate survival by default.\nexport const AgeStats = z.object({\n commits: z.number().int().nonnegative(),\n avgAgeDays: z.number().nonnegative(),\n medianAgeDays: z.number().nonnegative(),\n});\n\n// Task mix (#36): what kind of files each cohort touched. A persistence\n// comparison is only meaningful when the mixes are similar.\nexport const FileCategory = z.enum([\n 'source',\n 'tests',\n 'migrations',\n 'config',\n 'docs',\n 'generated',\n]);\n\nexport const CategoryCounts = z.object({\n source: z.number().int().nonnegative(),\n tests: z.number().int().nonnegative(),\n migrations: z.number().int().nonnegative(),\n config: z.number().int().nonnegative(),\n docs: z.number().int().nonnegative(),\n generated: z.number().int().nonnegative(),\n});\n\nexport const CohortContext = z.object({\n age: AgeStats.nullable(),\n taskMix: CategoryCounts.nullable(),\n});\n\nexport const CohortMergeRatio = z.object({\n commitsTotal: z.number().int().nonnegative(),\n commitsMerged: z.number().int().nonnegative(),\n mergeRatio: z.number().min(0).max(1),\n});\n\nexport const Baseline = z.object({\n // True when the cohort includes 'unknown' commits via defaultAttribution:\n // the baseline is an assumption, not observed attribution.\n assumed: z.boolean(),\n mergeRatio: CohortMergeRatio,\n persistence: Persistence,\n});\n\nexport const Delta = z.object({\n mergeRatio: z.number(),\n avgPersistenceDays: z.number(),\n medianPersistenceDays: z.number(),\n});\n\nexport const Metrics = z.object({\n generatedAt: z.string().datetime(),\n window: z.object({\n since: z.string().optional(),\n until: z.string().optional(),\n }),\n repoPath: z.string(),\n defaultBranch: z.string(),\n attribution: Attribution,\n mergeRatio: MergeRatio,\n persistence: Persistence,\n // Fairness context (#29, #36): age and task mix per cohort, so consumers\n // can judge whether the AI-vs-baseline comparison is apples-to-apples.\n cohorts: z.object({\n ai: CohortContext,\n baseline: CohortContext,\n }),\n // Null when no commit is attributed 'human' and no defaultAttribution prior\n // assigns the unknowns: AIDA does not invent a comparison cohort.\n baseline: Baseline.nullable(),\n delta: Delta.nullable(),\n caveats: z.array(z.string()),\n});\n\nexport type Attribution = z.infer<typeof Attribution>;\nexport type AgeStats = z.infer<typeof AgeStats>;\nexport type FileCategory = z.infer<typeof FileCategory>;\nexport type CategoryCounts = z.infer<typeof CategoryCounts>;\nexport type CohortContext = z.infer<typeof CohortContext>;\nexport type MergeRatio = z.infer<typeof MergeRatio>;\nexport type Persistence = z.infer<typeof Persistence>;\nexport type CohortMergeRatio = z.infer<typeof CohortMergeRatio>;\nexport type Baseline = z.infer<typeof Baseline>;\nexport type Delta = z.infer<typeof Delta>;\nexport type Metrics = z.infer<typeof Metrics>;\n"],"mappings":";AAAA,SAA+B,qBAAqB;;;ACApD,SAAiB,mBAAmB;AAM7B,SAAS,kBAAkB,SAAmB,KAA4B;AAC/E,MAAI,QAAQ,WAAW,GAAG;AACxB,WAAO;AAAA,EACT;AAEA,QAAM,OAAO,QACV,IAAI,CAAC,WAAW,YAAY,IAAI,KAAK,OAAO,UAAU,GAAG,GAAG,CAAC,EAC7D,KAAK,CAAC,GAAG,MAAM,IAAI,CAAC;AAEvB,QAAM,MAAM,KAAK,OAAO,CAAC,KAAK,SAAS,MAAM,MAAM,CAAC,IAAI,KAAK;AAC7D,QAAM,SACJ,KAAK,SAAS,MAAM,KACf,KAAK,KAAK,SAAS,IAAI,CAAC,IAAI,KAAK,KAAK,SAAS,CAAC,KAAK,IACtD,KAAK,KAAK,MAAM,KAAK,SAAS,CAAC,CAAC;AAEtC,SAAO;AAAA,IACL,SAAS,QAAQ;AAAA,IACjB,YAAY,KAAK,MAAM,MAAM,GAAG,IAAI;AAAA,IACpC,eAAe,KAAK,MAAM,SAAS,GAAG,IAAI;AAAA,EAC5C;AACF;AAMO,SAAS,eAAe,MAA4B;AACzD,QAAM,QAAQ,KAAK,YAAY;AAC/B,QAAM,OAAO,MAAM,MAAM,GAAG,EAAE,IAAI,KAAK;AAGvC,MACE,2IAA2I;AAAA,IACzI;AAAA,EACF,KACA,6BAA6B,KAAK,IAAI,KACtC,wDAAwD,KAAK,KAAK,GAClE;AACA,WAAO;AAAA,EACT;AAEA,MACE,wDAAwD,KAAK,KAAK,KAClE,yBAAyB,KAAK,IAAI,GAClC;AACA,WAAO;AAAA,EACT;AAEA,MAAI,sBAAsB,KAAK,KAAK,GAAG;AACrC,WAAO;AAAA,EACT;AAEA,MAAI,2BAA2B,KAAK,IAAI,KAAK,gBAAgB,KAAK,KAAK,GAAG;AACxE,WAAO;AAAA,EACT;AAEA,MACE,KAAK,WAAW,GAAG,KACnB,mDAAmD,KAAK,IAAI,KAC5D,oBAAoB,KAAK,IAAI,KAC7B,gDAAgD,KAAK,KAAK,KAC1D,wDAAwD,KAAK,IAAI,GACjE;AACA,WAAO;AAAA,EACT;AAEA,SAAO;AACT;AAEO,SAAS,wBAAwB,SAA0C;AAChF,MAAI,QAAQ,WAAW,GAAG;AACxB,WAAO;AAAA,EACT;AAEA,QAAM,SAAyB;AAAA,IAC7B,QAAQ;AAAA,IACR,OAAO;AAAA,IACP,YAAY;AAAA,IACZ,QAAQ;AAAA,IACR,MAAM;AAAA,IACN,WAAW;AAAA,EACb;AAEA,aAAW,UAAU,SAAS;AAC5B,eAAW,QAAQ,OAAO,MAAM,OAAO;AACrC,aAAO,eAAe,KAAK,IAAI,CAAC;AAAA,IAClC;AAAA,EACF;AAEA,SAAO;AACT;;;AC7FA,SAAS,kBAAkB,SAAqC;AAC9D,QAAM,SAAS,QAAQ,OAAO,CAAC,WAAW,OAAO,uBAAuB;AAExE,SAAO;AAAA,IACL,cAAc,QAAQ;AAAA,IACtB,eAAe,OAAO;AAAA,IACtB,YAAY,QAAQ,SAAS,IAAI,OAAO,SAAS,QAAQ,SAAS;AAAA,EACpE;AACF;AAEO,SAAS,oBACd,cACA,WAAwC,CAAC,WAAW,OAAO,KAAK,gBAAgB,MACpE;AACZ,QAAM,YAAY,aAAa,QAAQ,OAAO,QAAQ;AACtD,QAAM,SAAS,kBAAkB,SAAS;AAE1C,SAAO;AAAA,IACL,gBAAgB,OAAO;AAAA,IACvB,iBAAiB,OAAO;AAAA,IACxB,YAAY,OAAO;AAAA,EACrB;AACF;AAEO,SAAS,4BACd,cACA,WAAwC,CAAC,WAAW,OAAO,KAAK,gBAAgB,SAC9D;AAClB,QAAM,kBAAkB,aAAa,QAAQ,OAAO,QAAQ;AAC5D,SAAO,kBAAkB,eAAe;AAC1C;;;ACjCA,SAA+B,eAAAA,oBAAmB;AAU3C,SAAS,qBACd,cACA,WAAwC,CAAC,WAAW,OAAO,KAAK,gBAAgB,MACnE;AACb,QAAM,gBAAgB,aAAa,QAAQ,OAAO,QAAQ;AAE1D,MAAI,cAAc,WAAW,GAAG;AAC9B,WAAO;AAAA,MACL,mBAAmB;AAAA,MACnB,SAAS;AAAA,MACT,YAAY;AAAA,MACZ,SAAS;AAAA,QACP,MAAM;AAAA,QACN,MAAM;AAAA,QACN,OAAO;AAAA,QACP,QAAQ;AAAA,QACR,UAAU;AAAA,MACZ;AAAA,IACF;AAAA,EACF;AAGA,QAAM,iBAAiB,oBAAI,IAA2B;AAGtD,QAAM,gBAAgB,CAAC,GAAG,aAAa,OAAO,EAAE;AAAA,IAC9C,CAAC,GAAG,MAAM,IAAI,KAAK,EAAE,UAAU,EAAE,QAAQ,IAAI,IAAI,KAAK,EAAE,UAAU,EAAE,QAAQ;AAAA,EAC9E;AAGA,aAAW,UAAU,eAAe;AAClC,QAAI,SAAS,MAAM,GAAG;AACpB,YAAM,aAAa,IAAI,KAAK,OAAO,UAAU;AAE7C,iBAAW,QAAQ,OAAO,MAAM,OAAO;AACrC,YAAI,CAAC,eAAe,IAAI,KAAK,IAAI,GAAG;AAClC,yBAAe,IAAI,KAAK,MAAM;AAAA,YAC5B,MAAM,KAAK;AAAA,YACX,uBAAuB;AAAA,YACvB,cAAc;AAAA,YACd,iBAAiB;AAAA,UACnB,CAAC;AAAA,QACH;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAGA,aAAW,UAAU,eAAe;AAClC,UAAM,aAAa,IAAI,KAAK,OAAO,UAAU;AAE7C,eAAW,QAAQ,OAAO,MAAM,OAAO;AACrC,YAAM,YAAY,eAAe,IAAI,KAAK,IAAI;AAC9C,UAAI,WAAW;AAEb,YAAI,KAAK,WAAW,WAAW;AAC7B,oBAAU,eAAe;AAAA,QAC3B;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAGA,QAAM,kBAA4B,CAAC;AAEnC,aAAW,aAAa,eAAe,OAAO,GAAG;AAC/C,cAAU,kBAAkBA,aAAY,UAAU,uBAAuB,UAAU,YAAY;AAC/F,oBAAgB,KAAK,UAAU,eAAe;AAAA,EAChD;AAGA,QAAM,UACJ,gBAAgB,SAAS,IACrB,gBAAgB,OAAO,CAAC,KAAK,SAAS,MAAM,MAAM,CAAC,IAAI,gBAAgB,SACvE;AAEN,QAAM,aAAa,CAAC,GAAG,eAAe,EAAE,KAAK,CAAC,GAAG,MAAM,IAAI,CAAC;AAC5D,QAAM,aACJ,WAAW,SAAS,IAChB,WAAW,SAAS,MAAM,KACvB,WAAW,WAAW,SAAS,IAAI,CAAC,IAAI,WAAW,WAAW,SAAS,CAAC,KAAK,IAC9E,WAAW,KAAK,MAAM,WAAW,SAAS,CAAC,CAAC,IAC9C;AAGN,QAAM,UAAU;AAAA,IACd,MAAM,gBAAgB,OAAO,CAAC,SAAS,QAAQ,CAAC,EAAE;AAAA,IAClD,MAAM,gBAAgB,OAAO,CAAC,SAAS,QAAQ,KAAK,QAAQ,CAAC,EAAE;AAAA,IAC/D,OAAO,gBAAgB,OAAO,CAAC,SAAS,QAAQ,KAAK,QAAQ,EAAE,EAAE;AAAA,IACjE,QAAQ,gBAAgB,OAAO,CAAC,SAAS,QAAQ,MAAM,QAAQ,EAAE,EAAE;AAAA,IACnE,UAAU,gBAAgB,OAAO,CAAC,SAAS,OAAO,EAAE,EAAE;AAAA,EACxD;AAEA,SAAO;AAAA,IACL,mBAAmB,cAAc;AAAA,IACjC,SAAS,KAAK,MAAM,UAAU,GAAG,IAAI;AAAA;AAAA,IACrC,YAAY,KAAK,MAAM,aAAa,GAAG,IAAI;AAAA,IAC3C;AAAA,EACF;AACF;AAEO,SAAS,6BACd,cACA,WAAwC,CAAC,WAAW,OAAO,KAAK,gBAAgB,SACnE;AACb,SAAO,qBAAqB,cAAc,QAAQ;AACpD;;;ACpHA,SAAS,SAAS;AAIX,IAAM,cAAc,EAAE,OAAO;AAAA,EAClC,cAAc,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY;AAAA,EAC3C,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY;AAAA,EACjC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY;AAAA,EACpC,SAAS,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY;AAAA,EACtC,UAAU,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,CAAC;AAAA;AAAA,EACjC,oBAAoB,EAAE,KAAK,CAAC,MAAM,SAAS,SAAS,CAAC;AAAA;AAAA,EACrD,mBAAmB,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,CAAC;AAAA,EAC1C,gBAAgB,EAAE,QAAQ;AAAA;AAAA,EAE1B,OAAO,EAAE,OAAO;AAAA,IACd,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY;AAAA,IACnC,cAAc,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY;AAAA,IAC3C,UAAU,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY;AAAA,IACvC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY;AAAA,IACpC,SAAS,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY;AAAA,EACxC,CAAC;AAAA,EACD,cAAc,EAAE,OAAO;AAAA,IACrB,UAAU,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY;AAAA,IACvC,UAAU,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY;AAAA,IACvC,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY;AAAA,EACrC,CAAC;AACH,CAAC;AAEM,IAAM,aAAa,EAAE,OAAO;AAAA,EACjC,gBAAgB,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY;AAAA,EAC7C,iBAAiB,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY;AAAA,EAC9C,YAAY,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,CAAC;AACrC,CAAC;AAEM,IAAM,cAAc,EAAE,OAAO;AAAA,EAClC,mBAAmB,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY;AAAA,EAChD,SAAS,EAAE,OAAO,EAAE,YAAY;AAAA,EAChC,YAAY,EAAE,OAAO,EAAE,YAAY;AAAA,EACnC,SAAS,EAAE,OAAO;AAAA,IAChB,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY;AAAA,IACnC,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY;AAAA,IACnC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY;AAAA,IACpC,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY;AAAA,IACrC,UAAU,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY;AAAA,EACzC,CAAC;AACH,CAAC;AAIM,IAAM,WAAW,EAAE,OAAO;AAAA,EAC/B,SAAS,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY;AAAA,EACtC,YAAY,EAAE,OAAO,EAAE,YAAY;AAAA,EACnC,eAAe,EAAE,OAAO,EAAE,YAAY;AACxC,CAAC;AAIM,IAAM,eAAe,EAAE,KAAK;AAAA,EACjC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAEM,IAAM,iBAAiB,EAAE,OAAO;AAAA,EACrC,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY;AAAA,EACrC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY;AAAA,EACpC,YAAY,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY;AAAA,EACzC,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY;AAAA,EACrC,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY;AAAA,EACnC,WAAW,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY;AAC1C,CAAC;AAEM,IAAM,gBAAgB,EAAE,OAAO;AAAA,EACpC,KAAK,SAAS,SAAS;AAAA,EACvB,SAAS,eAAe,SAAS;AACnC,CAAC;AAEM,IAAM,mBAAmB,EAAE,OAAO;AAAA,EACvC,cAAc,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY;AAAA,EAC3C,eAAe,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY;AAAA,EAC5C,YAAY,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,CAAC;AACrC,CAAC;AAEM,IAAM,WAAW,EAAE,OAAO;AAAA;AAAA;AAAA,EAG/B,SAAS,EAAE,QAAQ;AAAA,EACnB,YAAY;AAAA,EACZ,aAAa;AACf,CAAC;AAEM,IAAM,QAAQ,EAAE,OAAO;AAAA,EAC5B,YAAY,EAAE,OAAO;AAAA,EACrB,oBAAoB,EAAE,OAAO;AAAA,EAC7B,uBAAuB,EAAE,OAAO;AAClC,CAAC;AAEM,IAAM,UAAU,EAAE,OAAO;AAAA,EAC9B,aAAa,EAAE,OAAO,EAAE,SAAS;AAAA,EACjC,QAAQ,EAAE,OAAO;AAAA,IACf,OAAO,EAAE,OAAO,EAAE,SAAS;AAAA,IAC3B,OAAO,EAAE,OAAO,EAAE,SAAS;AAAA,EAC7B,CAAC;AAAA,EACD,UAAU,EAAE,OAAO;AAAA,EACnB,eAAe,EAAE,OAAO;AAAA,EACxB,aAAa;AAAA,EACb,YAAY;AAAA,EACZ,aAAa;AAAA;AAAA;AAAA,EAGb,SAAS,EAAE,OAAO;AAAA,IAChB,IAAI;AAAA,IACJ,UAAU;AAAA,EACZ,CAAC;AAAA;AAAA;AAAA,EAGD,UAAU,SAAS,SAAS;AAAA,EAC5B,OAAO,MAAM,SAAS;AAAA,EACtB,SAAS,EAAE,MAAM,EAAE,OAAO,CAAC;AAC7B,CAAC;;;AJzGD,SAAS,MAAM,OAAe,UAA0B;AACtD,QAAM,SAAS,MAAM;AACrB,SAAO,KAAK,MAAM,QAAQ,MAAM,IAAI;AACtC;AAEO,SAAS,iBACd,cACA,UAA0B,CAAC,GAClB;AACT,QAAM,EAAE,qBAAqB,WAAW,oBAAoB,IAAI,IAAI;AAEpE,QAAM,SAAS,EAAE,IAAI,GAAG,OAAO,GAAG,SAAS,EAAE;AAC7C,QAAM,QAAQ,EAAE,MAAM,GAAG,cAAc,GAAG,UAAU,GAAG,OAAO,GAAG,SAAS,EAAE;AAC5E,QAAM,eAAe,EAAE,UAAU,GAAG,UAAU,GAAG,MAAM,EAAE;AACzD,aAAW,UAAU,aAAa,SAAS;AACzC,WAAO,OAAO,KAAK,WAAW;AAC9B,UAAM,OAAO,KAAK,IAAI;AACtB,iBAAa,OAAO,KAAK,YAAY;AAAA,EACvC;AACA,QAAM,QAAQ,aAAa,QAAQ;AACnC,QAAM,WAAW,QAAQ,KAAK,OAAO,KAAK,OAAO,SAAS,QAAQ;AAElE,QAAM,cAA2B;AAAA,IAC/B,cAAc;AAAA,IACd,IAAI,OAAO;AAAA,IACX,OAAO,OAAO;AAAA,IACd,SAAS,OAAO;AAAA,IAChB,UAAU,MAAM,UAAU,CAAC;AAAA,IAC3B;AAAA,IACA;AAAA,IACA,gBAAgB,WAAW;AAAA,IAC3B;AAAA,IACA;AAAA,EACF;AAKA,QAAM,aAAa,CAAC,WAAmB,OAAO,KAAK,QAAQ,SAAS,mBAAmB;AACvF,QAAM,OAAO,CAAC,WACZ,OAAO,KAAK,gBAAgB,QAC3B,uBAAuB,QAAQ,OAAO,KAAK,gBAAgB,aAAa,CAAC,WAAW,MAAM;AAC7F,QAAM,aAAa,CAAC,WAClB,OAAO,KAAK,gBAAgB,WAC3B,uBAAuB,WACtB,OAAO,KAAK,gBAAgB,aAC5B,CAAC,WAAW,MAAM;AAEtB,QAAM,aAAa,oBAAoB,cAAc,IAAI;AACzD,QAAM,cAAc,qBAAqB,cAAc,IAAI;AAG3D,QAAM,MAAM,oBAAI,KAAK;AACrB,QAAM,YAAY,aAAa,QAAQ,OAAO,IAAI;AAClD,QAAM,kBAAkB,aAAa,QAAQ,OAAO,UAAU;AAC9D,QAAM,UAAU;AAAA,IACd,IAAI;AAAA,MACF,KAAK,kBAAkB,WAAW,GAAG;AAAA,MACrC,SAAS,wBAAwB,SAAS;AAAA,IAC5C;AAAA,IACA,UAAU;AAAA,MACR,KAAK,kBAAkB,iBAAiB,GAAG;AAAA,MAC3C,SAAS,wBAAwB,eAAe;AAAA,IAClD;AAAA,EACF;AAIA,QAAM,eAAe,gBAAgB;AACrC,QAAM,kBAAkB,uBAAuB,WAAW,OAAO,UAAU;AAE3E,QAAM,WACJ,eAAe,IACX;AAAA,IACE,SAAS;AAAA,IACT,YAAY,4BAA4B,cAAc,UAAU;AAAA,IAChE,aAAa,6BAA6B,cAAc,UAAU;AAAA,EACpE,IACA;AAEN,QAAM,QAAQ,WACV;AAAA,IACE,YAAY,MAAM,WAAW,aAAa,SAAS,WAAW,YAAY,CAAC;AAAA,IAC3E,oBAAoB,MAAM,YAAY,UAAU,SAAS,YAAY,SAAS,CAAC;AAAA,IAC/E,uBAAuB;AAAA,MACrB,YAAY,aAAa,SAAS,YAAY;AAAA,MAC9C;AAAA,IACF;AAAA,EACF,IACA;AAEJ,QAAM,UAAU;AAAA,IACd,4BAA4B,WAAW,KAAK,QAAQ,CAAC,CAAC;AAAA,IACtD;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACA,MAAI,UAAU,SAAS;AACrB,YAAQ;AAAA,MACN,qBAAqB,OAAO,OAAO;AAAA,IACrC;AAAA,EACF;AACA,MAAI,CAAC,UAAU;AACb,YAAQ;AAAA,MACN;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AAAA,IACL,aAAa,cAAc,oBAAI,KAAK,CAAC;AAAA,IACrC,QAAQ;AAAA,MACN,OAAO,aAAa;AAAA,MACpB,OAAO,aAAa;AAAA,IACtB;AAAA,IACA,UAAU,aAAa;AAAA,IACvB,eAAe,aAAa;AAAA,IAC5B;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;","names":["daysBetween"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/cohort.ts","../src/merge-ratio.ts","../src/persistence.ts","../src/schema/metrics.ts"],"sourcesContent":["import { Commit, CommitStream, formatISODate } from '@aida-dev/core';\nimport { calculateAgeStats, calculateCategoryCounts } from './cohort.js';\nimport { calculateBaselineMergeRatio, calculateMergeRatio } from './merge-ratio.js';\nimport { calculateBaselinePersistence, calculatePersistence } from './persistence.js';\nimport { Attribution, Metrics } from './schema/metrics.js';\n\nexport * from './schema/metrics.js';\nexport * from './cohort.js';\nexport * from './merge-ratio.js';\nexport * from './persistence.js';\n\nexport interface MetricsOptions {\n // Prior applied to 'unknown' commits: which cohort (if any) they join.\n defaultAttribution?: 'ai' | 'human' | 'unknown';\n coverageThreshold?: number;\n}\n\nfunction round(value: number, decimals: number): number {\n const factor = 10 ** decimals;\n return Math.round(value * factor) / factor;\n}\n\nexport function calculateMetrics(\n commitStream: CommitStream,\n options: MetricsOptions = {}\n): Metrics {\n const { defaultAttribution = 'unknown', coverageThreshold = 0.7 } = options;\n\n const counts = { ai: 0, human: 0, automated: 0, unknown: 0 };\n const modes = { none: 0, autocomplete: 0, assisted: 0, agent: 0, unknown: 0 };\n const modeEvidence = { declared: 0, inferred: 0, none: 0 };\n for (const commit of commitStream.commits) {\n counts[commit.tags.attribution]++;\n modes[commit.tags.mode]++;\n modeEvidence[commit.tags.modeEvidence]++;\n }\n const total = commitStream.commits.length;\n // Automated commits have known provenance (#39): they count as covered\n const coverage = total > 0 ? (counts.ai + counts.human + counts.automated) / total : 0;\n\n const attribution: Attribution = {\n commitsTotal: total,\n ai: counts.ai,\n human: counts.human,\n automated: counts.automated,\n unknown: counts.unknown,\n coverage: round(coverage, 4),\n defaultAttribution,\n coverageThreshold,\n belowThreshold: coverage < coverageThreshold,\n modes,\n modeEvidence,\n };\n\n // Cohort membership: unknown commits join a cohort only via the prior.\n // 'automated' is its own state (#39): it joins no cohort and priors never\n // touch it — automation is not authored code.\n const isAI = (commit: Commit) =>\n commit.tags.attribution === 'ai' ||\n (defaultAttribution === 'ai' && commit.tags.attribution === 'unknown');\n const isBaseline = (commit: Commit) =>\n commit.tags.attribution === 'human' ||\n (defaultAttribution === 'human' && commit.tags.attribution === 'unknown');\n\n const mergeRatio = calculateMergeRatio(commitStream, isAI);\n const persistence = calculatePersistence(commitStream, isAI);\n\n // Fairness context (#29, #36): cohort age and task mix\n const now = new Date();\n const aiCommits = commitStream.commits.filter(isAI);\n const baselineCommits = commitStream.commits.filter(isBaseline);\n const cohorts = {\n ai: {\n age: calculateAgeStats(aiCommits, now),\n taskMix: calculateCategoryCounts(aiCommits),\n },\n baseline: {\n age: calculateAgeStats(baselineCommits, now),\n taskMix: calculateCategoryCounts(baselineCommits),\n },\n };\n\n // No baseline cohort → no baseline, no delta. AIDA does not invent a\n // comparison out of unattributed commits.\n const baselineSize = baselineCommits.length;\n const baselineAssumed = defaultAttribution === 'human' && counts.unknown > 0;\n\n const baseline =\n baselineSize > 0\n ? {\n assumed: baselineAssumed,\n mergeRatio: calculateBaselineMergeRatio(commitStream, isBaseline),\n persistence: calculateBaselinePersistence(commitStream, isBaseline),\n }\n : null;\n\n const delta = baseline\n ? {\n mergeRatio: round(mergeRatio.mergeRatio - baseline.mergeRatio.mergeRatio, 4),\n avgPersistenceDays: round(persistence.avgDays - baseline.persistence.avgDays, 2),\n medianPersistenceDays: round(\n persistence.medianDays - baseline.persistence.medianDays,\n 2\n ),\n }\n : null;\n\n const caveats = [\n `Attribution coverage is ${(coverage * 100).toFixed(1)}%: metrics only describe commits whose provenance is known.`,\n 'Persistence is file-level, not line-level.',\n 'Persistence is survival: days until the first subsequent modification. Files never modified again are censored at collection time. Migrations and generated files (convention-driven lifecycles) are excluded.',\n 'Persistence comparisons are only meaningful between cohorts of similar age and task mix — check the cohorts section before reading the delta.',\n 'Merge ratio: commits from all branches checked against default branch ancestry. Squash merges may undercount unmerged commits.',\n 'Time-windowed collection (--since) also windows the ancestry check: commits merged into the default branch before the window may appear unmerged.',\n 'AI tagging uses heuristic patterns; false positives/negatives possible.',\n ];\n if (baseline?.assumed) {\n caveats.push(\n `Baseline includes ${counts.unknown} unattributed commit(s) assumed human via defaultAttribution — undetected AI usage may leak into it.`\n );\n }\n if (!baseline) {\n caveats.push(\n 'No baseline: no commits are attributed as human. Set defaultAttribution to \"human\" in .aida.json if unattributed commits in this repo are human-authored.'\n );\n }\n\n return {\n generatedAt: formatISODate(new Date()),\n window: {\n since: commitStream.since,\n until: commitStream.until,\n },\n repoPath: commitStream.repoPath,\n defaultBranch: commitStream.defaultBranch,\n attribution,\n mergeRatio,\n persistence,\n cohorts,\n baseline,\n delta,\n caveats,\n };\n}\n","import { Commit, daysBetween } from '@aida-dev/core';\nimport { AgeStats, CategoryCounts, FileCategory } from './schema/metrics.js';\n\n// Cohort age (#29): persistence comparisons between cohorts of different\n// ages are misleading — old commits have had more time to accumulate\n// survival. Reporting each cohort's age lets consumers judge fairness.\nexport function calculateAgeStats(commits: Commit[], now: Date): AgeStats | null {\n if (commits.length === 0) {\n return null;\n }\n\n const ages = commits\n .map((commit) => daysBetween(new Date(commit.authorDate), now))\n .sort((a, b) => a - b);\n\n const avg = ages.reduce((sum, days) => sum + days, 0) / ages.length;\n const median =\n ages.length % 2 === 0\n ? (ages[ages.length / 2 - 1] + ages[ages.length / 2]) / 2\n : ages[Math.floor(ages.length / 2)];\n\n return {\n commits: commits.length,\n avgAgeDays: Math.round(avg * 100) / 100,\n medianAgeDays: Math.round(median * 100) / 100,\n };\n}\n\n// File categorization (#36, step 1): AI is often pointed at boilerplate,\n// tests, and migrations, which survive longer because nobody touches them.\n// Reporting each cohort's category mix shows when an AI-vs-baseline\n// comparison is apples-to-oranges. Order matters: first match wins.\nexport function categorizeFile(path: string): FileCategory {\n const lower = path.toLowerCase();\n const base = lower.split('/').pop() ?? lower;\n\n // Generated artifacts (lockfiles, changelogs, snapshots, build output)\n if (\n /^(pnpm-lock\\.yaml|package-lock\\.json|yarn\\.lock|bun\\.lockb?|composer\\.lock|cargo\\.lock|gemfile\\.lock|poetry\\.lock|changelog(\\.[a-z]+)?)$/.test(\n base\n ) ||\n /\\.(snap|min\\.js|min\\.css)$/.test(base) ||\n /(^|\\/)(dist|build|out|coverage|\\.next|node_modules)\\//.test(lower)\n ) {\n return 'generated';\n }\n\n if (\n /(^|\\/)(__tests__|__mocks__|tests?|spec|e2e|cypress)\\//.test(lower) ||\n /\\.(test|spec)\\.[a-z]+$/.test(base)\n ) {\n return 'tests';\n }\n\n if (/(^|\\/)migrations?\\//.test(lower)) {\n return 'migrations';\n }\n\n if (/\\.(md|mdx|rst|adoc|txt)$/.test(base) || /(^|\\/)docs?\\//.test(lower)) {\n return 'docs';\n }\n\n if (\n base.startsWith('.') ||\n /\\.(json|ya?ml|toml|ini|cfg|conf|env|properties)$/.test(base) ||\n /\\.config\\.[a-z]+$/.test(base) ||\n /(^|\\/)(\\.github|\\.circleci|\\.vscode|config)\\//.test(lower) ||\n /^(dockerfile|makefile|tsconfig.*|eslint.*|prettier.*)/.test(base)\n ) {\n return 'config';\n }\n\n return 'source';\n}\n\nexport function calculateCategoryCounts(commits: Commit[]): CategoryCounts | null {\n if (commits.length === 0) {\n return null;\n }\n\n const counts: CategoryCounts = {\n source: 0,\n tests: 0,\n migrations: 0,\n config: 0,\n docs: 0,\n generated: 0,\n };\n\n for (const commit of commits) {\n for (const file of commit.stats.files) {\n counts[categorizeFile(file.path)]++;\n }\n }\n\n return counts;\n}\n","import { Commit, CommitStream } from '@aida-dev/core';\nimport { CohortMergeRatio, MergeRatio } from './schema/metrics.js';\n\nfunction computeMergeRatio(commits: Commit[]): CohortMergeRatio {\n const merged = commits.filter((commit) => commit.inDefaultBranchAncestry);\n\n return {\n commitsTotal: commits.length,\n commitsMerged: merged.length,\n mergeRatio: commits.length > 0 ? merged.length / commits.length : 0,\n };\n}\n\nexport function calculateMergeRatio(\n commitStream: CommitStream,\n isTarget: (commit: Commit) => boolean = (commit) => commit.tags.attribution === 'ai'\n): MergeRatio {\n const aiCommits = commitStream.commits.filter(isTarget);\n const result = computeMergeRatio(aiCommits);\n\n return {\n aiCommitsTotal: result.commitsTotal,\n aiCommitsMerged: result.commitsMerged,\n mergeRatio: result.mergeRatio,\n };\n}\n\nexport function calculateBaselineMergeRatio(\n commitStream: CommitStream,\n isTarget: (commit: Commit) => boolean = (commit) => commit.tags.attribution === 'human'\n): CohortMergeRatio {\n const baselineCommits = commitStream.commits.filter(isTarget);\n return computeMergeRatio(baselineCommits);\n}\n","import { Commit, CommitStream, daysBetween } from '@aida-dev/core';\nimport { categorizeFile } from './cohort.js';\nimport { FileCategory, Persistence } from './schema/metrics.js';\n\n// Categories whose lifecycle is governed by convention, not code quality:\n// migrations are append-only (never modified once applied), generated files\n// (lockfiles, changelogs) churn on every release regardless of author.\n// Either way their survival carries no signal, so they are excluded from\n// persistence by default. They still appear in the task-mix table.\nexport const DEFAULT_PERSISTENCE_EXCLUDED_CATEGORIES: FileCategory[] = [\n 'migrations',\n 'generated',\n];\n\nexport interface PersistenceOptions {\n excludeCategories?: FileCategory[];\n // End of the observation window, used to measure censored files (never\n // modified again). Defaults to the stream's collection time.\n observationEnd?: Date;\n}\n\ninterface FileLifecycle {\n firstTargetIndex: number;\n firstTargetDate: Date;\n // Set when a later commit modifies or deletes the file: survival ends\n eventDate: Date | null;\n}\n\n// Persistence = survival: days from the first target-cohort touch of a file\n// until the FIRST subsequent modification (or deletion) by any commit.\n// Files never touched again are censored at the observation end — they\n// survived the whole window, which is the best possible outcome, not zero.\nexport function calculatePersistence(\n commitStream: CommitStream,\n isTarget: (commit: Commit) => boolean = (commit) => commit.tags.attribution === 'ai',\n options: PersistenceOptions = {}\n): Persistence {\n const {\n excludeCategories = DEFAULT_PERSISTENCE_EXCLUDED_CATEGORIES,\n observationEnd = new Date(commitStream.generatedAt),\n } = options;\n const excluded = new Set<FileCategory>(excludeCategories);\n\n const targetCommits = commitStream.commits.filter(isTarget);\n\n const empty: Persistence = {\n commitsConsidered: targetCommits.length,\n filesConsidered: 0,\n filesExcluded: 0,\n censored: 0,\n avgDays: 0,\n medianDays: 0,\n buckets: { d0_1: 0, d2_7: 0, d8_30: 0, d31_90: 0, d90_plus: 0 },\n };\n\n if (targetCommits.length === 0) {\n return { ...empty, commitsConsidered: 0 };\n }\n\n // Sort commits by date (oldest first)\n const sortedCommits = [...commitStream.commits].sort(\n (a, b) => new Date(a.authorDate).getTime() - new Date(b.authorDate).getTime()\n );\n\n // First target-cohort touch per file\n const lifecycles = new Map<string, FileLifecycle>();\n let filesExcluded = 0;\n sortedCommits.forEach((commit, index) => {\n if (!isTarget(commit)) return;\n for (const file of commit.stats.files) {\n if (lifecycles.has(file.path)) continue;\n if (excluded.has(categorizeFile(file.path))) {\n filesExcluded++;\n lifecycles.set(file.path, { firstTargetIndex: -1, firstTargetDate: new Date(0), eventDate: null });\n continue;\n }\n lifecycles.set(file.path, {\n firstTargetIndex: index,\n firstTargetDate: new Date(commit.authorDate),\n eventDate: null,\n });\n }\n });\n\n // First subsequent modification (or deletion) ends the survival clock\n sortedCommits.forEach((commit, index) => {\n for (const file of commit.stats.files) {\n const lifecycle = lifecycles.get(file.path);\n if (!lifecycle || lifecycle.firstTargetIndex < 0) continue; // excluded category\n if (index <= lifecycle.firstTargetIndex) continue; // not later than first touch\n if (lifecycle.eventDate === null) {\n lifecycle.eventDate = new Date(commit.authorDate);\n }\n }\n });\n\n const survivalDays: number[] = [];\n let censored = 0;\n for (const lifecycle of lifecycles.values()) {\n if (lifecycle.firstTargetIndex < 0) continue; // excluded category\n if (lifecycle.eventDate) {\n survivalDays.push(daysBetween(lifecycle.firstTargetDate, lifecycle.eventDate));\n } else {\n // Never modified again: survived to the end of the observation window\n censored++;\n survivalDays.push(Math.max(0, daysBetween(lifecycle.firstTargetDate, observationEnd)));\n }\n }\n\n if (survivalDays.length === 0) {\n return { ...empty, filesExcluded };\n }\n\n const avgDays = survivalDays.reduce((sum, days) => sum + days, 0) / survivalDays.length;\n const sortedDays = [...survivalDays].sort((a, b) => a - b);\n const medianDays =\n sortedDays.length % 2 === 0\n ? (sortedDays[sortedDays.length / 2 - 1] + sortedDays[sortedDays.length / 2]) / 2\n : sortedDays[Math.floor(sortedDays.length / 2)];\n\n return {\n commitsConsidered: targetCommits.length,\n filesConsidered: survivalDays.length,\n filesExcluded,\n censored,\n avgDays: Math.round(avgDays * 100) / 100,\n medianDays: Math.round(medianDays * 100) / 100,\n buckets: {\n d0_1: survivalDays.filter((days) => days <= 1).length,\n d2_7: survivalDays.filter((days) => days >= 2 && days <= 7).length,\n d8_30: survivalDays.filter((days) => days >= 8 && days <= 30).length,\n d31_90: survivalDays.filter((days) => days >= 31 && days <= 90).length,\n d90_plus: survivalDays.filter((days) => days > 90).length,\n },\n };\n}\n\nexport function calculateBaselinePersistence(\n commitStream: CommitStream,\n isTarget: (commit: Commit) => boolean = (commit) => commit.tags.attribution === 'human',\n options: PersistenceOptions = {}\n): Persistence {\n return calculatePersistence(commitStream, isTarget, options);\n}\n","import { z } from 'zod';\n\n// Attribution coverage (#34): the headline metric. Every other number in this\n// file is only as trustworthy as this block says it is.\nexport const Attribution = z.object({\n commitsTotal: z.number().int().nonnegative(),\n ai: z.number().int().nonnegative(),\n human: z.number().int().nonnegative(),\n // Provenance-known automation (#39): merge commits, bots, manifest-excluded.\n // Counts toward coverage, joins no cohort, untouched by priors.\n automated: z.number().int().nonnegative(),\n unknown: z.number().int().nonnegative(),\n coverage: z.number().min(0).max(1), // (ai + human + automated) / total\n defaultAttribution: z.enum(['ai', 'human', 'unknown']), // prior applied to unknown commits\n coverageThreshold: z.number().min(0).max(1),\n belowThreshold: z.boolean(),\n // Autonomy axis (#25): commit counts per mode and per mode-evidence level\n modes: z.object({\n none: z.number().int().nonnegative(),\n autocomplete: z.number().int().nonnegative(),\n assisted: z.number().int().nonnegative(),\n agent: z.number().int().nonnegative(),\n unknown: z.number().int().nonnegative(),\n }),\n modeEvidence: z.object({\n declared: z.number().int().nonnegative(),\n inferred: z.number().int().nonnegative(),\n none: z.number().int().nonnegative(),\n }),\n});\n\nexport const MergeRatio = z.object({\n aiCommitsTotal: z.number().int().nonnegative(),\n aiCommitsMerged: z.number().int().nonnegative(),\n mergeRatio: z.number().min(0).max(1),\n});\n\n// Persistence = survival: days from first target-cohort touch of a file to\n// the first subsequent modification. Files never modified again are censored\n// at the observation end (they survived the window — the best outcome).\n// Migrations and generated files are excluded by default: their lifecycle is\n// convention-driven and carries no quality signal.\nexport const Persistence = z.object({\n commitsConsidered: z.number().int().nonnegative(),\n filesConsidered: z.number().int().nonnegative(),\n filesExcluded: z.number().int().nonnegative(),\n censored: z.number().int().nonnegative(), // files that survived the whole window\n avgDays: z.number().nonnegative(),\n medianDays: z.number().nonnegative(),\n buckets: z.object({\n d0_1: z.number().int().nonnegative(),\n d2_7: z.number().int().nonnegative(),\n d8_30: z.number().int().nonnegative(),\n d31_90: z.number().int().nonnegative(),\n d90_plus: z.number().int().nonnegative(),\n }),\n});\n\n// Cohort age (#29): context for judging whether a persistence comparison\n// between cohorts is fair — older cohorts accumulate survival by default.\nexport const AgeStats = z.object({\n commits: z.number().int().nonnegative(),\n avgAgeDays: z.number().nonnegative(),\n medianAgeDays: z.number().nonnegative(),\n});\n\n// Task mix (#36): what kind of files each cohort touched. A persistence\n// comparison is only meaningful when the mixes are similar.\nexport const FileCategory = z.enum([\n 'source',\n 'tests',\n 'migrations',\n 'config',\n 'docs',\n 'generated',\n]);\n\nexport const CategoryCounts = z.object({\n source: z.number().int().nonnegative(),\n tests: z.number().int().nonnegative(),\n migrations: z.number().int().nonnegative(),\n config: z.number().int().nonnegative(),\n docs: z.number().int().nonnegative(),\n generated: z.number().int().nonnegative(),\n});\n\nexport const CohortContext = z.object({\n age: AgeStats.nullable(),\n taskMix: CategoryCounts.nullable(),\n});\n\nexport const CohortMergeRatio = z.object({\n commitsTotal: z.number().int().nonnegative(),\n commitsMerged: z.number().int().nonnegative(),\n mergeRatio: z.number().min(0).max(1),\n});\n\nexport const Baseline = z.object({\n // True when the cohort includes 'unknown' commits via defaultAttribution:\n // the baseline is an assumption, not observed attribution.\n assumed: z.boolean(),\n mergeRatio: CohortMergeRatio,\n persistence: Persistence,\n});\n\nexport const Delta = z.object({\n mergeRatio: z.number(),\n avgPersistenceDays: z.number(),\n medianPersistenceDays: z.number(),\n});\n\nexport const Metrics = z.object({\n generatedAt: z.string().datetime(),\n window: z.object({\n since: z.string().optional(),\n until: z.string().optional(),\n }),\n repoPath: z.string(),\n defaultBranch: z.string(),\n attribution: Attribution,\n mergeRatio: MergeRatio,\n persistence: Persistence,\n // Fairness context (#29, #36): age and task mix per cohort, so consumers\n // can judge whether the AI-vs-baseline comparison is apples-to-apples.\n cohorts: z.object({\n ai: CohortContext,\n baseline: CohortContext,\n }),\n // Null when no commit is attributed 'human' and no defaultAttribution prior\n // assigns the unknowns: AIDA does not invent a comparison cohort.\n baseline: Baseline.nullable(),\n delta: Delta.nullable(),\n caveats: z.array(z.string()),\n});\n\nexport type Attribution = z.infer<typeof Attribution>;\nexport type AgeStats = z.infer<typeof AgeStats>;\nexport type FileCategory = z.infer<typeof FileCategory>;\nexport type CategoryCounts = z.infer<typeof CategoryCounts>;\nexport type CohortContext = z.infer<typeof CohortContext>;\nexport type MergeRatio = z.infer<typeof MergeRatio>;\nexport type Persistence = z.infer<typeof Persistence>;\nexport type CohortMergeRatio = z.infer<typeof CohortMergeRatio>;\nexport type Baseline = z.infer<typeof Baseline>;\nexport type Delta = z.infer<typeof Delta>;\nexport type Metrics = z.infer<typeof Metrics>;\n"],"mappings":"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","names":["daysBetween","daysBetween"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aida-dev/metrics",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "Metrics engine for AIDA (AI Development Accounting) - Merge ratio, persistence, and AI impact analysis",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -11,7 +11,7 @@
11
11
  ],
12
12
  "dependencies": {
13
13
  "zod": "^3.22.4",
14
- "@aida-dev/core": "0.10.0"
14
+ "@aida-dev/core": "0.11.0"
15
15
  },
16
16
  "devDependencies": {
17
17
  "tsup": "^8.0.0",