@aida-dev/metrics 0.3.1 → 0.4.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
@@ -1,6 +1,35 @@
1
1
  import { CommitStream, Commit } from '@aida-dev/core';
2
2
  import { z } from 'zod';
3
3
 
4
+ declare const Attribution: z.ZodObject<{
5
+ commitsTotal: z.ZodNumber;
6
+ ai: z.ZodNumber;
7
+ human: z.ZodNumber;
8
+ unknown: z.ZodNumber;
9
+ coverage: z.ZodNumber;
10
+ defaultAttribution: z.ZodEnum<["ai", "human", "unknown"]>;
11
+ coverageThreshold: z.ZodNumber;
12
+ belowThreshold: z.ZodBoolean;
13
+ }, "strip", z.ZodTypeAny, {
14
+ ai: number;
15
+ human: number;
16
+ unknown: number;
17
+ commitsTotal: number;
18
+ coverage: number;
19
+ defaultAttribution: "ai" | "human" | "unknown";
20
+ coverageThreshold: number;
21
+ belowThreshold: boolean;
22
+ }, {
23
+ ai: number;
24
+ human: number;
25
+ unknown: number;
26
+ commitsTotal: number;
27
+ coverage: number;
28
+ defaultAttribution: "ai" | "human" | "unknown";
29
+ coverageThreshold: number;
30
+ belowThreshold: boolean;
31
+ }>;
32
+ type Attribution = z.infer<typeof Attribution>;
4
33
  declare const MergeRatio: z.ZodObject<{
5
34
  aiCommitsTotal: z.ZodNumber;
6
35
  aiCommitsMerged: z.ZodNumber;
@@ -67,27 +96,28 @@ declare const CohortMergeRatio: z.ZodObject<{
67
96
  commitsMerged: z.ZodNumber;
68
97
  mergeRatio: z.ZodNumber;
69
98
  }, "strip", z.ZodTypeAny, {
70
- mergeRatio: number;
71
99
  commitsTotal: number;
100
+ mergeRatio: number;
72
101
  commitsMerged: number;
73
102
  }, {
74
- mergeRatio: number;
75
103
  commitsTotal: number;
104
+ mergeRatio: number;
76
105
  commitsMerged: number;
77
106
  }>;
78
107
  type CohortMergeRatio = z.infer<typeof CohortMergeRatio>;
79
108
  declare const Baseline: z.ZodObject<{
109
+ assumed: z.ZodBoolean;
80
110
  mergeRatio: z.ZodObject<{
81
111
  commitsTotal: z.ZodNumber;
82
112
  commitsMerged: z.ZodNumber;
83
113
  mergeRatio: z.ZodNumber;
84
114
  }, "strip", z.ZodTypeAny, {
85
- mergeRatio: number;
86
115
  commitsTotal: number;
116
+ mergeRatio: number;
87
117
  commitsMerged: number;
88
118
  }, {
89
- mergeRatio: number;
90
119
  commitsTotal: number;
120
+ mergeRatio: number;
91
121
  commitsMerged: number;
92
122
  }>;
93
123
  persistence: z.ZodObject<{
@@ -138,8 +168,8 @@ declare const Baseline: z.ZodObject<{
138
168
  }>;
139
169
  }, "strip", z.ZodTypeAny, {
140
170
  mergeRatio: {
141
- mergeRatio: number;
142
171
  commitsTotal: number;
172
+ mergeRatio: number;
143
173
  commitsMerged: number;
144
174
  };
145
175
  persistence: {
@@ -154,10 +184,11 @@ declare const Baseline: z.ZodObject<{
154
184
  d90_plus: number;
155
185
  };
156
186
  };
187
+ assumed: boolean;
157
188
  }, {
158
189
  mergeRatio: {
159
- mergeRatio: number;
160
190
  commitsTotal: number;
191
+ mergeRatio: number;
161
192
  commitsMerged: number;
162
193
  };
163
194
  persistence: {
@@ -172,6 +203,7 @@ declare const Baseline: z.ZodObject<{
172
203
  d90_plus: number;
173
204
  };
174
205
  };
206
+ assumed: boolean;
175
207
  }>;
176
208
  type Baseline = z.infer<typeof Baseline>;
177
209
  declare const Delta: z.ZodObject<{
@@ -202,6 +234,34 @@ declare const Metrics: z.ZodObject<{
202
234
  }>;
203
235
  repoPath: z.ZodString;
204
236
  defaultBranch: z.ZodString;
237
+ attribution: z.ZodObject<{
238
+ commitsTotal: z.ZodNumber;
239
+ ai: z.ZodNumber;
240
+ human: z.ZodNumber;
241
+ unknown: z.ZodNumber;
242
+ coverage: z.ZodNumber;
243
+ defaultAttribution: z.ZodEnum<["ai", "human", "unknown"]>;
244
+ coverageThreshold: z.ZodNumber;
245
+ belowThreshold: z.ZodBoolean;
246
+ }, "strip", z.ZodTypeAny, {
247
+ ai: number;
248
+ human: number;
249
+ unknown: number;
250
+ commitsTotal: number;
251
+ coverage: number;
252
+ defaultAttribution: "ai" | "human" | "unknown";
253
+ coverageThreshold: number;
254
+ belowThreshold: boolean;
255
+ }, {
256
+ ai: number;
257
+ human: number;
258
+ unknown: number;
259
+ commitsTotal: number;
260
+ coverage: number;
261
+ defaultAttribution: "ai" | "human" | "unknown";
262
+ coverageThreshold: number;
263
+ belowThreshold: boolean;
264
+ }>;
205
265
  mergeRatio: z.ZodObject<{
206
266
  aiCommitsTotal: z.ZodNumber;
207
267
  aiCommitsMerged: z.ZodNumber;
@@ -261,18 +321,19 @@ declare const Metrics: z.ZodObject<{
261
321
  d90_plus: number;
262
322
  };
263
323
  }>;
264
- baseline: z.ZodObject<{
324
+ baseline: z.ZodNullable<z.ZodObject<{
325
+ assumed: z.ZodBoolean;
265
326
  mergeRatio: z.ZodObject<{
266
327
  commitsTotal: z.ZodNumber;
267
328
  commitsMerged: z.ZodNumber;
268
329
  mergeRatio: z.ZodNumber;
269
330
  }, "strip", z.ZodTypeAny, {
270
- mergeRatio: number;
271
331
  commitsTotal: number;
332
+ mergeRatio: number;
272
333
  commitsMerged: number;
273
334
  }, {
274
- mergeRatio: number;
275
335
  commitsTotal: number;
336
+ mergeRatio: number;
276
337
  commitsMerged: number;
277
338
  }>;
278
339
  persistence: z.ZodObject<{
@@ -323,8 +384,8 @@ declare const Metrics: z.ZodObject<{
323
384
  }>;
324
385
  }, "strip", z.ZodTypeAny, {
325
386
  mergeRatio: {
326
- mergeRatio: number;
327
387
  commitsTotal: number;
388
+ mergeRatio: number;
328
389
  commitsMerged: number;
329
390
  };
330
391
  persistence: {
@@ -339,10 +400,11 @@ declare const Metrics: z.ZodObject<{
339
400
  d90_plus: number;
340
401
  };
341
402
  };
403
+ assumed: boolean;
342
404
  }, {
343
405
  mergeRatio: {
344
- mergeRatio: number;
345
406
  commitsTotal: number;
407
+ mergeRatio: number;
346
408
  commitsMerged: number;
347
409
  };
348
410
  persistence: {
@@ -357,8 +419,9 @@ declare const Metrics: z.ZodObject<{
357
419
  d90_plus: number;
358
420
  };
359
421
  };
360
- }>;
361
- delta: z.ZodObject<{
422
+ assumed: boolean;
423
+ }>>;
424
+ delta: z.ZodNullable<z.ZodObject<{
362
425
  mergeRatio: z.ZodNumber;
363
426
  avgPersistenceDays: z.ZodNumber;
364
427
  medianPersistenceDays: z.ZodNumber;
@@ -370,7 +433,7 @@ declare const Metrics: z.ZodObject<{
370
433
  mergeRatio: number;
371
434
  avgPersistenceDays: number;
372
435
  medianPersistenceDays: number;
373
- }>;
436
+ }>>;
374
437
  caveats: z.ZodArray<z.ZodString, "many">;
375
438
  }, "strip", z.ZodTypeAny, {
376
439
  generatedAt: string;
@@ -380,6 +443,16 @@ declare const Metrics: z.ZodObject<{
380
443
  };
381
444
  repoPath: string;
382
445
  defaultBranch: string;
446
+ attribution: {
447
+ ai: number;
448
+ human: number;
449
+ unknown: number;
450
+ commitsTotal: number;
451
+ coverage: number;
452
+ defaultAttribution: "ai" | "human" | "unknown";
453
+ coverageThreshold: number;
454
+ belowThreshold: boolean;
455
+ };
383
456
  mergeRatio: {
384
457
  aiCommitsTotal: number;
385
458
  aiCommitsMerged: number;
@@ -399,8 +472,8 @@ declare const Metrics: z.ZodObject<{
399
472
  };
400
473
  baseline: {
401
474
  mergeRatio: {
402
- mergeRatio: number;
403
475
  commitsTotal: number;
476
+ mergeRatio: number;
404
477
  commitsMerged: number;
405
478
  };
406
479
  persistence: {
@@ -415,12 +488,13 @@ declare const Metrics: z.ZodObject<{
415
488
  d90_plus: number;
416
489
  };
417
490
  };
418
- };
491
+ assumed: boolean;
492
+ } | null;
419
493
  delta: {
420
494
  mergeRatio: number;
421
495
  avgPersistenceDays: number;
422
496
  medianPersistenceDays: number;
423
- };
497
+ } | null;
424
498
  caveats: string[];
425
499
  }, {
426
500
  generatedAt: string;
@@ -430,6 +504,16 @@ declare const Metrics: z.ZodObject<{
430
504
  };
431
505
  repoPath: string;
432
506
  defaultBranch: string;
507
+ attribution: {
508
+ ai: number;
509
+ human: number;
510
+ unknown: number;
511
+ commitsTotal: number;
512
+ coverage: number;
513
+ defaultAttribution: "ai" | "human" | "unknown";
514
+ coverageThreshold: number;
515
+ belowThreshold: boolean;
516
+ };
433
517
  mergeRatio: {
434
518
  aiCommitsTotal: number;
435
519
  aiCommitsMerged: number;
@@ -449,8 +533,8 @@ declare const Metrics: z.ZodObject<{
449
533
  };
450
534
  baseline: {
451
535
  mergeRatio: {
452
- mergeRatio: number;
453
536
  commitsTotal: number;
537
+ mergeRatio: number;
454
538
  commitsMerged: number;
455
539
  };
456
540
  persistence: {
@@ -465,22 +549,27 @@ declare const Metrics: z.ZodObject<{
465
549
  d90_plus: number;
466
550
  };
467
551
  };
468
- };
552
+ assumed: boolean;
553
+ } | null;
469
554
  delta: {
470
555
  mergeRatio: number;
471
556
  avgPersistenceDays: number;
472
557
  medianPersistenceDays: number;
473
- };
558
+ } | null;
474
559
  caveats: string[];
475
560
  }>;
476
561
  type Metrics = z.infer<typeof Metrics>;
477
562
 
478
- declare function calculateMergeRatio(commitStream: CommitStream): MergeRatio;
479
- declare function calculateBaselineMergeRatio(commitStream: CommitStream): CohortMergeRatio;
563
+ declare function calculateMergeRatio(commitStream: CommitStream, isTarget?: (commit: Commit) => boolean): MergeRatio;
564
+ declare function calculateBaselineMergeRatio(commitStream: CommitStream, isTarget?: (commit: Commit) => boolean): CohortMergeRatio;
480
565
 
481
566
  declare function calculatePersistence(commitStream: CommitStream, isTarget?: (commit: Commit) => boolean): Persistence;
482
- declare function calculateBaselinePersistence(commitStream: CommitStream): Persistence;
567
+ declare function calculateBaselinePersistence(commitStream: CommitStream, isTarget?: (commit: Commit) => boolean): Persistence;
483
568
 
484
- declare function calculateMetrics(commitStream: CommitStream): Metrics;
569
+ interface MetricsOptions {
570
+ defaultAttribution?: 'ai' | 'human' | 'unknown';
571
+ coverageThreshold?: number;
572
+ }
573
+ declare function calculateMetrics(commitStream: CommitStream, options?: MetricsOptions): Metrics;
485
574
 
486
- export { Baseline, CohortMergeRatio, Delta, MergeRatio, Metrics, Persistence, calculateBaselineMergeRatio, calculateBaselinePersistence, calculateMergeRatio, calculateMetrics, calculatePersistence };
575
+ export { Attribution, Baseline, CohortMergeRatio, Delta, MergeRatio, Metrics, type MetricsOptions, Persistence, calculateBaselineMergeRatio, calculateBaselinePersistence, calculateMergeRatio, calculateMetrics, calculatePersistence };
package/dist/index.js CHANGED
@@ -10,8 +10,8 @@ function computeMergeRatio(commits) {
10
10
  mergeRatio: commits.length > 0 ? merged.length / commits.length : 0
11
11
  };
12
12
  }
13
- function calculateMergeRatio(commitStream) {
14
- const aiCommits = commitStream.commits.filter((commit) => commit.tags.ai);
13
+ function calculateMergeRatio(commitStream, isTarget = (commit) => commit.tags.attribution === "ai") {
14
+ const aiCommits = commitStream.commits.filter(isTarget);
15
15
  const result = computeMergeRatio(aiCommits);
16
16
  return {
17
17
  aiCommitsTotal: result.commitsTotal,
@@ -19,14 +19,14 @@ function calculateMergeRatio(commitStream) {
19
19
  mergeRatio: result.mergeRatio
20
20
  };
21
21
  }
22
- function calculateBaselineMergeRatio(commitStream) {
23
- const nonAICommits = commitStream.commits.filter((commit) => !commit.tags.ai);
24
- return computeMergeRatio(nonAICommits);
22
+ function calculateBaselineMergeRatio(commitStream, isTarget = (commit) => commit.tags.attribution === "human") {
23
+ const baselineCommits = commitStream.commits.filter(isTarget);
24
+ return computeMergeRatio(baselineCommits);
25
25
  }
26
26
 
27
27
  // src/persistence.ts
28
28
  import { daysBetween } from "@aida-dev/core";
29
- function calculatePersistence(commitStream, isTarget = (commit) => commit.tags.ai) {
29
+ function calculatePersistence(commitStream, isTarget = (commit) => commit.tags.attribution === "ai") {
30
30
  const targetCommits = commitStream.commits.filter(isTarget);
31
31
  if (targetCommits.length === 0) {
32
32
  return {
@@ -95,12 +95,24 @@ function calculatePersistence(commitStream, isTarget = (commit) => commit.tags.a
95
95
  buckets
96
96
  };
97
97
  }
98
- function calculateBaselinePersistence(commitStream) {
99
- return calculatePersistence(commitStream, (commit) => !commit.tags.ai);
98
+ function calculateBaselinePersistence(commitStream, isTarget = (commit) => commit.tags.attribution === "human") {
99
+ return calculatePersistence(commitStream, isTarget);
100
100
  }
101
101
 
102
102
  // src/schema/metrics.ts
103
103
  import { z } from "zod";
104
+ var Attribution = z.object({
105
+ commitsTotal: z.number().int().nonnegative(),
106
+ ai: z.number().int().nonnegative(),
107
+ human: z.number().int().nonnegative(),
108
+ unknown: z.number().int().nonnegative(),
109
+ coverage: z.number().min(0).max(1),
110
+ // (ai + human) / total
111
+ defaultAttribution: z.enum(["ai", "human", "unknown"]),
112
+ // prior applied to unknown commits
113
+ coverageThreshold: z.number().min(0).max(1),
114
+ belowThreshold: z.boolean()
115
+ });
104
116
  var MergeRatio = z.object({
105
117
  aiCommitsTotal: z.number().int().nonnegative(),
106
118
  aiCommitsMerged: z.number().int().nonnegative(),
@@ -124,6 +136,9 @@ var CohortMergeRatio = z.object({
124
136
  mergeRatio: z.number().min(0).max(1)
125
137
  });
126
138
  var Baseline = z.object({
139
+ // True when the cohort includes 'unknown' commits via defaultAttribution:
140
+ // the baseline is an assumption, not observed attribution.
141
+ assumed: z.boolean(),
127
142
  mergeRatio: CohortMergeRatio,
128
143
  persistence: Persistence
129
144
  });
@@ -140,10 +155,13 @@ var Metrics = z.object({
140
155
  }),
141
156
  repoPath: z.string(),
142
157
  defaultBranch: z.string(),
158
+ attribution: Attribution,
143
159
  mergeRatio: MergeRatio,
144
160
  persistence: Persistence,
145
- baseline: Baseline,
146
- delta: Delta,
161
+ // Null when no commit is attributed 'human' and no defaultAttribution prior
162
+ // assigns the unknowns: AIDA does not invent a comparison cohort.
163
+ baseline: Baseline.nullable(),
164
+ delta: Delta.nullable(),
147
165
  caveats: z.array(z.string())
148
166
  });
149
167
 
@@ -152,22 +170,60 @@ function round(value, decimals) {
152
170
  const factor = 10 ** decimals;
153
171
  return Math.round(value * factor) / factor;
154
172
  }
155
- function calculateMetrics(commitStream) {
156
- const mergeRatio = calculateMergeRatio(commitStream);
157
- const persistence = calculatePersistence(commitStream);
158
- const baselineMergeRatio = calculateBaselineMergeRatio(commitStream);
159
- const baselinePersistence = calculateBaselinePersistence(commitStream);
160
- const delta = {
161
- mergeRatio: round(mergeRatio.mergeRatio - baselineMergeRatio.mergeRatio, 4),
162
- avgPersistenceDays: round(persistence.avgDays - baselinePersistence.avgDays, 2),
163
- medianPersistenceDays: round(persistence.medianDays - baselinePersistence.medianDays, 2)
173
+ function calculateMetrics(commitStream, options = {}) {
174
+ const { defaultAttribution = "unknown", coverageThreshold = 0.7 } = options;
175
+ const counts = { ai: 0, human: 0, unknown: 0 };
176
+ for (const commit of commitStream.commits) {
177
+ counts[commit.tags.attribution]++;
178
+ }
179
+ const total = commitStream.commits.length;
180
+ const coverage = total > 0 ? (counts.ai + counts.human) / total : 0;
181
+ const attribution = {
182
+ commitsTotal: total,
183
+ ai: counts.ai,
184
+ human: counts.human,
185
+ unknown: counts.unknown,
186
+ coverage: round(coverage, 4),
187
+ defaultAttribution,
188
+ coverageThreshold,
189
+ belowThreshold: coverage < coverageThreshold
164
190
  };
191
+ const isAI = (commit) => commit.tags.attribution === "ai" || defaultAttribution === "ai" && commit.tags.attribution === "unknown";
192
+ const isBaseline = (commit) => commit.tags.attribution === "human" || defaultAttribution === "human" && commit.tags.attribution === "unknown";
193
+ const mergeRatio = calculateMergeRatio(commitStream, isAI);
194
+ const persistence = calculatePersistence(commitStream, isAI);
195
+ const baselineSize = commitStream.commits.filter(isBaseline).length;
196
+ const baselineAssumed = defaultAttribution === "human" && counts.unknown > 0;
197
+ const baseline = baselineSize > 0 ? {
198
+ assumed: baselineAssumed,
199
+ mergeRatio: calculateBaselineMergeRatio(commitStream, isBaseline),
200
+ persistence: calculateBaselinePersistence(commitStream, isBaseline)
201
+ } : null;
202
+ const delta = baseline ? {
203
+ mergeRatio: round(mergeRatio.mergeRatio - baseline.mergeRatio.mergeRatio, 4),
204
+ avgPersistenceDays: round(persistence.avgDays - baseline.persistence.avgDays, 2),
205
+ medianPersistenceDays: round(
206
+ persistence.medianDays - baseline.persistence.medianDays,
207
+ 2
208
+ )
209
+ } : null;
165
210
  const caveats = [
211
+ `Attribution coverage is ${(coverage * 100).toFixed(1)}%: metrics only describe commits whose provenance is known.`,
166
212
  "Persistence is file-level, not line-level.",
167
213
  "Merge ratio: commits from all branches checked against default branch ancestry. Squash merges may undercount unmerged commits.",
168
- "AI tagging uses heuristic patterns; false positives/negatives possible.",
169
- "Baseline covers all non-AI-tagged commits; undetected AI usage may leak into the baseline."
214
+ "Time-windowed collection (--since) also windows the ancestry check: commits merged into the default branch before the window may appear unmerged.",
215
+ "AI tagging uses heuristic patterns; false positives/negatives possible."
170
216
  ];
217
+ if (baseline?.assumed) {
218
+ caveats.push(
219
+ `Baseline includes ${counts.unknown} unattributed commit(s) assumed human via defaultAttribution \u2014 undetected AI usage may leak into it.`
220
+ );
221
+ }
222
+ if (!baseline) {
223
+ caveats.push(
224
+ 'No baseline: no commits are attributed as human. Set defaultAttribution to "human" in .aida.json if unattributed commits in this repo are human-authored.'
225
+ );
226
+ }
171
227
  return {
172
228
  generatedAt: formatISODate(/* @__PURE__ */ new Date()),
173
229
  window: {
@@ -176,17 +232,16 @@ function calculateMetrics(commitStream) {
176
232
  },
177
233
  repoPath: commitStream.repoPath,
178
234
  defaultBranch: commitStream.defaultBranch,
235
+ attribution,
179
236
  mergeRatio,
180
237
  persistence,
181
- baseline: {
182
- mergeRatio: baselineMergeRatio,
183
- persistence: baselinePersistence
184
- },
238
+ baseline,
185
239
  delta,
186
240
  caveats
187
241
  };
188
242
  }
189
243
  export {
244
+ Attribution,
190
245
  Baseline,
191
246
  CohortMergeRatio,
192
247
  Delta,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/merge-ratio.ts","../src/persistence.ts","../src/schema/metrics.ts"],"sourcesContent":["import { CommitStream, formatISODate } from '@aida-dev/core';\nimport { calculateBaselineMergeRatio, calculateMergeRatio } from './merge-ratio.js';\nimport { calculateBaselinePersistence, calculatePersistence } from './persistence.js';\nimport { Metrics } from './schema/metrics.js';\n\nexport * from './schema/metrics.js';\nexport * from './merge-ratio.js';\nexport * from './persistence.js';\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(commitStream: CommitStream): Metrics {\n const mergeRatio = calculateMergeRatio(commitStream);\n const persistence = calculatePersistence(commitStream);\n const baselineMergeRatio = calculateBaselineMergeRatio(commitStream);\n const baselinePersistence = calculateBaselinePersistence(commitStream);\n\n const delta = {\n mergeRatio: round(mergeRatio.mergeRatio - baselineMergeRatio.mergeRatio, 4),\n avgPersistenceDays: round(persistence.avgDays - baselinePersistence.avgDays, 2),\n medianPersistenceDays: round(persistence.medianDays - baselinePersistence.medianDays, 2),\n };\n\n const caveats = [\n 'Persistence is file-level, not line-level.',\n 'Merge ratio: commits from all branches checked against default branch ancestry. Squash merges may undercount unmerged commits.',\n 'AI tagging uses heuristic patterns; false positives/negatives possible.',\n 'Baseline covers all non-AI-tagged commits; undetected AI usage may leak into the baseline.',\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 mergeRatio,\n persistence,\n baseline: {\n mergeRatio: baselineMergeRatio,\n persistence: baselinePersistence,\n },\n delta,\n caveats,\n };\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(commitStream: CommitStream): MergeRatio {\n const aiCommits = commitStream.commits.filter((commit) => commit.tags.ai);\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(commitStream: CommitStream): CohortMergeRatio {\n const nonAICommits = commitStream.commits.filter((commit) => !commit.tags.ai);\n return computeMergeRatio(nonAICommits);\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.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(commitStream: CommitStream): Persistence {\n return calculatePersistence(commitStream, (commit) => !commit.tags.ai);\n}\n","import { z } from 'zod';\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\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 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 mergeRatio: MergeRatio,\n persistence: Persistence,\n baseline: Baseline,\n delta: Delta,\n caveats: z.array(z.string()),\n});\n\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":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/merge-ratio.ts","../src/persistence.ts","../src/schema/metrics.ts"],"sourcesContent":["import { Commit, CommitStream, formatISODate } from '@aida-dev/core';\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 './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 for (const commit of commitStream.commits) {\n counts[commit.tags.attribution]++;\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 };\n\n // Cohort membership: unknown commits join a cohort only via the prior.\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 // No baseline cohort → no baseline, no delta. AIDA does not invent a\n // comparison out of unattributed commits.\n const baselineSize = commitStream.commits.filter(isBaseline).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 '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 baseline,\n delta,\n caveats,\n };\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});\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\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 // 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 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":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aida-dev/metrics",
3
- "version": "0.3.1",
3
+ "version": "0.4.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.8.0"
14
+ "@aida-dev/core": "0.9.0"
15
15
  },
16
16
  "devDependencies": {
17
17
  "tsup": "^8.0.0",