@aida-dev/metrics 0.6.0 → 0.8.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.js CHANGED
@@ -58,29 +58,6 @@ function calculateCategoryCounts(commits) {
58
58
  return counts;
59
59
  }
60
60
 
61
- // src/merge-ratio.ts
62
- function computeMergeRatio(commits) {
63
- const merged = commits.filter((commit) => commit.inDefaultBranchAncestry);
64
- return {
65
- commitsTotal: commits.length,
66
- commitsMerged: merged.length,
67
- mergeRatio: commits.length > 0 ? merged.length / commits.length : 0
68
- };
69
- }
70
- function calculateMergeRatio(commitStream, isTarget = (commit) => commit.tags.attribution === "ai") {
71
- const aiCommits = commitStream.commits.filter(isTarget);
72
- const result = computeMergeRatio(aiCommits);
73
- return {
74
- aiCommitsTotal: result.commitsTotal,
75
- aiCommitsMerged: result.commitsMerged,
76
- mergeRatio: result.mergeRatio
77
- };
78
- }
79
- function calculateBaselineMergeRatio(commitStream, isTarget = (commit) => commit.tags.attribution === "human") {
80
- const baselineCommits = commitStream.commits.filter(isTarget);
81
- return computeMergeRatio(baselineCommits);
82
- }
83
-
84
61
  // src/persistence.ts
85
62
  import { daysBetween as daysBetween2 } from "@aida-dev/core";
86
63
  var DEFAULT_PERSISTENCE_EXCLUDED_CATEGORIES = [
@@ -180,9 +157,12 @@ var Attribution = z.object({
180
157
  commitsTotal: z.number().int().nonnegative(),
181
158
  ai: z.number().int().nonnegative(),
182
159
  human: z.number().int().nonnegative(),
160
+ // Provenance-known automation (#39): merge commits, bots, manifest-excluded.
161
+ // Counts toward coverage, joins no cohort, untouched by priors.
162
+ automated: z.number().int().nonnegative(),
183
163
  unknown: z.number().int().nonnegative(),
184
164
  coverage: z.number().min(0).max(1),
185
- // (ai + human) / total
165
+ // (ai + human + automated) / total
186
166
  defaultAttribution: z.enum(["ai", "human", "unknown"]),
187
167
  // prior applied to unknown commits
188
168
  coverageThreshold: z.number().min(0).max(1),
@@ -201,11 +181,6 @@ var Attribution = z.object({
201
181
  none: z.number().int().nonnegative()
202
182
  })
203
183
  });
204
- var MergeRatio = z.object({
205
- aiCommitsTotal: z.number().int().nonnegative(),
206
- aiCommitsMerged: z.number().int().nonnegative(),
207
- mergeRatio: z.number().min(0).max(1)
208
- });
209
184
  var Persistence = z.object({
210
185
  commitsConsidered: z.number().int().nonnegative(),
211
186
  filesConsidered: z.number().int().nonnegative(),
@@ -247,23 +222,27 @@ var CohortContext = z.object({
247
222
  age: AgeStats.nullable(),
248
223
  taskMix: CategoryCounts.nullable()
249
224
  });
250
- var CohortMergeRatio = z.object({
251
- commitsTotal: z.number().int().nonnegative(),
252
- commitsMerged: z.number().int().nonnegative(),
253
- mergeRatio: z.number().min(0).max(1)
254
- });
255
225
  var Baseline = z.object({
256
226
  // True when the cohort includes 'unknown' commits via defaultAttribution:
257
227
  // the baseline is an assumption, not observed attribution.
258
228
  assumed: z.boolean(),
259
- mergeRatio: CohortMergeRatio,
260
229
  persistence: Persistence
261
230
  });
262
231
  var Delta = z.object({
263
- mergeRatio: z.number(),
264
232
  avgPersistenceDays: z.number(),
265
233
  medianPersistenceDays: z.number()
266
234
  });
235
+ var ModeStats = z.object({
236
+ commits: z.number().int().nonnegative(),
237
+ persistence: Persistence
238
+ });
239
+ var ByMode = z.object({
240
+ agent: ModeStats.nullable(),
241
+ assisted: ModeStats.nullable(),
242
+ autocomplete: ModeStats.nullable(),
243
+ none: ModeStats.nullable(),
244
+ unknown: ModeStats.nullable()
245
+ });
267
246
  var Metrics = z.object({
268
247
  generatedAt: z.string().datetime(),
269
248
  window: z.object({
@@ -273,7 +252,6 @@ var Metrics = z.object({
273
252
  repoPath: z.string(),
274
253
  defaultBranch: z.string(),
275
254
  attribution: Attribution,
276
- mergeRatio: MergeRatio,
277
255
  persistence: Persistence,
278
256
  // Fairness context (#29, #36): age and task mix per cohort, so consumers
279
257
  // can judge whether the AI-vs-baseline comparison is apples-to-apples.
@@ -281,6 +259,7 @@ var Metrics = z.object({
281
259
  ai: CohortContext,
282
260
  baseline: CohortContext
283
261
  }),
262
+ byMode: ByMode,
284
263
  // Null when no commit is attributed 'human' and no defaultAttribution prior
285
264
  // assigns the unknowns: AIDA does not invent a comparison cohort.
286
265
  baseline: Baseline.nullable(),
@@ -295,7 +274,7 @@ function round(value, decimals) {
295
274
  }
296
275
  function calculateMetrics(commitStream, options = {}) {
297
276
  const { defaultAttribution = "unknown", coverageThreshold = 0.7 } = options;
298
- const counts = { ai: 0, human: 0, unknown: 0 };
277
+ const counts = { ai: 0, human: 0, automated: 0, unknown: 0 };
299
278
  const modes = { none: 0, autocomplete: 0, assisted: 0, agent: 0, unknown: 0 };
300
279
  const modeEvidence = { declared: 0, inferred: 0, none: 0 };
301
280
  for (const commit of commitStream.commits) {
@@ -304,11 +283,12 @@ function calculateMetrics(commitStream, options = {}) {
304
283
  modeEvidence[commit.tags.modeEvidence]++;
305
284
  }
306
285
  const total = commitStream.commits.length;
307
- const coverage = total > 0 ? (counts.ai + counts.human) / total : 0;
286
+ const coverage = total > 0 ? (counts.ai + counts.human + counts.automated) / total : 0;
308
287
  const attribution = {
309
288
  commitsTotal: total,
310
289
  ai: counts.ai,
311
290
  human: counts.human,
291
+ automated: counts.automated,
312
292
  unknown: counts.unknown,
313
293
  coverage: round(coverage, 4),
314
294
  defaultAttribution,
@@ -317,10 +297,8 @@ function calculateMetrics(commitStream, options = {}) {
317
297
  modes,
318
298
  modeEvidence
319
299
  };
320
- const isExcluded = (commit) => commit.tags.sources.includes("manifest:excluded");
321
- const isAI = (commit) => commit.tags.attribution === "ai" || defaultAttribution === "ai" && commit.tags.attribution === "unknown" && !isExcluded(commit);
322
- const isBaseline = (commit) => commit.tags.attribution === "human" || defaultAttribution === "human" && commit.tags.attribution === "unknown" && !isExcluded(commit);
323
- const mergeRatio = calculateMergeRatio(commitStream, isAI);
300
+ const isAI = (commit) => commit.tags.attribution === "ai" || defaultAttribution === "ai" && commit.tags.attribution === "unknown";
301
+ const isBaseline = (commit) => commit.tags.attribution === "human" || defaultAttribution === "human" && commit.tags.attribution === "unknown";
324
302
  const persistence = calculatePersistence(commitStream, isAI);
325
303
  const now = /* @__PURE__ */ new Date();
326
304
  const aiCommits = commitStream.commits.filter(isAI);
@@ -335,15 +313,28 @@ function calculateMetrics(commitStream, options = {}) {
335
313
  taskMix: calculateCategoryCounts(baselineCommits)
336
314
  }
337
315
  };
316
+ const MODES = ["agent", "assisted", "autocomplete", "none", "unknown"];
317
+ const byMode = Object.fromEntries(
318
+ MODES.map((mode) => {
319
+ const isMode = (commit) => commit.tags.mode === mode && commit.tags.attribution !== "automated";
320
+ const modeCommits = commitStream.commits.filter(isMode);
321
+ if (modeCommits.length === 0) {
322
+ return [mode, null];
323
+ }
324
+ const stats = {
325
+ commits: modeCommits.length,
326
+ persistence: calculatePersistence(commitStream, isMode)
327
+ };
328
+ return [mode, stats];
329
+ })
330
+ );
338
331
  const baselineSize = baselineCommits.length;
339
332
  const baselineAssumed = defaultAttribution === "human" && counts.unknown > 0;
340
333
  const baseline = baselineSize > 0 ? {
341
334
  assumed: baselineAssumed,
342
- mergeRatio: calculateBaselineMergeRatio(commitStream, isBaseline),
343
335
  persistence: calculateBaselinePersistence(commitStream, isBaseline)
344
336
  } : null;
345
337
  const delta = baseline ? {
346
- mergeRatio: round(mergeRatio.mergeRatio - baseline.mergeRatio.mergeRatio, 4),
347
338
  avgPersistenceDays: round(persistence.avgDays - baseline.persistence.avgDays, 2),
348
339
  medianPersistenceDays: round(
349
340
  persistence.medianDays - baseline.persistence.medianDays,
@@ -355,8 +346,6 @@ function calculateMetrics(commitStream, options = {}) {
355
346
  "Persistence is file-level, not line-level.",
356
347
  "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.",
357
348
  "Persistence comparisons are only meaningful between cohorts of similar age and task mix \u2014 check the cohorts section before reading the delta.",
358
- "Merge ratio: commits from all branches checked against default branch ancestry. Squash merges may undercount unmerged commits.",
359
- "Time-windowed collection (--since) also windows the ancestry check: commits merged into the default branch before the window may appear unmerged.",
360
349
  "AI tagging uses heuristic patterns; false positives/negatives possible."
361
350
  ];
362
351
  if (baseline?.assumed) {
@@ -378,9 +367,9 @@ function calculateMetrics(commitStream, options = {}) {
378
367
  repoPath: commitStream.repoPath,
379
368
  defaultBranch: commitStream.defaultBranch,
380
369
  attribution,
381
- mergeRatio,
382
370
  persistence,
383
371
  cohorts,
372
+ byMode,
384
373
  baseline,
385
374
  delta,
386
375
  caveats
@@ -390,20 +379,18 @@ export {
390
379
  AgeStats,
391
380
  Attribution,
392
381
  Baseline,
382
+ ByMode,
393
383
  CategoryCounts,
394
384
  CohortContext,
395
- CohortMergeRatio,
396
385
  DEFAULT_PERSISTENCE_EXCLUDED_CATEGORIES,
397
386
  Delta,
398
387
  FileCategory,
399
- MergeRatio,
400
388
  Metrics,
389
+ ModeStats,
401
390
  Persistence,
402
391
  calculateAgeStats,
403
- calculateBaselineMergeRatio,
404
392
  calculateBaselinePersistence,
405
393
  calculateCategoryCounts,
406
- calculateMergeRatio,
407
394
  calculateMetrics,
408
395
  calculatePersistence,
409
396
  categorizeFile
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 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 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\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"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/cohort.ts","../src/persistence.ts","../src/schema/metrics.ts"],"sourcesContent":["import { Commit, CommitStream, formatISODate } from '@aida-dev/core';\nimport { calculateAgeStats, calculateCategoryCounts } from './cohort.js';\nimport { calculateBaselinePersistence, calculatePersistence } from './persistence.js';\nimport { Attribution, ByMode, Metrics, ModeStats } from './schema/metrics.js';\n\nexport * from './schema/metrics.js';\nexport * from './cohort.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 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 // Per-autonomy-level metrics (#25, step 2). Automated commits are\n // excluded: automation is not authored code, whatever its mode field says.\n const MODES = ['agent', 'assisted', 'autocomplete', 'none', 'unknown'] as const;\n const byMode = Object.fromEntries(\n MODES.map((mode) => {\n const isMode = (commit: Commit) =>\n commit.tags.mode === mode && commit.tags.attribution !== 'automated';\n const modeCommits = commitStream.commits.filter(isMode);\n if (modeCommits.length === 0) {\n return [mode, null];\n }\n const stats: ModeStats = {\n commits: modeCommits.length,\n persistence: calculatePersistence(commitStream, isMode),\n };\n return [mode, stats];\n })\n ) as ByMode;\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 persistence: calculateBaselinePersistence(commitStream, isBaseline),\n }\n : null;\n\n const delta = baseline\n ? {\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 '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 persistence,\n cohorts,\n byMode,\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, 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\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 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 persistence: Persistence,\n});\n\nexport const Delta = z.object({\n avgPersistenceDays: z.number(),\n medianPersistenceDays: z.number(),\n});\n\n// Per-autonomy-level metrics (#25, step 2): the durable comparison in an\n// AI-first world is between autonomy levels, not AI vs human. Automated\n// commits are excluded — automation is not authored code. Null when the\n// mode has no commits.\nexport const ModeStats = z.object({\n commits: z.number().int().nonnegative(),\n persistence: Persistence,\n});\n\nexport const ByMode = z.object({\n agent: ModeStats.nullable(),\n assisted: ModeStats.nullable(),\n autocomplete: ModeStats.nullable(),\n none: ModeStats.nullable(),\n unknown: ModeStats.nullable(),\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 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 byMode: ByMode,\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 ModeStats = z.infer<typeof ModeStats>;\nexport type ByMode = z.infer<typeof ByMode>;\nexport type Persistence = z.infer<typeof Persistence>;\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.6.0",
3
+ "version": "0.8.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",