@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.d.ts +1572 -116
- package/dist/index.js +39 -52
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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
|
|
321
|
-
const
|
|
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.
|
|
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.
|
|
14
|
+
"@aida-dev/core": "0.11.0"
|
|
15
15
|
},
|
|
16
16
|
"devDependencies": {
|
|
17
17
|
"tsup": "^8.0.0",
|