diff --git a/.beads/interactions.jsonl b/.beads/interactions.jsonl index b772f40..867a339 100644 --- a/.beads/interactions.jsonl +++ b/.beads/interactions.jsonl @@ -7,3 +7,8 @@ {"id":"int-3e1630bd","kind":"field_change","created_at":"2026-04-07T12:38:16.042213Z","actor":"Ken Judy","issue_id":"code-quality-metrics-h22","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"lint clean, typecheck clean, 117/117 tests, 96.21% lines / 96.42% functions — all thresholds met"}} {"id":"int-cee835a3","kind":"field_change","created_at":"2026-04-07T12:38:16.543793Z","actor":"Ken Judy","issue_id":"code-quality-metrics-oga","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"All 7 implementation steps complete. Formula fixed, fields renamed, spec updated, tests green."}} {"id":"int-d7bd06be","kind":"field_change","created_at":"2026-04-07T12:41:46.049872Z","actor":"Ken Judy","issue_id":"code-quality-metrics-zx6","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Both workflow files updated: formula fixed (bounded), fields renamed to net_additions_ratio_*, thresholds updated to <0.50, display labels updated. Also cleaned up redundant sort+map in code-metrics.yml ratioStats computation."}} +{"id":"int-8c6635bc","kind":"field_change","created_at":"2026-04-07T13:28:25.173297Z","actor":"Ken Judy","issue_id":"code-quality-metrics-2hj","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"isTestFile moved to lib/metrics.js, re-exported from lib/git.js. 119/119 tests pass."}} +{"id":"int-94ca1d4f","kind":"field_change","created_at":"2026-04-07T13:33:11.382272Z","actor":"Ken Judy","issue_id":"code-quality-metrics-bul","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"code-metrics.yml refactored: inline helpers removed, require() added, CONFIG.* used for all thresholds, velocity_commits_per_day added, classifyDoraArchetype signature fixed"}} +{"id":"int-5369ba6b","kind":"field_change","created_at":"2026-04-07T13:33:11.931574Z","actor":"Ken Judy","issue_id":"code-quality-metrics-a6p","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"pr-metrics.yml refactored: inline helpers removed, require() added, isTestFile() replaces 4x regex, CONFIG.* thresholds, classifyDoraArchetype signature fixed"}} +{"id":"int-7a66b645","kind":"field_change","created_at":"2026-04-07T13:34:14.142186Z","actor":"Ken Judy","issue_id":"code-quality-metrics-ac0","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"CLAUDE.md updated: architecture section reflects shared lib/, config section notes single source of truth, 'update both places' warning removed, net additions ratio threshold updated"}} +{"id":"int-6b197fa6","kind":"field_change","created_at":"2026-04-07T13:34:39.195467Z","actor":"Ken Judy","issue_id":"code-quality-metrics-22h","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"lint clean, typecheck clean, 119/119, 96.21%/96.42% coverage — all thresholds met"}} diff --git a/.github/workflows/code-metrics.yml b/.github/workflows/code-metrics.yml index 4ae970f..5b68ae2 100644 --- a/.github/workflows/code-metrics.yml +++ b/.github/workflows/code-metrics.yml @@ -30,60 +30,10 @@ jobs: with: script: | const fs = require('fs'); - - // --------------------------------------------------------------- - // Inline helpers (mirrors local-code-metrics.js logic) - // --------------------------------------------------------------- - - const CONVENTIONAL_COMMIT_RE = /^(feat|fix|refactor|test|chore|docs|perf|ci|build|revert)(\(.+\))?:/i; - function scoreMessageQuality(message) { - if (!message) return false; - if (CONVENTIONAL_COMMIT_RE.test(message)) return true; - return message.split(/\s+/).filter(Boolean).length >= 10; - } - - function quantile(sorted, p) { - if (sorted.length === 0) return 0; - if (sorted.length === 1) return sorted[0]; - const idx = p * (sorted.length - 1); - const lo = Math.floor(idx); - const hi = Math.ceil(idx); - return sorted[lo] + (sorted[hi] - sorted[lo]) * (idx - lo); - } - - function computeStats(values) { - if (values.length === 0) return { p50: 0, p90: 0, p95: 0, mean: 0, stddev: 0 }; - const sorted = [...values].sort((a, b) => a - b); - const mean = values.reduce((s, v) => s + v, 0) / values.length; - const variance = values.reduce((s, v) => s + (v - mean) ** 2, 0) / values.length; - return { - p50: quantile(sorted, 0.5), - p90: quantile(sorted, 0.9), - p95: quantile(sorted, 0.95), - mean, - stddev: Math.sqrt(variance) - }; - } - - function computeVelocityTrend(timestamps) { - if (timestamps.length < 2) return 'stable'; - const ms = timestamps.map(t => new Date(t).getTime()).sort((a, b) => a - b); - const spanDays = (ms[ms.length - 1] - ms[0]) / 86400000 || 1; - const midMs = (ms[0] + ms[ms.length - 1]) / 2; - const halfSpan = spanDays / 2 || 1; - const firstRate = ms.filter(t => t <= midMs).length / halfSpan; - const secondRate = ms.filter(t => t > midMs).length / halfSpan; - if (secondRate > firstRate * 1.25) return 'accelerating'; - if (secondRate < firstRate * 0.75) return 'decelerating'; - return 'stable'; - } - - function classifyDoraArchetype(large, sprawling, testFirst, msgQuality) { - if (large < 20 && sprawling < 10 && testFirst > 50 && msgQuality > 60) return 'harmonious-high-achiever'; - if (sprawling > 25 && large > 30) return 'legacy-bottleneck'; - if (large > 40 || (testFirst < 30 && large > 20)) return 'foundational-challenges'; - return 'mixed-signals'; - } + // lib/ modules are available after checkout — require() resolves from $GITHUB_WORKSPACE + const { CONFIG } = require('./lib/config'); + const { computeStatistics, computeVelocity } = require('./lib/statistics'); + const { scoreMessageQuality, classifyDoraArchetype, isTestFile } = require('./lib/metrics'); // --------------------------------------------------------------- // Main collection @@ -91,7 +41,7 @@ jobs: async function collectMetrics() { const [owner, repo] = process.env.GITHUB_REPOSITORY.split('/'); - const since = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString(); + const since = new Date(Date.now() - CONFIG.ANALYSIS_DAYS * 24 * 60 * 60 * 1000).toISOString(); console.log(`Repository: ${owner}/${repo}`); console.log(`Looking for commits since: ${since}`); @@ -165,6 +115,7 @@ jobs: p90_lines_changed: 0, p95_lines_changed: 0, stddev_lines_changed: 0, + velocity_commits_per_day: 0, velocity_trend: "stable", net_additions_ratio_median: 0, net_additions_ratio_p90: 0, @@ -178,9 +129,9 @@ jobs: } const metrics = []; - console.log(`Processing ${Math.min(uniqueCommits.length, 50)} commits...`); + console.log(`Processing ${Math.min(uniqueCommits.length, CONFIG.MAX_COMMITS)} commits...`); - for (const commit of uniqueCommits.slice(0, 50)) { + for (const commit of uniqueCommits.slice(0, CONFIG.MAX_COMMITS)) { try { const detail = await github.rest.repos.getCommit({ owner, @@ -188,13 +139,8 @@ jobs: ref: commit.sha }); - const testFiles = detail.data.files?.filter(f => - /\.(test|spec)\.|Tests?\.cs$|Test\.cs$|__tests__|\/tests?\//i.test(f.filename) - ) || []; - - const prodFiles = detail.data.files?.filter(f => - !/\.(test|spec)\.|Tests?\.cs$|Test\.cs$|__tests__|\/tests?\//i.test(f.filename) - ) || []; + const testFiles = detail.data.files?.filter(f => isTestFile(f.filename)) || []; + const prodFiles = detail.data.files?.filter(f => !isTestFile(f.filename)) || []; const message = commit.commit.message.split('\n')[0]; @@ -212,8 +158,8 @@ jobs: test_first_indicator: testFiles.length > 0 && prodFiles.length > 0, prod_additions: prodFiles.reduce((sum, f) => sum + f.additions, 0), prod_deletions: prodFiles.reduce((sum, f) => sum + f.deletions, 0), - large_commit: (prodFiles.reduce((sum, f) => sum + f.additions + f.deletions, 0)) > 100, - sprawling_commit: (detail.data.files?.length || 0) > 5, + large_commit: (prodFiles.reduce((sum, f) => sum + f.additions + f.deletions, 0)) > CONFIG.LARGE_COMMIT_THRESHOLD, + sprawling_commit: (detail.data.files?.length || 0) > CONFIG.SPRAWLING_COMMIT_THRESHOLD, message_quality: scoreMessageQuality(message), commit_type: "feature_branch" }); @@ -228,15 +174,16 @@ jobs: fs.writeFileSync('commit_metrics.json', JSON.stringify(metrics, null, 2)); // Statistical distributions + const timestamps = metrics.map(m => new Date(m.date).getTime()); const lineSizes = metrics.map(m => m.total_additions + m.total_deletions); - const lineStats = computeStats(lineSizes); - const fileStats = computeStats(metrics.map(m => m.files_changed)); + const lineStats = computeStatistics(lineSizes, timestamps); + const fileStats = computeStatistics(metrics.map(m => m.files_changed), timestamps); const ratios = metrics.map(m => { const t = m.total_additions + m.total_deletions; return t === 0 ? 0 : (m.total_additions - m.total_deletions) / t; }); - const ratioStats = computeStats(ratios); - const velocityTrend = computeVelocityTrend(metrics.map(m => m.date)); + const ratioStats = computeStatistics(ratios, timestamps); + const velocity = computeVelocity(metrics.map(m => m.date)); const qualityCount = metrics.filter(m => m.message_quality).length; const msgQualityPct = metrics.length > 0 @@ -264,11 +211,12 @@ jobs: stddev_lines_changed: Math.round(lineStats.stddev), p50_files_changed: Math.round(fileStats.p50), p90_files_changed: Math.round(fileStats.p90), - velocity_trend: velocityTrend, + velocity_commits_per_day: velocity.commits_per_day, + velocity_trend: velocity.trend, net_additions_ratio_median: parseFloat(ratioStats.p50.toFixed(2)), net_additions_ratio_p90: parseFloat(ratioStats.p90.toFixed(2)), message_quality_pct: msgQualityPct, - dora_archetype: classifyDoraArchetype(largePct, sprawlingPct, testFirstPct, parseFloat(msgQualityPct)), + dora_archetype: classifyDoraArchetype({ large_commits_pct: largePct.toFixed(2), sprawling_commits_pct: sprawlingPct.toFixed(2), test_first_pct: testFirstPct.toFixed(2), message_quality_pct: msgQualityPct }), note: "Feature branches only - main/master excluded. Large commit threshold applied to production code only." }; diff --git a/.github/workflows/pr-metrics.yml b/.github/workflows/pr-metrics.yml index 489cc79..36bf056 100644 --- a/.github/workflows/pr-metrics.yml +++ b/.github/workflows/pr-metrics.yml @@ -17,23 +17,9 @@ jobs: script: | const pr = context.payload.pull_request; - // --------------------------------------------------------------- - // Inline helpers (mirrors local-code-metrics.js logic) - // --------------------------------------------------------------- - - const CONVENTIONAL_COMMIT_RE = /^(feat|fix|refactor|test|chore|docs|perf|ci|build|revert)(\(.+\))?:/i; - function scoreMessageQuality(message) { - if (!message) return false; - if (CONVENTIONAL_COMMIT_RE.test(message)) return true; - return message.split(/\s+/).filter(Boolean).length >= 10; - } - - function classifyDoraArchetype(large, sprawling, testFirst, msgQuality) { - if (large < 20 && sprawling < 10 && testFirst > 50 && msgQuality > 60) return 'harmonious-high-achiever'; - if (sprawling > 25 && large > 30) return 'legacy-bottleneck'; - if (large > 40 || (testFirst < 30 && large > 20)) return 'foundational-challenges'; - return 'mixed-signals'; - } + // lib/ modules are available after checkout — require() resolves from $GITHUB_WORKSPACE + const { CONFIG } = require('./lib/config'); + const { scoreMessageQuality, classifyDoraArchetype, isTestFile } = require('./lib/metrics'); // --------------------------------------------------------------- // Fetch PR data @@ -55,13 +41,8 @@ jobs: console.log(`Analyzing ${commits.length} commits and ${files.length} files in PR`); - const testFiles = files.filter(f => - /\.(test|spec)\.|Tests?\.cs$|Test\.cs$|__tests__|\/tests?\//i.test(f.filename) - ); - - const prodFiles = files.filter(f => - !/\.(test|spec)\.|Tests?\.cs$|Test\.cs$|__tests__|\/tests?\//i.test(f.filename) - ); + const testFiles = files.filter(f => isTestFile(f.filename)); + const prodFiles = files.filter(f => !isTestFile(f.filename)); // --------------------------------------------------------------- // Per-commit analysis @@ -76,13 +57,8 @@ jobs: ref: commit.sha }); - const commitTestFiles = detail.data.files?.filter(f => - /\.(test|spec)\.|Tests?\.cs$|Test\.cs$|__tests__|\/tests?\//i.test(f.filename) - ) || []; - - const commitProdFiles = detail.data.files?.filter(f => - !/\.(test|spec)\.|Tests?\.cs$|Test\.cs$|__tests__|\/tests?\//i.test(f.filename) - ) || []; + const commitTestFiles = detail.data.files?.filter(f => isTestFile(f.filename)) || []; + const commitProdFiles = detail.data.files?.filter(f => !isTestFile(f.filename)) || []; const prodChanges = commitProdFiles.reduce((sum, f) => sum + f.additions + f.deletions, 0); const testChanges = commitTestFiles.reduce((sum, f) => sum + f.additions + f.deletions, 0); @@ -102,8 +78,8 @@ jobs: prod_changes: prodChanges, test_changes: testChanges, total_changes: detail.data.stats.additions + detail.data.stats.deletions, - large_commit: prodChanges > 100, - sprawling_commit: (detail.data.files?.length || 0) > 5, + large_commit: prodChanges > CONFIG.LARGE_COMMIT_THRESHOLD, + sprawling_commit: (detail.data.files?.length || 0) > CONFIG.SPRAWLING_COMMIT_THRESHOLD, test_first_indicator: commitTestFiles.length > 0 && commitProdFiles.length > 0, test_only: commitTestFiles.length > 0 && commitProdFiles.length === 0, prod_only: commitProdFiles.length > 0 && commitTestFiles.length === 0, @@ -274,7 +250,7 @@ jobs: // --------------------------------------------------------------- const archetype = commitMetrics.length > 0 - ? classifyDoraArchetype(largePct, sprawlingPct, testFirstPct, msgQualityPct) + ? classifyDoraArchetype({ large_commits_pct: largePct.toFixed(2), sprawling_commits_pct: sprawlingPct.toFixed(2), test_first_pct: testFirstPct.toFixed(2), message_quality_pct: msgQualityPct.toFixed(2) }) : 'mixed-signals'; const archetypeDescriptions = { diff --git a/AGENTS.md b/AGENTS.md index 35d27f5..224e831 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -132,6 +132,21 @@ npm run lint # lint must be clean before moving to next step When introducing new tooling (new lint rules, type checking, config changes), run the gate immediately after setup — before writing any tests or code — to catch configuration gaps early. +**When modifying `.github/workflows/` or `lib/`**, run a workflow smoke test: + +```bash +gh workflow run code-metrics.yml --ref +gh run watch # wait for completion +gh run download --dir /tmp/smoke && cat /tmp/smoke/*/metrics_summary.json | python3 -m json.tool +``` + +Verify: +- The artifact contains `velocity_commits_per_day` (numeric, not absent) +- No `Cannot find module` errors in the log +- Confirm the run's commit SHA matches your branch HEAD: `gh run view ` + +A green run ≠ new code ran. The artifact is the proof. + ## Landing the Plane (Session Completion) **When ending a work session**, you MUST complete ALL steps below. Work is NOT complete until `git push` succeeds. diff --git a/CLAUDE.md b/CLAUDE.md index 2e35596..e22804e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -40,20 +40,20 @@ npm install # triggers `prepare`, which sets core.hooksPath to .githooks ## Architecture -Three public components with no shared code between them: +Three public components sharing pure-computation logic via `lib/`: 1. **`local-code-metrics.js`**: Standalone Node.js script (requires Node ≥18). Orchestration entry point that delegates to focused modules in `lib/`. Reads local git history via shell commands, classifies files as test vs. production, computes metrics, writes `local_commit_metrics.json` + `local_metrics_summary.json` + (optionally) `local_claude_analysis.json`, and prints a console report with insights. - The `lib/` directory contains the internal modules (not shared with workflows): - - `lib/config.js` — CONFIG object; single source of truth for all thresholds - - `lib/git.js` — git shell commands, log parsing, per-commit analysis, diff extraction - - `lib/statistics.js` — statistical distributions (p50/p90/p95/stddev), velocity and trend - - `lib/metrics.js` — message quality scoring, DORA archetype classification, insights generation - - `lib/claude.js` — Anthropic client setup, commit pre-filtering, diff-level API analysis + The `lib/` directory contains the internal modules: + - `lib/config.js` — CONFIG object; single source of truth for all thresholds (**shared with workflows**) + - `lib/statistics.js` — statistical distributions (p50/p90/p95/stddev), velocity and trend (**shared with workflows**) + - `lib/metrics.js` — message quality scoring, DORA archetype classification, test file detection, insights generation (**shared with workflows**) + - `lib/git.js` — git shell commands, log parsing, per-commit analysis, diff extraction (local only — workflows use REST API) + - `lib/claude.js` — Anthropic client setup, commit pre-filtering, diff-level API analysis (local only — workflows use GitHub-managed auth) -2. **`.github/workflows/code-metrics.yml`**: Weekly GitHub Actions workflow. Uses the GitHub API to analyze feature branches from the past 30 days. Outputs a JSON artifact and creates a GitHub issue with the summary. +2. **`.github/workflows/code-metrics.yml`**: Weekly GitHub Actions workflow. Uses the GitHub API to analyze feature branches from the past 30 days. Requires `lib/config.js`, `lib/statistics.js`, and `lib/metrics.js` via `require()`. Outputs a JSON artifact and creates a GitHub issue with the summary. -3. **`.github/workflows/pr-metrics.yml`**: Per-PR GitHub Actions workflow. Posts a detailed comment on each PR with commit-by-commit analysis, test adequacy, and development pattern detection. +3. **`.github/workflows/pr-metrics.yml`**: Per-PR GitHub Actions workflow. Requires `lib/config.js` and `lib/metrics.js` via `require()`. Posts a detailed comment on each PR with commit-by-commit analysis, test adequacy, and development pattern detection. ## Key Metrics and Thresholds @@ -64,7 +64,7 @@ Three public components with no shared code between them: | Test-to-production ratio | 0.5–2.0:1 | | Avg files changed per commit | <5 | | Commit message quality % | >60% | -| Additions-to-deletions ratio (median) | <3.0 | +| Net additions ratio (median) | <0.50 | Statistical distributions (p50/p90/p95/stddev) are computed for lines changed and files changed. Commit velocity trend and DORA team archetype are included in the summary. @@ -91,7 +91,7 @@ The script degrades gracefully when the key is absent. No SDK install is require ## Configuration -Thresholds are configured in the `CONFIG` object in `lib/config.js`, which is the single source of truth for the local script. Key values: +Thresholds are configured in the `CONFIG` object in `lib/config.js`, which is the single source of truth for **all three components**. The GitHub Actions workflows `require('./lib/config')` directly — changing a value in `lib/config.js` propagates automatically to the local script and both workflows. No manual synchronization needed. Key values: | Key | Default | Description | |-----|---------|-------------| @@ -102,9 +102,7 @@ Thresholds are configured in the `CONFIG` object in `lib/config.js`, which is th | `AI_DIFF_MAX_CHARS` | 4000 | Diff truncation limit for Claude API calls | | `AI_RISK_ADDITIONS_RATIO` | 3 | Additions/deletions multiplier for Claude pre-filter | -The GitHub workflows have equivalent values hard-coded in their shell/jq logic. When adjusting thresholds, update both places. - -Test file detection uses patterns for JS, Python, Go, Java, and C#. Extend `TEST_FILE_PATTERNS` in `lib/config.js` or the equivalent grep patterns in the workflows for other languages. +Test file detection uses patterns for JS, Python, Go, Java, and C#. Extend `TEST_FILE_PATTERNS` in `lib/config.js` — the change propagates automatically to all three components. ## Workflow Permissions diff --git a/README.md b/README.md index 6053ae2..a8c0c31 100644 --- a/README.md +++ b/README.md @@ -29,19 +29,48 @@ Research shows that AI coding tools can lead to increased batch sizes, reduced r ## Quick Start ### Option 1: GitHub Actions (Recommended) + +**Step 1 — Copy the workflow files and shared lib modules into your repository** +```bash +mkdir -p .github/workflows lib +curl -o .github/workflows/code-metrics.yml \ + https://raw.githubusercontent.com/stride-nyc/code-quality-metrics/main/.github/workflows/code-metrics.yml +curl -o .github/workflows/pr-metrics.yml \ + https://raw.githubusercontent.com/stride-nyc/code-quality-metrics/main/.github/workflows/pr-metrics.yml +curl -o lib/config.js \ + https://raw.githubusercontent.com/stride-nyc/code-quality-metrics/main/lib/config.js +curl -o lib/statistics.js \ + https://raw.githubusercontent.com/stride-nyc/code-quality-metrics/main/lib/statistics.js +curl -o lib/metrics.js \ + https://raw.githubusercontent.com/stride-nyc/code-quality-metrics/main/lib/metrics.js +``` + +> The workflows use `require('./lib/config')`, `require('./lib/statistics')`, and `require('./lib/metrics')` at runtime. These three files must be committed alongside the workflow files. + +**Step 2 — Create the required issue labels** (used by the weekly report) ```bash -# 1. Copy workflows to your repository -mkdir -p .github/workflows -# Copy .github/workflows/code-metrics.yml and pr-metrics.yml from this repo +gh label create metrics --color 0075ca --description "Code metrics reports" +gh label create automated --color e4e669 --description "Automated workflow output" +``` -# 2. Ensure feature branches aren't auto-deleted -# Go to repo Settings > General > Pull Requests -# Uncheck "Automatically delete head branches" +**Step 3 — Ensure feature branches are not auto-deleted** -# 3. Run manually or wait for weekly schedule +Go to your repo **Settings → General → Pull Requests** and uncheck +"Automatically delete head branches" — the weekly workflow needs branches to exist to analyze them. + +**Step 4 — Set workflow permissions** + +Go to **Settings → Actions → General → Workflow permissions** and select +"Read and write permissions" (required for creating issues and PR comments). + +**Step 5 — Trigger the first run** +```bash gh workflow run code-metrics.yml +gh run watch # follow the run live ``` +The PR analysis workflow (`pr-metrics.yml`) triggers automatically on every new or updated pull request — no manual step needed. + ### Option 2: Local Analysis ```bash # 1. Clone and run @@ -60,7 +89,7 @@ node local-code-metrics.js | **Sprawling Commit %** | <10% | Identifies scattered changes across files | | **Test-First Discipline** | >50% | Monitors TDD practices with AI tools | | **Message Quality %** | >60% | Conventional commits or descriptive messages | -| **Additions/Deletions Ratio (median)** | <3.0 | Flags batch-acceptance pattern | +| **Net Additions Ratio (median)** | <0.50 | Flags batch-acceptance pattern (bounded 0–1: 1.0 = entirely net-new code) | | **Avg Files Changed** | <5 | Measures development granularity | ## Real-World Example @@ -114,7 +143,7 @@ Large commits: <20% Sprawling commits: <10% Test-first discipline: >50% Message quality: >60% -Additions ratio (median): <3.0 +Net additions ratio (median): <0.33 ``` ### Warning Signs @@ -122,7 +151,7 @@ Additions ratio (median): <3.0 Large commits: 20-40% Sprawling commits: 10-25% Test-first discipline: 30-50% -Additions ratio (median): 3.0-6.0 +Net additions ratio (median): 0.33-0.50 ``` ### Critical Issues @@ -130,7 +159,7 @@ Additions ratio (median): 3.0-6.0 Large commits: >40% Sprawling commits: >25% Test-first discipline: <30% -Additions ratio (median): >6.0 +Net additions ratio (median): >0.50 ``` ## DORA Archetype Classification diff --git a/__tests__/isTestFile.test.js b/__tests__/isTestFile.test.js index c8eb338..729d527 100644 --- a/__tests__/isTestFile.test.js +++ b/__tests__/isTestFile.test.js @@ -2,6 +2,20 @@ const { isTestFile } = require('../local-code-metrics'); +// isTestFile must also be available directly from lib/metrics (for workflow require() calls) +describe('isTestFile available from lib/metrics', () => { + test('isTestFile is exported from lib/metrics.js', () => { + const { isTestFile: isTestFileFromMetrics } = require('../lib/metrics'); + expect(typeof isTestFileFromMetrics).toBe('function'); + }); + + test('isTestFile from lib/metrics behaves identically to the one from local-code-metrics', () => { + const { isTestFile: isTestFileFromMetrics } = require('../lib/metrics'); + expect(isTestFileFromMetrics('src/app.test.js')).toBe(true); + expect(isTestFileFromMetrics('src/app.js')).toBe(false); + }); +}); + describe('isTestFile', () => { // --- degenerate case --- test('returns false for a plain production source file', () => { diff --git a/lib/git.js b/lib/git.js index f3c59f7..c329a8b 100644 --- a/lib/git.js +++ b/lib/git.js @@ -3,6 +3,7 @@ const { execSync } = require('child_process'); const { CONFIG } = require('./config'); +const { isTestFile } = require('./metrics'); /** * Execute Git command with error handling @@ -50,15 +51,6 @@ function parseGitLog(logOutput) { return commits; } -/** - * Check if a filename matches test file patterns - * @param {string} filename - * @returns {boolean} - */ -function isTestFile(filename) { - return CONFIG.TEST_FILE_PATTERNS.some(pattern => pattern.test(filename)); -} - /** * Analyze a single commit for AI drift indicators * @param {string} sha diff --git a/lib/metrics.js b/lib/metrics.js index 1b1b3c6..70b3a10 100644 --- a/lib/metrics.js +++ b/lib/metrics.js @@ -3,6 +3,17 @@ const { CONFIG } = require('./config'); +/** + * Check if a filename matches test file patterns. + * Defined here (not lib/git.js) so GitHub Actions workflows can require('./lib/metrics') + * without pulling in child_process-dependent git functions. + * @param {string} filename + * @returns {boolean} + */ +function isTestFile(filename) { + return CONFIG.TEST_FILE_PATTERNS.some(pattern => pattern.test(filename)); +} + /** @type {RegExp} */ const CONVENTIONAL_COMMIT_RE = /^(feat|fix|refactor|test|chore|docs|perf|ci|build|revert)(\(.+\))?:/i; @@ -95,4 +106,4 @@ function generateInsights(summary, metrics) { return { insights, warnings, recommendations }; } -module.exports = { scoreMessageQuality, classifyDoraArchetype, generateInsights }; +module.exports = { isTestFile, scoreMessageQuality, classifyDoraArchetype, generateInsights };