From c33afd666431dd84e7a714874c1ce47ca6b69fd6 Mon Sep 17 00:00:00 2001 From: Ken Judy Date: Tue, 9 Jun 2026 11:15:05 -0400 Subject: [PATCH 1/9] refactor: add duplicate detection config keys to lib/config.js Adds DUPLICATE_MIN_LINES (5), DUPLICATE_MIN_TOKENS (50), DUPLICATE_IGNORE_PATTERNS ([]), and DUPLICATE_SCAN_PATHS ([]) to CONFIG. All four keys are configurable so teams using this on other repos can tune thresholds without touching library code. Co-Authored-By: Claude Sonnet 4.6 --- .beads/issues.jsonl | 78 ++++++++++++++++++++++++++++++++++++++++ __tests__/config.test.js | 21 +++++++++++ lib/config.js | 6 ++++ 3 files changed, 105 insertions(+) create mode 100644 .beads/issues.jsonl create mode 100644 __tests__/config.test.js diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl new file mode 100644 index 0000000..1dbcace --- /dev/null +++ b/.beads/issues.jsonl @@ -0,0 +1,78 @@ +{"id":"code-quality-metrics-20o","title":"feat: implement runDuplicateCheck in lib/duplicate.js","description":"Create lib/duplicate.js with runDuplicateCheck(filePaths) function. Runs jscpd via child_process using CONFIG values, reads the JSON report it writes to CONFIG.DUPLICATE_OUTPUT_DIR (a temp path), parses it, and returns a structured array.\n\nRETURN SHAPE (mirrors jscpd JSON output directly):\n Array\u003c{\n firstFile: { name: string, start: number, end: number },\n secondFile: { name: string, start: number, end: number },\n lines: number,\n tokens: number\n }\u003e\n\nJSCPD INVOCATION:\n jscpd --min-lines CONFIG.DUPLICATE_MIN_LINES --min-tokens CONFIG.DUPLICATE_MIN_TOKENS\n --ignore-pattern CONFIG.DUPLICATE_IGNORE_PATTERNS\n --reporters json --output CONFIG.DUPLICATE_OUTPUT_DIR\n [filePaths...]\n\nMOCK STRATEGY: mock execSync (child_process) for the jscpd call; mock fs.readFileSync for reading the output JSON; mock fs.existsSync to control whether the output file exists.\n\nBEHAVIORAL TEST LIST (one RED test before each GREEN):\n1. 'runDuplicateCheck returns empty array when filePaths is empty without calling jscpd'\n Predicted failure: received undefined or thrown error\n2. 'runDuplicateCheck returns empty array when jscpd output file does not exist'\n Predicted failure: received undefined or fs.readFileSync called unexpectedly\n3. 'runDuplicateCheck parses jscpd JSON and returns firstFile/secondFile/lines/tokens for each duplicate'\n Predicted failure: received [] (not yet parsing)\n4. 'runDuplicateCheck returns empty array when jscpd exits non-zero'\n Predicted failure: thrown error propagated\n\nDefinition of done: all 4 tests green, no stubs, exports runDuplicateCheck from lib/duplicate.js.","status":"open","priority":1,"issue_type":"task","owner":"kjudy@stride.build","created_at":"2026-06-09T14:20:40Z","created_by":"Ken Judy","updated_at":"2026-06-09T14:58:31Z","dependencies":[{"issue_id":"code-quality-metrics-20o","depends_on_id":"code-quality-metrics-een","type":"blocks","created_at":"2026-06-09T10:20:50Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":1,"dependent_count":2,"comment_count":0} +{"id":"code-quality-metrics-5o3","title":"feat: integrate jscpd findings into pr-metrics.yml PR comment","description":"Require lib/duplicate.js in pr-metrics.yml. Run runDuplicateCheck against the already-computed prodFiles array (map to filenames). Add a Duplication section to the PR comment when findings exist; omit the section silently when findings array is empty.\n\nPR COMMENT SECTION FORMAT (exact strings matter for CHECK):\n ## Duplicate Code\n Found N duplicate block(s) across the changed files:\n - `path/to/file.js:10-25` duplicates `path/to/other.js:5-20` (15 lines, 120 tokens)\n (one bullet per finding, capped at 10 — add 'and N more...' if truncated)\n\nWhen no findings: section is omitted entirely (no 'No duplicates found' line).\n\nVERIFICATION (no unit tests for YAML): grep audit confirms\n - require('./lib/duplicate') appears in the script block\n - runDuplicateCheck is called with prodFiles.map(f =\u003e f.filename)\n - '## Duplicate Code' header string is present\n - commentParts.filter(Boolean) already handles optional sections (no extra guard needed)\n\nDefinition of done: grep audit passes, PR comment renders section only when findings exist.","status":"open","priority":1,"issue_type":"task","owner":"kjudy@stride.build","created_at":"2026-06-09T14:20:40Z","created_by":"Ken Judy","updated_at":"2026-06-09T14:58:55Z","dependencies":[{"issue_id":"code-quality-metrics-5o3","depends_on_id":"code-quality-metrics-20o","type":"blocks","created_at":"2026-06-09T10:20:50Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} +{"id":"code-quality-metrics-729","title":"feat: integrate semantic findings into pr-metrics.yml","description":"Extend pr-metrics.yml Duplication section to include Layer 2 semantic findings when ANTHROPIC_API_KEY is present. Degrade gracefully when key is absent — consistent with local-code-metrics.js pattern.\n\nLAYER INDICATOR STRINGS (exact, appended to Duplication section footer):\n When both layers ran:\n '_Layer 1 (static) and Layer 2 (semantic) ran._'\n When Layer 1 only (no API key):\n '_Layer 1 (static) ran. Set ANTHROPIC_API_KEY to enable semantic analysis._'\n\nSEMANTIC FINDINGS FORMAT (appended after static findings list):\n ### Semantic Similarity\n - `file1.js` / `file2.js`: similarity description (confidence: high)\n\nWhen semantic layer returns no findings: omit the Semantic Similarity subsection, still show layer indicator.\n\nVERIFICATION: grep audit confirms both indicator strings are present in the YAML; check conditional on ANTHROPIC_API_KEY mirrors existing pattern in the script block.\n\nDefinition of done: grep audit passes, both indicator strings are present, Layer 2 block is guarded by API key check.","status":"open","priority":1,"issue_type":"task","owner":"kjudy@stride.build","created_at":"2026-06-09T14:20:40Z","created_by":"Ken Judy","updated_at":"2026-06-09T14:59:30Z","dependencies":[{"issue_id":"code-quality-metrics-729","depends_on_id":"code-quality-metrics-5o3","type":"blocks","created_at":"2026-06-09T10:20:50Z","created_by":"Ken Judy","metadata":"{}"},{"issue_id":"code-quality-metrics-729","depends_on_id":"code-quality-metrics-790","type":"blocks","created_at":"2026-06-09T10:20:50Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":2,"dependent_count":1,"comment_count":0} +{"id":"code-quality-metrics-790","title":"feat: add semantic duplicate detection via lib/claude.js","description":"Add analyzeDuplicatesWithClaude(client, filePaths, staticFindings) to lib/claude.js (same file as existing analyzeCommitWithClaude — consistent module boundary, no new file).\n\nRETURN SCHEMA:\n Array\u003c{\n file1: string, // relative path\n file2: string, // relative path\n similarity: string, // one-sentence description of what is similar\n concern: string, // why this matters (e.g. 'same error-handling pattern repeated')\n confidence: 'high' | 'medium' | 'low'\n }\u003e\n\nSYSTEM PROMPT INTENT: Given a set of changed files and their neighbors, identify semantically similar functions or logic blocks that suggest copy-paste or AI-generated duplication — even when variable names or formatting differ. Static analysis has already flagged token-level matches (passed as staticFindings); the agent focuses on structural/conceptual similarity that jscpd would miss.\n\nBEHAVIORAL TEST LIST:\n1. 'analyzeDuplicatesWithClaude returns empty array when client is null'\n Predicted failure: throws TypeError (null client not guarded)\n2. 'analyzeDuplicatesWithClaude constructs prompt including file contents and staticFindings count'\n Predicted failure: prompt does not mention staticFindings (not yet wired)\n3. 'analyzeDuplicatesWithClaude parses structured response into file1/file2/similarity/concern/confidence'\n Predicted failure: returns raw string instead of array\n4. 'analyzeDuplicatesWithClaude returns empty array when API response is malformed'\n Predicted failure: throws JSON parse error\n\nMOCK STRATEGY: mock Anthropic client.messages.create; return fixture JSON matching return schema.\n\nDefinition of done: all 4 tests green, function exported from lib/claude.js.","status":"open","priority":1,"issue_type":"task","owner":"kjudy@stride.build","created_at":"2026-06-09T14:20:40Z","created_by":"Ken Judy","updated_at":"2026-06-09T14:59:17Z","dependencies":[{"issue_id":"code-quality-metrics-790","depends_on_id":"code-quality-metrics-uhs","type":"blocks","created_at":"2026-06-09T10:20:50Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} +{"id":"code-quality-metrics-een","title":"refactor: add duplicate detection config keys to lib/config.js","description":"Add four keys to the CONFIG object in lib/config.js:\n DUPLICATE_MIN_LINES: 5 (number)\n DUPLICATE_MIN_TOKENS: 50 (number)\n DUPLICATE_IGNORE_PATTERNS: [] (array of glob strings — caller populates for their repo; TEST_FILE_PATTERNS provides the right starting reference but is not hard-coded here)\n DUPLICATE_SCAN_PATHS: [] (array of strings — empty means scan all provided file paths)\n\nNo behavioral change to any existing function.\n\nBEHAVIORAL TEST LIST (name + predicted failure):\n1. 'CONFIG.DUPLICATE_MIN_LINES defaults to 5'\n Predicted failure: received undefined (key missing)\n2. 'CONFIG.DUPLICATE_MIN_TOKENS defaults to 50'\n Predicted failure: received undefined (key missing)\n3. 'CONFIG.DUPLICATE_IGNORE_PATTERNS is an array'\n Predicted failure: received undefined (key missing)\n4. 'CONFIG.DUPLICATE_SCAN_PATHS is an array'\n Predicted failure: received undefined (key missing)\n\nDefinition of done: all 4 tests green; npm test passes with no threshold changes; no other file modified.","status":"open","priority":1,"issue_type":"task","owner":"kjudy@stride.build","created_at":"2026-06-09T14:20:40Z","created_by":"Ken Judy","updated_at":"2026-06-09T15:00:05Z","dependency_count":0,"dependent_count":2,"comment_count":0} +{"id":"code-quality-metrics-uhs","title":"feat: implement resolveModuleNeighbors in lib/duplicate.js","description":"Add resolveModuleNeighbors(changedFilePaths) to lib/duplicate.js. Parses require()/import statements in each changed file using regex, resolves relative paths to absolute using path.resolve(path.dirname(sourceFile), importPath), appends .js if no extension, returns the union of changed files plus their direct (not transitive) local imports.\n\nEDGE CASE DECISIONS (made now, not during implementation):\n- Non-JS files (e.g. .yml, .json, .md): include in the returned set as-is, skip import parsing\n- Files that do not exist on disk (deleted in PR): skip with fs.existsSync check, do not throw\n- External/node_modules imports (no leading './'): ignore, do not resolve\n\nBEHAVIORAL TEST LIST:\n1. 'resolveModuleNeighbors returns changed files plus resolved local imports'\n Predicted failure: returns only the input files (import parsing not yet implemented)\n2. 'resolveModuleNeighbors skips import resolution for non-JS files'\n Predicted failure: throws or returns unexpected paths for .yml input\n3. 'resolveModuleNeighbors skips files that do not exist on disk'\n Predicted failure: throws ENOENT or returns broken paths\n4. 'resolveModuleNeighbors ignores external/node_modules imports'\n Predicted failure: includes node_modules paths or throws on resolution\n\nMOCK STRATEGY: mock fs.readFileSync for file contents; mock fs.existsSync for on-disk check.\n\nDefinition of done: all 4 tests green, function exported from lib/duplicate.js alongside runDuplicateCheck.","status":"open","priority":1,"issue_type":"task","owner":"kjudy@stride.build","created_at":"2026-06-09T14:20:40Z","created_by":"Ken Judy","updated_at":"2026-06-09T14:59:06Z","dependencies":[{"issue_id":"code-quality-metrics-uhs","depends_on_id":"code-quality-metrics-20o","type":"blocks","created_at":"2026-06-09T10:20:50Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} +{"id":"code-quality-metrics-aat","title":"Feature: Two-layer duplicate code detection in PR check (#42)","description":"Add jscpd static detection (Layer 1, always-on) and agent-based semantic detection (Layer 2, conditional on ANTHROPIC_API_KEY) to pr-metrics.yml. All thresholds and ignore patterns live in lib/config.js as documented configurable parameters so the toolkit works across repos with different test conventions and directory structures.","notes":"Analysis: lib/config.js is single source of truth for all thresholds. lib/duplicate.js follows PR #33 pattern (require() in workflow). jscpd v5 uses --ignore-pattern flag, JSON output via --reporters json. Baseline: 0 duplicates in this repo production code at defaults. Other repos need to override DUPLICATE_IGNORE_PATTERNS. Layer 2 conditional on ANTHROPIC_API_KEY. Deliver Layer 1 first.","status":"open","priority":1,"issue_type":"epic","owner":"kjudy@stride.build","created_at":"2026-06-09T14:19:59Z","created_by":"Ken Judy","updated_at":"2026-06-09T14:20:18Z","dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"code-quality-metrics-141","title":"Update churn distribution in local-code-metrics.js","description":"TDD: update collectLocalMetrics.test.js first to expect churn_intensity_median and churn_intensity_p90 instead of additions_ratio_median and additions_ratio_p90, then implement. Replace local-code-metrics.js:181 ratio array with: metrics.map(m =\u003e Math.min(m.total_additions, m.total_deletions) / Math.max(m.total_additions + m.total_deletions, 1)). Rename summary fields from additions_ratio_median/additions_ratio_p90 to churn_intensity_median/churn_intensity_p90.","status":"closed","priority":1,"issue_type":"task","assignee":"Ken Judy","owner":"kjudy@stride.build","created_at":"2026-06-08T20:36:14Z","created_by":"Ken Judy","updated_at":"2026-06-08T20:44:27Z","started_at":"2026-06-08T20:43:05Z","closed_at":"2026-06-08T20:44:27Z","close_reason":"Closed","dependencies":[{"issue_id":"code-quality-metrics-141","depends_on_id":"code-quality-metrics-4ck","type":"blocks","created_at":"2026-06-08T16:36:27Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":1,"dependent_count":3,"comment_count":0} +{"id":"code-quality-metrics-4ck","title":"Replace change_ratio with churn_intensity in lib/git.js","description":"TDD: write failing tests first, then implement. Replace the string field change_ratio (which emits 'inf' for zero-deletion commits) with a numeric churn_intensity field using the formula: min(additions, deletions) / max(additions + deletions, 1). This is bounded [0.0, 0.5] and is always a number. Zero means pure addition or pure deletion; 0.5 means equal adds and deletes (maximum rework). Update __tests__/analyzeCommit.test.js: replace the two tests that assert change_ratio='inf' and change_ratio='2.00' with tests for churn_intensity. Add cases: no-deletions yields 0.0; equal adds/deletes yields 0.5; typical 10 adds / 5 deletes yields 0.333.","status":"closed","priority":1,"issue_type":"task","assignee":"Ken Judy","owner":"kjudy@stride.build","created_at":"2026-06-08T20:36:14Z","created_by":"Ken Judy","updated_at":"2026-06-08T20:42:20Z","started_at":"2026-06-08T20:40:00Z","closed_at":"2026-06-08T20:42:20Z","close_reason":"Closed","dependencies":[{"issue_id":"code-quality-metrics-4ck","depends_on_id":"code-quality-metrics-di7","type":"blocks","created_at":"2026-06-08T16:36:27Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} +{"id":"code-quality-metrics-di7","title":"Fix unbounded churn ratio for statistically valid p50/p90","description":"The additions/deletions ratio (additions / max(deletions, 1)) is unbounded. A commit of entirely new files yields ratio = additions/1, making p50/p90 meaningless (observed p90=446x). Replace with churn_intensity = min(additions, deletions) / max(additions + deletions, 1), which is bounded [0, 0.5] and measures rework fraction directly. This is more important than backwards compatibility with existing field names.","status":"closed","priority":1,"issue_type":"feature","assignee":"Ken Judy","owner":"kjudy@stride.build","created_at":"2026-06-08T20:35:53Z","created_by":"Ken Judy","updated_at":"2026-06-08T20:47:45Z","started_at":"2026-06-08T20:39:59Z","closed_at":"2026-06-08T20:47:45Z","close_reason":"Closed","dependency_count":0,"dependent_count":1,"comment_count":0} +{"id":"code-quality-metrics-moo","title":"Step 6: Update metrics-specification.md and run quality gates","description":"Replace test_first_pct section in metrics-specification.md with three new metrics. Run npm test, npm run lint. If workflows were modified, run smoke test per AGENTS.md.","status":"closed","priority":1,"issue_type":"task","assignee":"Ken Judy","owner":"kjudy@stride.build","created_at":"2026-04-07T14:57:21Z","created_by":"Ken Judy","updated_at":"2026-04-07T15:23:06Z","closed_at":"2026-04-07T15:23:06Z","dependencies":[{"issue_id":"code-quality-metrics-moo","depends_on_id":"code-quality-metrics-cek","type":"discovered-from","created_at":"2026-04-07T11:57:20Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"code-quality-metrics-38c","title":"Step 5: Mirror changes in GitHub Actions workflows","description":"Update code-metrics.yml (lines 158, 195, 204, 219, 284) and pr-metrics.yml (lines 83, 118, 253, 278) to add test_only_commit and uncovered_prod_commit per-commit fields and new summary rates. No new unit tests — covered by smoke test per AGENTS.md.","status":"closed","priority":1,"issue_type":"task","assignee":"Ken Judy","owner":"kjudy@stride.build","created_at":"2026-04-07T14:57:20Z","created_by":"Ken Judy","updated_at":"2026-04-07T15:19:32Z","closed_at":"2026-04-07T15:19:32Z","dependencies":[{"issue_id":"code-quality-metrics-38c","depends_on_id":"code-quality-metrics-cek","type":"discovered-from","created_at":"2026-04-07T11:57:20Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"code-quality-metrics-yuf","title":"Step 4: Update classifyDoraArchetype to use uncovered_prod_rate","description":"TDD: write failing tests in doraArchetype.test.js — high uncovered_prod_rate drives foundational-challenges, low contributes to harmonious-high-achiever. Update classifyDoraArchetype signature and logic in lib/metrics.js.","status":"closed","priority":1,"issue_type":"task","assignee":"Ken Judy","owner":"kjudy@stride.build","created_at":"2026-04-07T14:57:20Z","created_by":"Ken Judy","updated_at":"2026-04-07T15:14:29Z","closed_at":"2026-04-07T15:14:29Z","dependencies":[{"issue_id":"code-quality-metrics-yuf","depends_on_id":"code-quality-metrics-cek","type":"discovered-from","created_at":"2026-04-07T11:57:19Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"code-quality-metrics-84l","title":"Step 3: Update generateInsights for new metric signals","description":"TDD: write failing tests in generateInsights.test.js — high uncovered_prod_rate triggers critical warning, high test_isolation_rate triggers positive insight, low test_coverage_rate triggers warning. Implement in lib/metrics.js.","status":"closed","priority":1,"issue_type":"task","assignee":"Ken Judy","owner":"kjudy@stride.build","created_at":"2026-04-07T14:57:19Z","created_by":"Ken Judy","updated_at":"2026-04-07T15:11:39Z","closed_at":"2026-04-07T15:11:39Z","dependencies":[{"issue_id":"code-quality-metrics-84l","depends_on_id":"code-quality-metrics-cek","type":"discovered-from","created_at":"2026-04-07T11:57:18Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"code-quality-metrics-4j6","title":"Step 1: Per-commit fields — test_only_commit and uncovered_prod_commit in lib/git.js","description":"TDD: write failing tests in analyzeCommit.test.js for test_only_commit (testFiles\u003e0 \u0026\u0026 prodFiles===0) and uncovered_prod_commit (testFiles===0 \u0026\u0026 prodFiles\u003e0 \u0026\u0026 large_commit). Then implement both fields in lib/git.js:111 alongside existing test_first_indicator.","status":"closed","priority":1,"issue_type":"task","assignee":"Ken Judy","owner":"kjudy@stride.build","created_at":"2026-04-07T14:57:18Z","created_by":"Ken Judy","updated_at":"2026-04-07T15:03:21Z","closed_at":"2026-04-07T15:03:21Z","dependencies":[{"issue_id":"code-quality-metrics-4j6","depends_on_id":"code-quality-metrics-cek","type":"discovered-from","created_at":"2026-04-07T11:57:17Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"code-quality-metrics-g0a","title":"Step 2: Summary aggregates — test_coverage_rate, test_isolation_rate, uncovered_prod_rate in local-code-metrics.js","description":"TDD: write failing tests in collectLocalMetrics.test.js for three new summary fields (test_coverage_rate, test_isolation_rate, uncovered_prod_rate) and removal of test_first_pct. Implement in local-code-metrics.js summary computation.","status":"closed","priority":1,"issue_type":"task","assignee":"Ken Judy","owner":"kjudy@stride.build","created_at":"2026-04-07T14:57:18Z","created_by":"Ken Judy","updated_at":"2026-04-07T15:07:12Z","closed_at":"2026-04-07T15:07:12Z","dependencies":[{"issue_id":"code-quality-metrics-g0a","depends_on_id":"code-quality-metrics-cek","type":"discovered-from","created_at":"2026-04-07T11:57:18Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"code-quality-metrics-cek","title":"Granular test coverage metrics (replace test_first_indicator)","description":"Replace binary test_first_indicator with three granular metrics: test_coverage_rate (test+prod commits / total), test_isolation_rate (test-only commits / total), uncovered_prod_rate (large prod-only commits / total). uncovered_prod_rate is the highest-risk AI drift signal — currently invisible. Resolves GitHub issue #25.","status":"closed","priority":1,"issue_type":"feature","assignee":"Ken Judy","owner":"kjudy@stride.build","created_at":"2026-04-07T14:56:59Z","created_by":"Ken Judy","updated_at":"2026-04-07T15:23:06Z","closed_at":"2026-04-07T15:23:06Z","dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"code-quality-metrics-0jd","title":"Step 7: Update README.md Quick Start — add lib/ files to installation curl commands","description":"After the Option A refactor, the GitHub Actions workflows require lib/config.js, lib/statistics.js, and lib/metrics.js to be present in the target repo. Update the Quick Start Step 1 curl commands to also fetch these three files: curl -o lib/config.js ..., curl -o lib/statistics.js ..., curl -o lib/metrics.js ... Also add a note that lib/git.js and lib/claude.js are NOT needed for the workflows — only the three pure-computation modules.","status":"closed","priority":1,"issue_type":"task","owner":"kjudy@stride.build","created_at":"2026-04-07T13:25:03Z","created_by":"Ken Judy","updated_at":"2026-04-07T14:19:19Z","closed_at":"2026-04-07T14:19:19Z","close_reason":"Closed","dependencies":[{"issue_id":"code-quality-metrics-0jd","depends_on_id":"code-quality-metrics-136","type":"blocks","created_at":"2026-04-07T10:25:02Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} +{"id":"code-quality-metrics-136","title":"Step 6: Manual smoke test — trigger workflow_dispatch and verify require() resolves","description":"Cannot be unit-tested locally — requires an actual GitHub Actions run. Run: gh workflow run code-metrics.yml \u0026\u0026 gh run watch. Verify in the run log: (1) no 'Cannot find module' errors, (2) summary JSON now includes velocity_commits_per_day field (was missing before), (3) test file detection uses full 8-pattern CONFIG.TEST_FILE_PATTERNS (not the old 4-pattern single regex), (4) overall run completes successfully and creates a GitHub issue. This is the human review gate before merging.","status":"closed","priority":1,"issue_type":"task","assignee":"Ken Judy","owner":"kjudy@stride.build","created_at":"2026-04-07T13:24:40Z","created_by":"Ken Judy","updated_at":"2026-04-07T14:19:18Z","closed_at":"2026-04-07T14:19:18Z","close_reason":"Closed","dependencies":[{"issue_id":"code-quality-metrics-136","depends_on_id":"code-quality-metrics-22h","type":"blocks","created_at":"2026-04-07T10:24:39Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} +{"id":"code-quality-metrics-22h","title":"Step 5: Run lint + typecheck + tests — full quality gate","description":"Run: npm run lint \u0026\u0026 npm run typecheck \u0026\u0026 npm test. All 117+ tests must pass. Lint and typecheck must be clean. This is the same gate the pre-commit hook runs. Pay attention to: (1) any JSDoc typedef in local-code-metrics.js that references isTestFile — update if needed after the move, (2) exports.test.js which checks module exports — verify isTestFile is still exported from lib/git.js (via re-export) and now also from lib/metrics.js.","status":"closed","priority":1,"issue_type":"task","assignee":"Ken Judy","owner":"kjudy@stride.build","created_at":"2026-04-07T13:23:46Z","created_by":"Ken Judy","updated_at":"2026-04-07T13:34:39Z","closed_at":"2026-04-07T13:34:39Z","close_reason":"lint clean, typecheck clean, 119/119, 96.21%/96.42% coverage — all thresholds met","dependencies":[{"issue_id":"code-quality-metrics-22h","depends_on_id":"code-quality-metrics-ac0","type":"blocks","created_at":"2026-04-07T10:23:46Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} +{"id":"code-quality-metrics-ac0","title":"Step 4: Update CLAUDE.md — remove sync warning, document shared lib/","description":"Remove the note: 'The GitHub workflows have equivalent values hard-coded in their shell/jq logic. When adjusting thresholds, update both places.' Replace with: 'Thresholds and pure computation logic are defined once in lib/config.js, lib/statistics.js, and lib/metrics.js. The GitHub Actions workflows require these modules directly — no manual synchronization needed. lib/git.js and lib/claude.js are not shared (local shell commands and local SDK auth respectively).' Also update the Configuration section threshold table to note that changing lib/config.js CONFIG values propagates automatically to both local script and workflows.","status":"closed","priority":1,"issue_type":"task","assignee":"Ken Judy","owner":"kjudy@stride.build","created_at":"2026-04-07T13:23:18Z","created_by":"Ken Judy","updated_at":"2026-04-07T13:34:14Z","closed_at":"2026-04-07T13:34:14Z","close_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","dependencies":[{"issue_id":"code-quality-metrics-ac0","depends_on_id":"code-quality-metrics-a6p","type":"blocks","created_at":"2026-04-07T10:23:18Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} +{"id":"code-quality-metrics-a6p","title":"Step 3: Refactor pr-metrics.yml — replace inline helpers with require()","description":"Remove inline function definitions: scoreMessageQuality, classifyDoraArchetype. Replace with: const { CONFIG } = require('./lib/config'); const { scoreMessageQuality, classifyDoraArchetype, isTestFile } = require('./lib/metrics'); Replace hardcoded test file regex with isTestFile(f.filename). Replace hardcoded threshold literals with CONFIG.* references (LARGE_COMMIT_THRESHOLD: 100, SPRAWLING_COMMIT_THRESHOLD: 5, MESSAGE_QUALITY_MIN_WORDS: 10). pr-metrics.yml does not use statistics distributions so lib/statistics.js require is not needed here.","status":"closed","priority":1,"issue_type":"task","owner":"kjudy@stride.build","created_at":"2026-04-07T13:22:58Z","created_by":"Ken Judy","updated_at":"2026-04-07T13:33:12Z","closed_at":"2026-04-07T13:33:12Z","close_reason":"pr-metrics.yml refactored: inline helpers removed, require() added, isTestFile() replaces 4x regex, CONFIG.* thresholds, classifyDoraArchetype signature fixed","dependencies":[{"issue_id":"code-quality-metrics-a6p","depends_on_id":"code-quality-metrics-bul","type":"blocks","created_at":"2026-04-07T10:22:58Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} +{"id":"code-quality-metrics-bul","title":"Step 2: Refactor code-metrics.yml — replace inline helpers with require()","description":"Remove inline function definitions: scoreMessageQuality, quantile, computeStats, computeVelocityTrend, classifyDoraArchetype. Replace with at top of script block: const { CONFIG } = require('./lib/config'); const { computeStatistics, computeVelocity } = require('./lib/statistics'); const { scoreMessageQuality, classifyDoraArchetype, isTestFile } = require('./lib/metrics'); Replace all hardcoded threshold literals (100, 5, 10, \u003e20, \u003e40 etc.) with CONFIG.* references. Replace single-regex test file detection with isTestFile(filename). Replace computeStats calls with computeStatistics (takes sizes + timestamps arrays). Add velocity_commits_per_day to summary output — currently missing because inline computeVelocityTrend dropped it (computeVelocity returns {commits_per_day, trend}). The script block should have no helper function definitions after this step.","status":"closed","priority":1,"issue_type":"task","assignee":"Ken Judy","owner":"kjudy@stride.build","created_at":"2026-04-07T13:22:47Z","created_by":"Ken Judy","updated_at":"2026-04-07T13:33:11Z","closed_at":"2026-04-07T13:33:11Z","close_reason":"code-metrics.yml refactored: inline helpers removed, require() added, CONFIG.* used for all thresholds, velocity_commits_per_day added, classifyDoraArchetype signature fixed","dependencies":[{"issue_id":"code-quality-metrics-bul","depends_on_id":"code-quality-metrics-2hj","type":"blocks","created_at":"2026-04-07T10:22:47Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} +{"id":"code-quality-metrics-2hj","title":"Step 1: Move isTestFile from lib/git.js to lib/metrics.js","description":"isTestFile() in lib/git.js is a pure function — it only applies CONFIG.TEST_FILE_PATTERNS with no I/O or child_process dependency. It belongs in lib/metrics.js (alongside other classification logic) so workflows can require('./lib/metrics') cleanly without importing child_process-dependent functions. Steps: (1) copy isTestFile to lib/metrics.js and add to module.exports, (2) in lib/git.js import it from lib/metrics.js and re-export for existing callers, (3) run npm test — all 117 tests must pass unchanged.","status":"closed","priority":1,"issue_type":"task","assignee":"Ken Judy","owner":"kjudy@stride.build","created_at":"2026-04-07T13:22:30Z","created_by":"Ken Judy","updated_at":"2026-04-07T13:28:25Z","closed_at":"2026-04-07T13:28:25Z","close_reason":"isTestFile moved to lib/metrics.js, re-exported from lib/git.js. 119/119 tests pass.","dependencies":[{"issue_id":"code-quality-metrics-2hj","depends_on_id":"code-quality-metrics-9ji","type":"discovered-from","created_at":"2026-04-07T10:22:30Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":0,"dependent_count":1,"comment_count":0} +{"id":"code-quality-metrics-9ji","title":"Eliminate workflow logic duplication via direct require() of lib/ modules","description":"Epic: Replace inline JavaScript helper functions in .github/workflows/code-metrics.yml and pr-metrics.yml with require() calls to lib/config.js, lib/statistics.js, and lib/metrics.js. Currently all three components (local script + 2 workflows) duplicate threshold values and computation logic. The additions_ratio fix just required manual updates to 3 locations and nearly missed a divergent bug in pr-metrics.yml:90. Option A: actions/github-script@v7 runs in Node.js with require() resolving from GITHUB_WORKSPACE root after checkout — no build step or template engine needed. Shareable: lib/config.js, lib/statistics.js, lib/metrics.js. Not shareable: lib/git.js (child_process), lib/claude.js (local SDK auth).","status":"closed","priority":1,"issue_type":"epic","owner":"kjudy@stride.build","created_at":"2026-04-07T13:21:50Z","created_by":"Ken Judy","updated_at":"2026-04-07T14:19:19Z","closed_at":"2026-04-07T14:19:19Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"code-quality-metrics-h22","title":"Step 7: Final quality gate — lint, typecheck, full test","description":"Pre-commit gate. Run: npm run lint \u0026\u0026 npm run typecheck \u0026\u0026 npm test. All three must pass cleanly. This is the same gate the pre-commit hook runs. If lint or typecheck finds issues with the renamed fields (e.g. JSDoc typedef out of sync), fix them before committing. Note: GitHub workflow files (.github/workflows/) have additions ratio logic hard-coded in jq — these are out of scope for this cycle but should be tracked as follow-on work.","status":"closed","priority":1,"issue_type":"task","assignee":"Ken Judy","owner":"kjudy@stride.build","created_at":"2026-04-07T12:28:16Z","created_by":"Ken Judy","updated_at":"2026-04-07T12:38:16Z","closed_at":"2026-04-07T12:38:16Z","close_reason":"lint clean, typecheck clean, 117/117 tests, 96.21% lines / 96.42% functions — all thresholds met","dependencies":[{"issue_id":"code-quality-metrics-h22","depends_on_id":"code-quality-metrics-prz","type":"blocks","created_at":"2026-04-07T09:28:16Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} +{"id":"code-quality-metrics-prz","title":"Step 6: Update metrics-specification.md Metric 7","description":"Update the Metric 7 (Additions-to-Deletions Ratio) section in metrics-specification.md: (1) Formula: change from 'additions / max(deletions, 1)' to '(additions - deletions) / (additions + deletions)'. (2) Field names: additions_ratio_median -\u003e net_additions_ratio_median, additions_ratio_p90 -\u003e net_additions_ratio_p90. (3) Thresholds table: \u003c 0.33 healthy (was \u003c 2.0), 0.33-0.50 monitor (was 2.0-3.0), \u003e 0.50 warning (was \u003e 3.0). (4) Interpretation: '0.50 = more than half of all lines changed are net-new code'. (5) Remove the false positive note about net-new-file commits since the formula fix eliminates this issue.","status":"closed","priority":1,"issue_type":"task","assignee":"Ken Judy","owner":"kjudy@stride.build","created_at":"2026-04-07T12:28:07Z","created_by":"Ken Judy","updated_at":"2026-04-07T12:37:55Z","closed_at":"2026-04-07T12:37:55Z","close_reason":"metrics-specification.md updated: formula, field names, thresholds (0.33/0.50), interpretation, formula rejection rationale. Output format reference section updated.","dependencies":[{"issue_id":"code-quality-metrics-prz","depends_on_id":"code-quality-metrics-c77","type":"blocks","created_at":"2026-04-07T09:28:07Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} +{"id":"code-quality-metrics-c77","title":"Step 5: Run full test suite and verify green + no regressions","description":"Human review gate. Run: npm test. Verify: (1) Step 1 failing test now passes. (2) All 114+ tests pass. (3) No additions_ratio_* references remain in source (grep confirms). (4) Coverage thresholds still met: 80% lines, 90% functions. If any test fails, diagnose before proceeding to Step 6.","status":"closed","priority":1,"issue_type":"task","assignee":"Ken Judy","owner":"kjudy@stride.build","created_at":"2026-04-07T12:27:58Z","created_by":"Ken Judy","updated_at":"2026-04-07T12:36:05Z","closed_at":"2026-04-07T12:36:05Z","close_reason":"117/117 tests pass. New regression test green. No additions_ratio_* in source. Console warns are pre-existing intentional error-handling tests in claudeAnalysis.test.js.","dependencies":[{"issue_id":"code-quality-metrics-c77","depends_on_id":"code-quality-metrics-fii","type":"blocks","created_at":"2026-04-07T09:27:58Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} +{"id":"code-quality-metrics-fii","title":"Step 4: Update collectLocalMetrics tests for renamed fields","description":"In __tests__/collectLocalMetrics.test.js lines 144-145, update assertions from additions_ratio_median/additions_ratio_p90 to net_additions_ratio_median/net_additions_ratio_p90. Verify no other test files reference the old field names. Run: grep -r additions_ratio __tests__/ to confirm clean sweep.","status":"closed","priority":1,"issue_type":"task","assignee":"Ken Judy","owner":"kjudy@stride.build","created_at":"2026-04-07T12:27:48Z","created_by":"Ken Judy","updated_at":"2026-04-07T12:35:46Z","closed_at":"2026-04-07T12:35:46Z","close_reason":"Test assertions updated to net_additions_ratio_median/p90. Confirmed no stale additions_ratio_* in source code.","dependencies":[{"issue_id":"code-quality-metrics-fii","depends_on_id":"code-quality-metrics-at3","type":"blocks","created_at":"2026-04-07T09:27:48Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} +{"id":"code-quality-metrics-at3","title":"Step 3: Rename summary fields additions_ratio_* to net_additions_ratio_*","description":"In local-code-metrics.js lines 245-246, rename: additions_ratio_median -\u003e net_additions_ratio_median, additions_ratio_p90 -\u003e net_additions_ratio_p90. This signals the semantic change in the formula. Also update the CommitStats typedef at line 30 if it references these field names. The rename must be complete — no additions_ratio_* references should remain in the output JSON structure.","status":"closed","priority":1,"issue_type":"task","owner":"kjudy@stride.build","created_at":"2026-04-07T12:27:31Z","created_by":"Ken Judy","updated_at":"2026-04-07T12:35:02Z","closed_at":"2026-04-07T12:35:02Z","close_reason":"Fields renamed: additions_ratio_median -\u003e net_additions_ratio_median, additions_ratio_p90 -\u003e net_additions_ratio_p90 in local-code-metrics.js:251-252","dependencies":[{"issue_id":"code-quality-metrics-at3","depends_on_id":"code-quality-metrics-bko","type":"blocks","created_at":"2026-04-07T09:27:30Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} +{"id":"code-quality-metrics-bko","title":"Step 2: Replace ratio formula in local-code-metrics.js","description":"Change line 181 from: const ratios = metrics.map(m =\u003e m.total_additions / (m.total_deletions || 1)); To: const ratios = metrics.map(m =\u003e (m.total_additions - m.total_deletions) / ((m.total_additions + m.total_deletions) || 1)); New formula is bounded [-1, +1]. Value 1.0 = all net-new code. Value 0.0 = balanced. Value -0.33 = cleanup. Human review gate after this step: Step 1 failing test must now pass.","status":"closed","priority":1,"issue_type":"task","assignee":"Ken Judy","owner":"kjudy@stride.build","created_at":"2026-04-07T12:27:24Z","created_by":"Ken Judy","updated_at":"2026-04-07T12:35:01Z","closed_at":"2026-04-07T12:35:01Z","close_reason":"Formula replaced: bounded (additions-deletions)/(additions+deletions). Zero-total guard added.","dependencies":[{"issue_id":"code-quality-metrics-bko","depends_on_id":"code-quality-metrics-zbj","type":"blocks","created_at":"2026-04-07T09:27:23Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} +{"id":"code-quality-metrics-zbj","title":"Step 1: Write failing test documenting additions ratio bug","description":"TDD red phase. In __tests__/statisticalDistribution.test.js (or new additionsRatio.test.js), write a test that passes an array of all-zero-deletion commits through the ratio computation and asserts median=1.0 (or close to it), not a large number like 500. This test must FAIL before the fix is applied. It defines the bug and the fix target. Files: __tests__/statisticalDistribution.test.js, local-code-metrics.js:181","status":"closed","priority":1,"issue_type":"task","assignee":"Ken Judy","owner":"kjudy@stride.build","created_at":"2026-04-07T12:27:10Z","created_by":"Ken Judy","updated_at":"2026-04-07T12:34:26Z","closed_at":"2026-04-07T12:34:26Z","close_reason":"Failing test written and confirmed red: net_additions_ratio_median is undefined (field doesn't exist yet)","dependencies":[{"issue_id":"code-quality-metrics-zbj","depends_on_id":"code-quality-metrics-oga","type":"discovered-from","created_at":"2026-04-07T09:27:10Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":0,"dependent_count":1,"comment_count":0} +{"id":"code-quality-metrics-oga","title":"Fix additions ratio metric distortion from net-new-file commits","description":"Epic: The additions ratio metric uses additions / max(deletions, 1), which yields values approaching infinity for commits with zero deletions (new files). This distorts both median and p90 of the distribution. The RETROSPECTIVE documented p90=446x as almost certainly dominated by net-new-file commits. Fix by replacing the formula with (additions - deletions) / (additions + deletions), which is bounded [-1, +1] and immune to the distortion. Rename summary fields from additions_ratio_* to net_additions_ratio_* to signal the semantic change. Update thresholds: old 3.0 warning maps to new 0.50.","status":"closed","priority":1,"issue_type":"epic","owner":"kjudy@stride.build","created_at":"2026-04-07T12:26:57Z","created_by":"Ken Judy","updated_at":"2026-04-07T12:38:17Z","closed_at":"2026-04-07T12:38:17Z","close_reason":"All 7 implementation steps complete. Formula fixed, fields renamed, spec updated, tests green.","dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"code-quality-metrics-3pa","title":"docs: update CLAUDE.md with duplicate detection config parameters","description":"Add DUPLICATE_MIN_LINES, DUPLICATE_MIN_TOKENS, DUPLICATE_IGNORE_PATTERNS, DUPLICATE_SCAN_PATHS to the Key Metrics and Configuration tables in CLAUDE.md. Include example override values for common repo conventions (Java src/test/, Python tests/, Go _test.go). Note that defaults were validated against this repo baseline (0 production duplicates).","status":"open","priority":2,"issue_type":"task","owner":"kjudy@stride.build","created_at":"2026-06-09T14:20:40Z","created_by":"Ken Judy","updated_at":"2026-06-09T14:20:40Z","dependencies":[{"issue_id":"code-quality-metrics-3pa","depends_on_id":"code-quality-metrics-een","type":"blocks","created_at":"2026-06-09T10:20:50Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} +{"id":"code-quality-metrics-a28","title":"CHECK: Verify duplicate detection against acceptance criteria","description":"Verify all acceptance criteria: config keys present with correct defaults, runDuplicateCheck returns structured findings, PR comment includes Duplication section, Layer 2 conditional on API key, CLAUDE.md updated, all tests passing, no regressions.","status":"open","priority":2,"issue_type":"task","owner":"kjudy@stride.build","created_at":"2026-06-09T14:20:40Z","created_by":"Ken Judy","updated_at":"2026-06-09T14:20:40Z","dependencies":[{"issue_id":"code-quality-metrics-a28","depends_on_id":"code-quality-metrics-3pa","type":"blocks","created_at":"2026-06-09T10:20:50Z","created_by":"Ken Judy","metadata":"{}"},{"issue_id":"code-quality-metrics-a28","depends_on_id":"code-quality-metrics-729","type":"blocks","created_at":"2026-06-09T10:20:50Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":2,"dependent_count":1,"comment_count":0} +{"id":"code-quality-metrics-qsg","title":"ACT: Retrospective for duplicate detection PDCA cycle","description":"Facilitate retrospective on the duplicate detection implementation cycle using the /act skill. Store insights to the beads epic.\n\nRETROSPECTIVE FOCUS AREAS:\n1. Threshold calibration — DUPLICATE_MIN_LINES=5/DUPLICATE_MIN_TOKENS=50 produced 8 false positives on this repo at defaults; were the thresholds right for other repos (Java/Python codebases with more boilerplate)? Should defaults be higher?\n2. jscpd temp file strategy — jscpd writes to DUPLICATE_OUTPUT_DIR; is /tmp adequate or should this be a genuinely configurable path for CI environments with restricted tmp access?\n3. Layer 2 scope boundary — resolveModuleNeighbors uses direct imports only (not transitive). Was that the right call for semantic detection, or does it miss too much context?\n4. Config key documentation quality — are the four config keys (MIN_LINES, MIN_TOKENS, IGNORE_PATTERNS, SCAN_PATHS) named and described clearly enough for teams using this on unfamiliar repos?\n5. TDD discipline — note any RED tests that failed differently than predicted and what that indicated.\n\nOutputs: retrospective stored on the epic via 'bd update \u003cepic-id\u003e --notes'; any config adjustments captured as follow-on issues if warranted.\n\nDefinition of done: /act skill facilitated, insights stored to epic, any new issues created for follow-on work.","status":"open","priority":2,"issue_type":"task","owner":"kjudy@stride.build","created_at":"2026-06-09T14:20:40Z","created_by":"Ken Judy","updated_at":"2026-06-09T14:59:41Z","dependencies":[{"issue_id":"code-quality-metrics-qsg","depends_on_id":"code-quality-metrics-a28","type":"blocks","created_at":"2026-06-09T10:20:50Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} +{"id":"code-quality-metrics-8vg","title":"Update code-metrics.yml churn calculation","description":"Mirror the local-code-metrics.js change in the GitHub Actions workflow. In code-metrics.yml: replace the ratio formula (line ~234: m.total_additions / (m.total_deletions || 1)) with the bounded churn_intensity formula. Rename output fields additions_ratio_median/additions_ratio_p90 to churn_intensity_median/churn_intensity_p90. Update the threshold check (currently \u003c 3.0) to reflect the new [0, 0.5] range.","status":"closed","priority":2,"issue_type":"task","assignee":"Ken Judy","owner":"kjudy@stride.build","created_at":"2026-06-08T20:36:14Z","created_by":"Ken Judy","updated_at":"2026-06-08T20:47:44Z","started_at":"2026-06-08T20:45:01Z","closed_at":"2026-06-08T20:47:44Z","close_reason":"Closed","dependencies":[{"issue_id":"code-quality-metrics-8vg","depends_on_id":"code-quality-metrics-141","type":"blocks","created_at":"2026-06-08T16:36:27Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} +{"id":"code-quality-metrics-ac9","title":"Update pr-metrics.yml churn calculation","description":"Mirror the change in pr-metrics.yml. Replace per-commit additions_ratio field with churn_intensity. Update the median computation (~line 160) and all threshold checks (currently \u003e 3.0). Rename all display references from additions ratio to churn intensity in the PR comment output.","status":"closed","priority":2,"issue_type":"task","assignee":"Ken Judy","owner":"kjudy@stride.build","created_at":"2026-06-08T20:36:14Z","created_by":"Ken Judy","updated_at":"2026-06-08T20:47:44Z","started_at":"2026-06-08T20:45:01Z","closed_at":"2026-06-08T20:47:44Z","close_reason":"Closed","dependencies":[{"issue_id":"code-quality-metrics-ac9","depends_on_id":"code-quality-metrics-141","type":"blocks","created_at":"2026-06-08T16:36:27Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} +{"id":"code-quality-metrics-aww","title":"Update CLAUDE.md health threshold table for churn_intensity","description":"Update the health threshold table row: rename 'Additions-to-deletions ratio (median)' to 'Churn intensity (median)' and change the threshold from '\u003c3.0' to '\u003e0.05' (low churn = mostly net-new code = AI drift signal; high churn = rework = healthy refactoring). Also update the Key Metrics section description if present.","status":"closed","priority":2,"issue_type":"task","assignee":"Ken Judy","owner":"kjudy@stride.build","created_at":"2026-06-08T20:36:14Z","created_by":"Ken Judy","updated_at":"2026-06-08T20:47:44Z","started_at":"2026-06-08T20:45:01Z","closed_at":"2026-06-08T20:47:44Z","close_reason":"Closed","dependencies":[{"issue_id":"code-quality-metrics-aww","depends_on_id":"code-quality-metrics-141","type":"blocks","created_at":"2026-06-08T16:36:27Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} +{"id":"code-quality-metrics-csi.7","title":"Step 7: CHECK — full test suite passes, coverage thresholds met","status":"closed","priority":2,"issue_type":"task","assignee":"Ken Judy","owner":"kjudy@stride.build","created_at":"2026-04-08T14:44:13Z","created_by":"Ken Judy","updated_at":"2026-04-08T14:57:39Z","closed_at":"2026-04-08T14:57:39Z","close_reason":"✓ 120/120 tests. Lines 96.21% (≥80%). Functions 96.77% (≥90%). Typecheck clean.","dependencies":[{"issue_id":"code-quality-metrics-csi.7","depends_on_id":"code-quality-metrics-csi","type":"parent-child","created_at":"2026-04-08T11:44:13Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"code-quality-metrics-csi.5","title":"Step 5: RED — test summary fields use addition_fraction_p50 and addition_fraction_p90","status":"closed","priority":2,"issue_type":"task","assignee":"Ken Judy","owner":"kjudy@stride.build","created_at":"2026-04-08T14:44:12Z","created_by":"Ken Judy","updated_at":"2026-04-08T14:57:19Z","closed_at":"2026-04-08T14:57:19Z","close_reason":"✓ Tests assert addition_fraction_p50/p90 present, bounded \u003c=1.0, old names absent","dependencies":[{"issue_id":"code-quality-metrics-csi.5","depends_on_id":"code-quality-metrics-csi","type":"parent-child","created_at":"2026-04-08T11:44:11Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"code-quality-metrics-csi.6","title":"Step 6: GREEN — rename summary fields in local-code-metrics.js:245-246","status":"closed","priority":2,"issue_type":"task","owner":"kjudy@stride.build","created_at":"2026-04-08T14:44:12Z","created_by":"Ken Judy","updated_at":"2026-04-08T14:57:19Z","closed_at":"2026-04-08T14:57:19Z","close_reason":"✓ local-code-metrics.js:245-246 renamed. All 120 tests pass.","dependencies":[{"issue_id":"code-quality-metrics-csi.6","depends_on_id":"code-quality-metrics-csi","type":"parent-child","created_at":"2026-04-08T11:44:12Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"code-quality-metrics-csi.3","title":"Step 3: RED — test computeStatistics on net-new-file ratio data yields p90 \u003c= 1.0","status":"closed","priority":2,"issue_type":"task","assignee":"Ken Judy","owner":"kjudy@stride.build","created_at":"2026-04-08T14:44:11Z","created_by":"Ken Judy","updated_at":"2026-04-08T14:53:18Z","closed_at":"2026-04-08T14:53:18Z","close_reason":"✓ computeStatistics is already correct — contracts documented. Fix is in ratios array construction.","dependencies":[{"issue_id":"code-quality-metrics-csi.3","depends_on_id":"code-quality-metrics-csi","type":"parent-child","created_at":"2026-04-08T11:44:10Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"code-quality-metrics-csi.4","title":"Step 4: GREEN — update local-code-metrics.js:181 ratios array to bounded formula, filter nulls","status":"closed","priority":2,"issue_type":"task","assignee":"Ken Judy","owner":"kjudy@stride.build","created_at":"2026-04-08T14:44:11Z","created_by":"Ken Judy","updated_at":"2026-04-08T14:56:32Z","closed_at":"2026-04-08T14:56:32Z","close_reason":"✓ local-code-metrics.js:181 uses change_ratio (bounded [0,1]), nulls filtered. Typedef updated. All 120 tests pass.","dependencies":[{"issue_id":"code-quality-metrics-csi.4","depends_on_id":"code-quality-metrics-csi","type":"parent-child","created_at":"2026-04-08T11:44:11Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"code-quality-metrics-csi.2","title":"Step 2: GREEN — update lib/git.js:124 change_ratio to bounded addition fraction","status":"closed","priority":2,"issue_type":"task","assignee":"Ken Judy","owner":"kjudy@stride.build","created_at":"2026-04-08T14:44:10Z","created_by":"Ken Judy","updated_at":"2026-04-08T14:51:31Z","closed_at":"2026-04-08T14:51:31Z","close_reason":"✓ lib/git.js:124 updated: additions/(additions+deletions), null when both zero","dependencies":[{"issue_id":"code-quality-metrics-csi.2","depends_on_id":"code-quality-metrics-csi","type":"parent-child","created_at":"2026-04-08T11:44:09Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"code-quality-metrics-csi.1","title":"Step 1: RED — test analyzeCommit pure-addition commit yields change_ratio as number in [0,1]","status":"closed","priority":2,"issue_type":"task","assignee":"Ken Judy","owner":"kjudy@stride.build","created_at":"2026-04-08T14:44:09Z","created_by":"Ken Judy","updated_at":"2026-04-08T14:51:23Z","closed_at":"2026-04-08T14:51:23Z","close_reason":"✓ Tests pass. lib/git.js change_ratio now bounded [0,1], null for binary-only commits","dependencies":[{"issue_id":"code-quality-metrics-csi.1","depends_on_id":"code-quality-metrics-csi","type":"parent-child","created_at":"2026-04-08T11:44:09Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"code-quality-metrics-csi","title":"Fix: Replace unbounded additions/deletions churn ratio with bounded addition fraction","notes":"## Retrospective\n\nSession goal: Replace unbounded additions/max(deletions,1) churn ratio with bounded addition fraction [0,1], fixing p50/p90 distortion from net-new-file commits.\n\nWhat worked:\n- Choosing additions/(additions+deletions) over Winsorization/log-transform — naturally bounded, no arbitrary cap, semantically precise. Decision locked in PLAN, no mid-cycle drift.\n- computeStatistics needed zero changes — confirmed clean architectural separation between stat function and input construction.\n- @ts-check typedef caught change_ratio type mismatch immediately when implementation changed from string to number|null.\n- Naming the metric semantically (addition_fraction not ratio_v2) made rename obvious and test assertions self-documenting.\n\nWhat to improve:\n- Step 3 RED test passed immediately because it tested computeStatistics with already-bounded inputs, not the actual integration. Locate tests at the integration boundary (summary JSON output) when the bug is in the caller, not the callee.\n- Step 3+4 collapsed into one unit in practice — plan should merge them when test and implementation always land in the same commit.\n- change_ratio computed in lib/git.js, null-filtering inline in local-code-metrics.js — minor asymmetry to watch if ratios logic grows.\n\nKey insights:\n- Write the integration-level failing test first (what does summary JSON look like?), then unit tests. Work inward from observable output.\n- 'Backwards compatibility explicitly deprioritized' stated in PLAN freed clean field rename without aliases.\n- When a RED test passes immediately, the bug is one layer up — rewrite the test at that layer.\n\nAction items for next cycle:\n1. Start: State which layer a RED test targets before writing it. If it passes immediately, move test up one layer.\n2. Stop: Separate beads tasks for RED+GREEN pairs that always collapse into one commit.\n3. Keep: Lock metric naming semantics in PLAN phase before implementation begins.","status":"closed","priority":2,"issue_type":"epic","owner":"kjudy@stride.build","created_at":"2026-04-08T14:43:03Z","created_by":"Ken Judy","updated_at":"2026-04-08T15:00:28Z","closed_at":"2026-04-08T14:57:40Z","close_reason":"✓ Complete. Bounded addition fraction [0,1] replaces unbounded ratio. p90 no longer distorted by net-new-file commits.","dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"code-quality-metrics-9cf.5","title":"Step 5: REFACTOR — update existing analyzeCommit assertions for new proportion values","status":"closed","priority":2,"issue_type":"task","assignee":"Ken Judy","owner":"kjudy@stride.build","created_at":"2026-04-08T01:01:52Z","created_by":"Ken Judy","updated_at":"2026-04-08T13:26:29Z","closed_at":"2026-04-08T13:26:29Z","close_reason":"REFACTOR: stale assertions updated to proportion values; 15/15 pass","dependencies":[{"issue_id":"code-quality-metrics-9cf.5","depends_on_id":"code-quality-metrics-9cf","type":"parent-child","created_at":"2026-04-07T22:01:51Z","created_by":"Ken Judy","metadata":"{}"},{"issue_id":"code-quality-metrics-9cf.5","depends_on_id":"code-quality-metrics-9cf.4","type":"blocks","created_at":"2026-04-07T22:02:08Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} +{"id":"code-quality-metrics-9cf.6","title":"Step 6: CHECK — run full test suite and typecheck; verify p90 no longer inflated","status":"closed","priority":2,"issue_type":"task","assignee":"Ken Judy","owner":"kjudy@stride.build","created_at":"2026-04-08T01:01:52Z","created_by":"Ken Judy","updated_at":"2026-04-08T13:27:18Z","closed_at":"2026-04-08T13:27:18Z","close_reason":"CHECK: 119/119 pass, typecheck clean","dependencies":[{"issue_id":"code-quality-metrics-9cf.6","depends_on_id":"code-quality-metrics-9cf","type":"parent-child","created_at":"2026-04-07T22:01:52Z","created_by":"Ken Judy","metadata":"{}"},{"issue_id":"code-quality-metrics-9cf.6","depends_on_id":"code-quality-metrics-9cf.5","type":"blocks","created_at":"2026-04-07T22:02:09Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} +{"id":"code-quality-metrics-9cf.4","title":"Step 4: GREEN — rename additions_ratio_* output fields to addition_proportion_*","status":"closed","priority":2,"issue_type":"task","assignee":"Ken Judy","owner":"kjudy@stride.build","created_at":"2026-04-08T01:01:51Z","created_by":"Ken Judy","updated_at":"2026-04-08T13:25:51Z","closed_at":"2026-04-08T13:25:51Z","close_reason":"Field rename done in same edit","dependencies":[{"issue_id":"code-quality-metrics-9cf.4","depends_on_id":"code-quality-metrics-9cf","type":"parent-child","created_at":"2026-04-07T22:01:50Z","created_by":"Ken Judy","metadata":"{}"},{"issue_id":"code-quality-metrics-9cf.4","depends_on_id":"code-quality-metrics-9cf.3","type":"blocks","created_at":"2026-04-07T22:02:07Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} +{"id":"code-quality-metrics-9cf.3","title":"Step 3: GREEN — update ratio array computation in local-code-metrics.js","status":"closed","priority":2,"issue_type":"task","assignee":"Ken Judy","owner":"kjudy@stride.build","created_at":"2026-04-08T01:01:50Z","created_by":"Ken Judy","updated_at":"2026-04-08T13:25:50Z","closed_at":"2026-04-08T13:25:50Z","close_reason":"GREEN: ratio array uses proportion formula; output fields renamed to addition_proportion_*","dependencies":[{"issue_id":"code-quality-metrics-9cf.3","depends_on_id":"code-quality-metrics-9cf","type":"parent-child","created_at":"2026-04-07T22:01:50Z","created_by":"Ken Judy","metadata":"{}"},{"issue_id":"code-quality-metrics-9cf.3","depends_on_id":"code-quality-metrics-9cf.2","type":"blocks","created_at":"2026-04-07T22:02:07Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} +{"id":"code-quality-metrics-9cf.1","title":"Step 1: RED — write failing tests for addition proportion behavior","status":"closed","priority":2,"issue_type":"task","assignee":"Ken Judy","owner":"kjudy@stride.build","created_at":"2026-04-08T01:01:49Z","created_by":"Ken Judy","updated_at":"2026-04-08T13:24:29Z","closed_at":"2026-04-08T13:24:29Z","close_reason":"RED confirmed: 3 failing tests written","dependencies":[{"issue_id":"code-quality-metrics-9cf.1","depends_on_id":"code-quality-metrics-9cf","type":"parent-child","created_at":"2026-04-07T22:01:48Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":0,"dependent_count":1,"comment_count":0} +{"id":"code-quality-metrics-9cf.2","title":"Step 2: GREEN — update change_ratio in lib/git.js to proportion formula","status":"closed","priority":2,"issue_type":"task","assignee":"Ken Judy","owner":"kjudy@stride.build","created_at":"2026-04-08T01:01:49Z","created_by":"Ken Judy","updated_at":"2026-04-08T13:25:08Z","closed_at":"2026-04-08T13:25:08Z","close_reason":"GREEN: change_ratio now proportion formula, 3 new tests pass","dependencies":[{"issue_id":"code-quality-metrics-9cf.2","depends_on_id":"code-quality-metrics-9cf","type":"parent-child","created_at":"2026-04-07T22:01:49Z","created_by":"Ken Judy","metadata":"{}"},{"issue_id":"code-quality-metrics-9cf.2","depends_on_id":"code-quality-metrics-9cf.1","type":"blocks","created_at":"2026-04-07T22:02:06Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} +{"id":"code-quality-metrics-9cf","title":"Feature: Fix unbounded additions/deletions churn ratio","notes":"## Retrospective\n\nSession goal: Replace unbounded additions/max(deletions,1) churn ratio with addition_proportion bounded [0,1].\n\nWhat worked:\n- Calling shots before each phase kept steps tight and stale-assertion failures expected, not surprising\n- PLAN analysis ruled out capped ratio and log-diff before coding — no mid-stream rethinking\n- Proportion formula required zero config changes; statistically clean with no magic numbers\n\nWhat to improve:\n- After any field rename, grep the full test suite for the old name before marking step done\n- REFACTOR scope check was too narrow: only updated the file being actively edited, missed collectLocalMetrics.test.js\n\nKey insights:\n- add grep -r \u003cold_symbol\u003e __tests__/ as explicit first action in any REFACTOR step involving renames\n- Full suite run in CHECK is non-negotiable — caught the missed assertion that targeted-file run would have missed\n- proportion = additions/(additions+deletions) is the right formula for any bounded churn metric; reuse in workflows\n\nAction items for next cycle:\n1. Add interface-grep step to REFACTOR: grep -r \u003cold_symbol\u003e __tests__/ before touching assertions\n2. Smoke test (node local-code-metrics.js) belongs in CHECK definition-of-done for script-level changes\n3. Keep shot-calling discipline — it was the single biggest accelerator this session","status":"closed","priority":2,"issue_type":"epic","owner":"kjudy@stride.build","created_at":"2026-04-08T00:53:46Z","created_by":"Ken Judy","updated_at":"2026-04-08T13:29:32Z","closed_at":"2026-04-08T13:27:18Z","close_reason":"All steps complete. change_ratio now bounded [0,1] proportion. p90 no longer inflated by net-new-file commits.","metadata":{"pdca_complete":"true,domain=statistics,complexity=small"},"dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"code-quality-metrics-mkl.5","title":"chore: update @typedef CommitStats with net_additions_ratio field","status":"closed","priority":2,"issue_type":"task","owner":"kjudy@stride.build","created_at":"2026-04-07T23:18:47Z","created_by":"Ken Judy","updated_at":"2026-04-07T23:24:07Z","closed_at":"2026-04-07T23:24:07Z","dependencies":[{"issue_id":"code-quality-metrics-mkl.5","depends_on_id":"code-quality-metrics-mkl","type":"parent-child","created_at":"2026-04-07T20:18:47Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"code-quality-metrics-mkl.3","title":"test: summary includes net_additions_ratio_median and _p90","status":"closed","priority":2,"issue_type":"task","assignee":"Ken Judy","owner":"kjudy@stride.build","created_at":"2026-04-07T23:18:46Z","created_by":"Ken Judy","updated_at":"2026-04-07T23:24:05Z","closed_at":"2026-04-07T23:24:05Z","dependencies":[{"issue_id":"code-quality-metrics-mkl.3","depends_on_id":"code-quality-metrics-mkl","type":"parent-child","created_at":"2026-04-07T20:18:45Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"code-quality-metrics-mkl.4","title":"feat: compute net ratio distribution and add to summary in local-code-metrics.js","status":"closed","priority":2,"issue_type":"task","owner":"kjudy@stride.build","created_at":"2026-04-07T23:18:46Z","created_by":"Ken Judy","updated_at":"2026-04-07T23:24:06Z","closed_at":"2026-04-07T23:24:06Z","dependencies":[{"issue_id":"code-quality-metrics-mkl.4","depends_on_id":"code-quality-metrics-mkl","type":"parent-child","created_at":"2026-04-07T20:18:46Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"code-quality-metrics-mkl.2","title":"feat: add net_additions_ratio to analyzeCommit in lib/git.js","status":"closed","priority":2,"issue_type":"task","owner":"kjudy@stride.build","created_at":"2026-04-07T23:18:45Z","created_by":"Ken Judy","updated_at":"2026-04-07T23:22:38Z","closed_at":"2026-04-07T23:22:38Z","dependencies":[{"issue_id":"code-quality-metrics-mkl.2","depends_on_id":"code-quality-metrics-mkl","type":"parent-child","created_at":"2026-04-07T20:18:45Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"code-quality-metrics-mkl.1","title":"test: analyzeCommit returns net_additions_ratio bounded [0,1]","status":"closed","priority":2,"issue_type":"task","assignee":"Ken Judy","owner":"kjudy@stride.build","created_at":"2026-04-07T23:18:44Z","created_by":"Ken Judy","updated_at":"2026-04-07T23:22:37Z","closed_at":"2026-04-07T23:22:37Z","dependencies":[{"issue_id":"code-quality-metrics-mkl.1","depends_on_id":"code-quality-metrics-mkl","type":"parent-child","created_at":"2026-04-07T20:18:44Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"code-quality-metrics-mkl","title":"Feature: Add net_additions_ratio metric to fix unbounded churn ratio distortion","notes":"## Retrospective\n\nSession goal: Fix unbounded additions/deletions churn ratio distorting p50/p90 by adding bounded net_additions_ratio alongside existing metric.\n\nWhat worked:\n- Add-not-replace approach: preserves backward compatibility, avoids silencing drift signals\n- net_additions_ratio reuses totalLines already in scope — zero extra computation\n- Full test list planned before opening files — DO phase had no surprises\n- Called shots matched actual failures in both RED cycles\n\nWhat to improve:\n- Verify beads flag syntax (bd help update) at session start, not mid-PLAN\n- mkl.2 (git.js field) and mkl.5 (typedef) were one atomic unit — should be one task\n\nKey insights:\n- For bounded ratio metrics: (add - del) / (add + del) is the canonical form; immune to new-file distortion\n- When IDE surfaces a type error mid-step, resolve it immediately rather than deferring — keeps implementation coherent\n- Called shot mismatches are the signal, not the noise — stop and diagnose when prediction != actual failure\n\nAction items for next cycle:\n1. Run bd help update once during PLAN to confirm flag names\n2. Treat field addition + typedef update as one task when they are inseparable\n3. Keep called shots — two for two this session","status":"closed","priority":2,"issue_type":"epic","owner":"kjudy@stride.build","created_at":"2026-04-07T23:17:38Z","created_by":"Ken Judy","updated_at":"2026-04-07T23:26:56Z","closed_at":"2026-04-07T23:24:07Z","dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"code-quality-metrics-zx6","title":"Follow-on: Update GitHub workflow files for renamed ratio fields","description":"Out of scope for this cycle. The GitHub workflow files (.github/workflows/code-metrics.yml and pr-metrics.yml) have additions ratio logic hard-coded in jq shell scripts. They reference the old field names and thresholds. After the local script changes are merged, these workflows need corresponding updates: field name references and threshold values (\u003e3.0 -\u003e \u003e0.50). This is deferred to avoid scope creep in the current cycle.","status":"closed","priority":2,"issue_type":"task","assignee":"Ken Judy","owner":"kjudy@stride.build","created_at":"2026-04-07T12:29:33Z","created_by":"Ken Judy","updated_at":"2026-04-07T12:41:46Z","closed_at":"2026-04-07T12:41:46Z","close_reason":"Both workflow files updated: formula fixed (bounded), fields renamed to net_additions_ratio_*, thresholds updated to \u003c0.50, display labels updated. Also cleaned up redundant sort+map in code-metrics.yml ratioStats computation.","dependencies":[{"issue_id":"code-quality-metrics-zx6","depends_on_id":"code-quality-metrics-h22","type":"blocks","created_at":"2026-04-07T09:29:33Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} +{"id":"code-quality-metrics-803","title":"D2-Step1: computeStatistics RED-GREEN-REFACTOR","description":"TDD step: implement computeStatistics(sizes, timestamps) returning p50/p90/p95/stddev/mean/trend for the statistical distributions feature. Part of D2 (Implement: Statistical analysis).","status":"closed","priority":2,"issue_type":"task","owner":"kjudy@stride.build","created_at":"2026-03-26T14:21:53Z","created_by":"Ken Judy","updated_at":"2026-03-26T14:24:37Z","closed_at":"2026-03-26T14:24:37Z","close_reason":"GREEN. computeStatistics() implemented: p50/p90/p95/mean/stddev/trend/isOutlier. 8 tests. Zero new dependencies — hand-rolled quantile, linear regression, stddev. Full suite 71/71 pass.","dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"code-quality-metrics-hi4","title":"Retrospect: Compare drift detection accuracy before/after enhancement","description":"ACT phase. Depends on: C1, C2. Run old and new versions against the same commit set; document which commits new metrics flagged that old ones missed; note any false positives from Claude API analysis; evaluate whether DORA archetype classification matched manual assessment; update thresholds in CONFIG if findings suggest recalibration; write findings to a RETROSPECTIVE.md or update the measuring-ai-code-drift article with empirical results.","status":"closed","priority":2,"issue_type":"feature","owner":"kjudy@stride.build","created_at":"2026-03-25T18:50:56Z","created_by":"Ken Judy","updated_at":"2026-03-26T20:38:30Z","closed_at":"2026-03-26T20:38:30Z","close_reason":"Closed","dependencies":[{"issue_id":"code-quality-metrics-hi4","depends_on_id":"code-quality-metrics-zbs","type":"blocks","created_at":"2026-03-25T14:51:15Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} +{"id":"code-quality-metrics-09v","title":"Document: Update CLAUDE.md with new metrics, thresholds, and Node 18 requirement","description":"ACT phase. Depends on: C1, C2. Update CLAUDE.md: add 3 new metrics to Key Metrics table (velocity trend, additions ratio, message quality); document ANTHROPIC_API_KEY optional usage and local_claude_analysis.json output; update Node.js requirement from \u003e=14 to \u003e=18; add simple-statistics and @anthropic-ai/sdk to dev notes; update test command descriptions for new test files.","status":"closed","priority":2,"issue_type":"chore","owner":"kjudy@stride.build","created_at":"2026-03-25T18:50:48Z","created_by":"Ken Judy","updated_at":"2026-03-26T20:32:52Z","closed_at":"2026-03-26T20:32:52Z","close_reason":"Closed","dependencies":[{"issue_id":"code-quality-metrics-09v","depends_on_id":"code-quality-metrics-zbs","type":"blocks","created_at":"2026-03-25T14:51:15Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} +{"id":"code-quality-metrics-zbs","title":"Validate: Run enhanced tool against real repo, verify output quality","description":"CHECK phase. Depends on: C1 (tests passing). Steps: (1) run node local-code-metrics.js, confirm new JSON fields present (velocity_trend, message_quality_pct, dora_archetype, p50_lines, p90_lines, p95_lines, commit_size_trend); (2) set ANTHROPIC_API_KEY and re-run, confirm local_claude_analysis.json written with expected structure; (3) verify Claude analysis only runs on commits meeting the risk pre-filter; (4) compare archetype to manual assessment; (5) run without API key, confirm graceful skip; (6) check no regression on existing metrics.","status":"closed","priority":2,"issue_type":"feature","owner":"kjudy@stride.build","created_at":"2026-03-25T18:50:39Z","created_by":"Ken Judy","updated_at":"2026-03-26T20:32:51Z","closed_at":"2026-03-26T20:32:51Z","close_reason":"Closed","dependencies":[{"issue_id":"code-quality-metrics-zbs","depends_on_id":"code-quality-metrics-m83","type":"blocks","created_at":"2026-03-25T14:51:15Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":1,"dependent_count":2,"comment_count":0} +{"id":"code-quality-metrics-m83","title":"Test: New unit tests for DORA metrics and statistical functions","description":"CHECK phase. Depends on: D1, D2, D3.\nFiles: __tests__/ (new test files)\n\nNew test files needed:\n- __tests__/commitVelocity.test.js: velocity calculation, trend detection (first-half vs second-half split), edge cases (single commit, same-day commits)\n- __tests__/messageQuality.test.js: conventional commit regex against valid and invalid examples, word count threshold, vague message detection\n- __tests__/doraArchetype.test.js: archetype classification logic for all three archetypes + boundary conditions\n- __tests__/statisticalDistribution.test.js: p50/p90/p95 with known datasets, outlier detection, trend slope sign\n- __tests__/claudeAnalysis.test.js: mock @anthropic-ai/sdk; verify pre-filter logic, diff truncation, graceful skip when no API key, JSON output structure\n\nMaintain thresholds: 80% line coverage, 90% function coverage.\nAll tests must mock child_process and fs — no real git repo required.","status":"closed","priority":2,"issue_type":"feature","owner":"kjudy@stride.build","created_at":"2026-03-25T18:50:11Z","created_by":"Ken Judy","updated_at":"2026-03-26T20:32:44Z","closed_at":"2026-03-26T20:32:44Z","close_reason":"Closed","dependencies":[{"issue_id":"code-quality-metrics-m83","depends_on_id":"code-quality-metrics-07h","type":"blocks","created_at":"2026-03-25T14:51:14Z","created_by":"Ken Judy","metadata":"{}"},{"issue_id":"code-quality-metrics-m83","depends_on_id":"code-quality-metrics-3ui","type":"blocks","created_at":"2026-03-25T14:51:14Z","created_by":"Ken Judy","metadata":"{}"},{"issue_id":"code-quality-metrics-m83","depends_on_id":"code-quality-metrics-4nf","type":"blocks","created_at":"2026-03-25T14:51:14Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":3,"dependent_count":1,"comment_count":0} +{"id":"code-quality-metrics-47z","title":"Implement: GitHub workflow DORA enhancements","description":"DO phase. Depends on: D1, D2, D3.\nFiles: .github/workflows/code-metrics.yml, .github/workflows/pr-metrics.yml\n\ncode-metrics.yml changes:\n- Add DORA archetype classification to GitHub issue report (map metrics to harmonious-high-achiever / foundational-challenges / legacy-bottleneck)\n- Replace average commit size with p90 in the issue summary table\n- Add PR size trajectory section: compare current 30-day window to prior 30-day window (requires storing prior artifact or computing from git history range)\n- Add Claude API step (keyed by ANTHROPIC_API_KEY secret): analyze top 5 risk commits from weekly batch\n\npr-metrics.yml changes:\n- Add review burden score to PR comment: (total_additions + total_deletions) × changed_files / 100, rated low/medium/high/extreme\n- Add small-batch score: % of commits in this PR that are under 100 lines and under 5 files\n- Add DORA capability alignment summary: which of the 5 measurable capabilities does this PR demonstrate?\n- Add Claude API step (keyed by ANTHROPIC_API_KEY secret): analyze highest-risk commit in the PR","status":"closed","priority":2,"issue_type":"feature","owner":"kjudy@stride.build","created_at":"2026-03-25T18:50:01Z","created_by":"Ken Judy","updated_at":"2026-03-26T20:27:07Z","closed_at":"2026-03-26T20:27:07Z","close_reason":"Closed","dependencies":[{"issue_id":"code-quality-metrics-47z","depends_on_id":"code-quality-metrics-07h","type":"blocks","created_at":"2026-03-25T14:51:04Z","created_by":"Ken Judy","metadata":"{}"},{"issue_id":"code-quality-metrics-47z","depends_on_id":"code-quality-metrics-3ui","type":"blocks","created_at":"2026-03-25T14:51:04Z","created_by":"Ken Judy","metadata":"{}"},{"issue_id":"code-quality-metrics-47z","depends_on_id":"code-quality-metrics-4nf","type":"blocks","created_at":"2026-03-25T14:51:04Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":3,"dependent_count":0,"comment_count":0} +{"id":"code-quality-metrics-07h","title":"Implement: Claude API diff-level integration (optional)","description":"DO phase. Depends on: Design issue P2.\nFiles: local-code-metrics.js, package.json\n\nChanges:\n1. npm install @anthropic-ai/sdk\n2. At startup: check process.env.ANTHROPIC_API_KEY; if absent log \"Claude analysis skipped (no ANTHROPIC_API_KEY)\" and proceed\n3. Pre-filter: sort commits by (large_commit \u0026\u0026 additions \u003e deletions*2) desc; take top AI_ANALYSIS_MAX_COMMITS (default 5)\n4. For each filtered commit: run git show --stat {sha} + git diff {sha}^ {sha} -- (truncate combined output at 4000 chars)\n5. Send to claude-sonnet-4-6 with structured output prompt asking for: { ai_confidence: 0-100, risk_score: 0-100, patterns: string[], architectural_concerns: string[], summary: string }\n6. Annotate matching entries in commit metrics array with Claude findings\n7. Write local_claude_analysis.json: { analyzed_at, model, commits_analyzed: N, results: [...] }\n8. Add Claude findings to console report section\nNode 18+ required for @anthropic-ai/sdk.","status":"closed","priority":2,"issue_type":"feature","owner":"kjudy@stride.build","created_at":"2026-03-25T18:49:34Z","created_by":"Ken Judy","updated_at":"2026-03-26T17:47:52Z","closed_at":"2026-03-26T17:47:52Z","close_reason":"Closed","dependencies":[{"issue_id":"code-quality-metrics-07h","depends_on_id":"code-quality-metrics-4o8","type":"blocks","created_at":"2026-03-25T14:51:03Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":1,"dependent_count":2,"comment_count":0} +{"id":"code-quality-metrics-3ui","title":"Implement: Statistical analysis with simple-statistics","description":"DO phase. Depends on: Design issues (P1).\nFiles: local-code-metrics.js, package.json\n\nChanges:\n1. Add simple-statistics as prod dependency (npm install simple-statistics)\n2. Replace avg_lines_changed with distribution: p50_lines, p90_lines, p95_lines, stddev_lines\n3. Replace avg_files_changed with: p50_files, p90_files, p95_files, stddev_files\n4. Add outlier detection: flag individual commits where lines_changed \u003e mean + 2*stddev as outlier:true\n5. Add trend slope: linear regression of commit sizes over time; slope \u003e 0 = growing, \u003c 0 = shrinking; include in summary as commit_size_trend: \"growing|stable|shrinking\"\n6. Keep existing avg_ fields for backwards compatibility (or alias them to p50)","status":"closed","priority":2,"issue_type":"feature","owner":"kjudy@stride.build","created_at":"2026-03-25T18:49:23Z","created_by":"Ken Judy","updated_at":"2026-03-26T14:26:51Z","closed_at":"2026-03-26T14:26:51Z","close_reason":"D2 complete. computeStatistics() implemented with hand-rolled quantile/stddev/linear-regression (zero new deps). Adds p50/p90/p95/stddev/trend to summary JSON; outlier flag per commit. 72/72 tests pass, typecheck clean.","dependencies":[{"issue_id":"code-quality-metrics-3ui","depends_on_id":"code-quality-metrics-6g2","type":"blocks","created_at":"2026-03-25T14:51:03Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":1,"dependent_count":2,"comment_count":0} +{"id":"code-quality-metrics-4nf","title":"Implement: DORA-aligned metrics in local-code-metrics.js","description":"DO phase. Depends on: Design issues (P1, P2).\nFiles: local-code-metrics.js\n\nChanges:\n1. Commit velocity metric: calculate commits/day over the analysis window; split into first-half / second-half to compute velocity trend (accelerating / stable / decelerating)\n2. Additions-to-deletions ratio distribution: track ratio per commit, compute median and p90 across all commits (supplement existing per-commit large_commit flag)\n3. Commit message quality scoring: regex for conventional commit prefixes (feat|fix|refactor|test|chore|docs|perf|ci|build|revert) + word count \u003e= MESSAGE_QUALITY_MIN_WORDS; report % compliant\n4. DORA archetype classification: map metric combinations to archetypes (harmonious-high-achiever / foundational-challenges / legacy-bottleneck / other) based on composite score across all 8 metrics\n5. New CONFIG keys: MESSAGE_QUALITY_MIN_WORDS (default 10), VELOCITY_TREND_WINDOW\n6. Add archetype + velocity trend + message quality to summary JSON and console report","status":"closed","priority":2,"issue_type":"feature","owner":"kjudy@stride.build","created_at":"2026-03-25T18:48:57Z","created_by":"Ken Judy","updated_at":"2026-03-26T14:35:25Z","closed_at":"2026-03-26T14:35:25Z","close_reason":"D1 complete. Added: computeVelocity(), scoreMessageQuality(), classifyDoraArchetype(). All wired into collectLocalMetrics() summary. 4 new CONFIG keys (MESSAGE_QUALITY_MIN_WORDS, AI_ANALYSIS_MAX_COMMITS, AI_DIFF_MAX_CHARS, AI_RISK_ADDITIONS_RATIO). 89/89 tests, typecheck clean, zero new deps.","dependencies":[{"issue_id":"code-quality-metrics-4nf","depends_on_id":"code-quality-metrics-6g2","type":"blocks","created_at":"2026-03-25T14:51:03Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":1,"dependent_count":2,"comment_count":0} +{"id":"code-quality-metrics-4o8","title":"Design: Claude API integration architecture","description":"PLAN phase — prerequisite to D3 implementation.\n\nArchitecture decisions:\n- Diff extraction: git show --stat + git diff {sha}^ {sha} truncated at 4000 chars per commit\n- Risk pre-filter: only analyze commits where large_commit=true AND additions \u003e deletions * 2 (reduces API calls to ~3-5 per run)\n- Graceful degradation: if no ANTHROPIC_API_KEY in env, skip silently, add note to summary JSON\n- New output file: local_claude_analysis.json with per-commit risk scores and findings\n- Model: claude-sonnet-4-6 with structured JSON output (ai_confidence 0-100, risk_score 0-100, patterns[], architectural_concerns[])\n- Prompt design: system prompt describing AI code patterns to detect (generic names, boilerplate CRUD, identical-structure repetition, absent domain language, boundary violations)\n- GitHub workflows: same pattern, keyed by ANTHROPIC_API_KEY repository secret","status":"closed","priority":2,"issue_type":"feature","owner":"kjudy@stride.build","created_at":"2026-03-25T18:44:43Z","created_by":"Ken Judy","updated_at":"2026-03-26T14:20:36Z","closed_at":"2026-03-26T14:20:36Z","close_reason":"P2 complete. Architecture decisions: single-file approach (local-code-metrics.js only), conditional require() for graceful Node 16 degradation, sequential API calls (max 5), CLAUDE_SYSTEM_PROMPT as exported const, selectClaudeCommits pre-filter reuses AI_RISK_ADDITIONS_RATIO. Test mock pattern documented. New exports: computeStatistics, computeVelocity, scoreMessageQuality, classifyDoraArchetype, getAnthropicClient, selectClaudeCommits, analyzeWithClaude, getCommitDiff, CLAUDE_SYSTEM_PROMPT.","dependency_count":0,"dependent_count":1,"comment_count":0} +{"id":"code-quality-metrics-6g2","title":"Design: DORA metric gap analysis and library selection","description":"PLAN phase — prerequisite to all implementation work.\n\nDecisions to document:\n- Map new metrics to DORA capabilities: velocity trend → DORA cap #5 (small batches), additions ratio → cap #4 (version control), message quality → cap #1 (clear AI stance)\n- Confirm dependency additions: simple-statistics (stats), @anthropic-ai/sdk (Claude API), compromise (optional NLP)\n- Define new CONFIG keys: AI_ANALYSIS_MAX_COMMITS (default 5), MESSAGE_QUALITY_MIN_WORDS (default 10), VELOCITY_TREND_WINDOW\n- Confirm Node 18+ requirement (needed for @anthropic-ai/sdk; current requirement is ≥14)\n- Decision: use compromise for message classification or pure regex? Regex is zero-dep, compromise is more accurate.","status":"closed","priority":2,"issue_type":"feature","owner":"kjudy@stride.build","created_at":"2026-03-25T18:44:29Z","created_by":"Ken Judy","updated_at":"2026-03-26T14:17:46Z","closed_at":"2026-03-26T14:17:46Z","close_reason":"P1 complete. Library decisions: simple-statistics YES, @anthropic-ai/sdk YES, compromise NO (regex sufficient), simple-git NO. Node 18+ required. 4 new CONFIG keys. All 8 metric definitions finalized. DORA coverage: capabilities 4 and 5 fully covered; 1/2/3/6/7 are acknowledged gaps. Findings documented in metrics-specification.md.","dependency_count":0,"dependent_count":2,"comment_count":0} +{"id":"code-quality-metrics-zy6","title":"Enhanced DORA-Aligned Analytics with Claude API Integration","description":"Extend metrics coverage to align with DORA 2025 AI Capabilities research, add statistical distributions via simple-statistics, and add optional Claude API diff-level commit analysis. Covers both local-code-metrics.js and GitHub workflows. Adds 3 new metrics (velocity trend, additions ratio distribution, message quality), replaces averages with distributions (p50/p90/p95), and integrates Claude API for per-commit AI pattern detection.","status":"closed","priority":2,"issue_type":"epic","owner":"kjudy@stride.build","created_at":"2026-03-25T18:44:14Z","created_by":"Ken Judy","updated_at":"2026-03-26T20:38:30Z","closed_at":"2026-03-26T20:38:30Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"code-quality-metrics-jtn.3","title":"Step 3: Write pure function tests (parseGitLog, isTestFile, generateInsights)","description":"No mocks needed. parseGitLog: empty string, malformed lines, SHA length guard, pipe in message. isTestFile: all 8 TEST_FILE_PATTERNS + production file negatives. generateInsights: all threshold boundaries, AI pattern detection trigger.","status":"closed","priority":2,"issue_type":"task","assignee":"Ken Judy","owner":"kjudy@stride.build","created_at":"2026-03-25T17:07:00Z","created_by":"Ken Judy","updated_at":"2026-03-25T17:16:18Z","closed_at":"2026-03-25T17:16:18Z","close_reason":"41 tests across 4 files. parseGitLog: 8 tests (degenerate, malformed, happy path, pipe-in-message). isTestFile: 14 tests (all 8 patterns + 2 negative cases). generateInsights: 13 tests (all threshold boundaries, AI pattern detection). One test expectation corrected during TDD (insights count was 2, actual 3 — default summary triggers test-first insight too).","dependencies":[{"issue_id":"code-quality-metrics-jtn.3","depends_on_id":"code-quality-metrics-jtn","type":"parent-child","created_at":"2026-03-25T13:06:59Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"code-quality-metrics-jtn.4","title":"Step 4: Write mocked tests for analyzeCommit + collectLocalMetrics","description":"jest.mock child_process and fs. analyzeCommit: binary files, empty stats, large/sprawling thresholds. collectLocalMetrics: early exits (not git repo, no branches, no commits), verify JSON output shape. Spy on process.exit, process.stdout.write, console methods.","status":"closed","priority":2,"issue_type":"task","assignee":"Ken Judy","owner":"kjudy@stride.build","created_at":"2026-03-25T17:07:00Z","created_by":"Ken Judy","updated_at":"2026-03-25T17:38:39Z","closed_at":"2026-03-25T17:38:39Z","close_reason":"12 analyzeCommit tests (null on empty/throw, binary files, test vs prod classification, large/sprawling thresholds, change_ratio, source_branch). 10 collectLocalMetrics tests (4 early exits, successful run with file output shape, warnings/recommendations output paths, \u003e10 commit truncation, dedup). Fixed eslint.config.js to include globals.node. 63 tests total, 95.96% lines, 96.15% functions — all thresholds met.","dependencies":[{"issue_id":"code-quality-metrics-jtn.4","depends_on_id":"code-quality-metrics-jtn","type":"parent-child","created_at":"2026-03-25T13:06:59Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"code-quality-metrics-jtn.1","title":"Step 1: Add package.json, jest.config.js, eslint.config.js","description":"Create package.json with Jest + ESLint devDependencies and test/lint scripts. jest.config.js with node environment, coverage config (80% lines, 90% functions). eslint.config.js with @eslint/js recommended + eslint-plugin-jest for test files.","status":"closed","priority":2,"issue_type":"task","assignee":"Ken Judy","owner":"kjudy@stride.build","created_at":"2026-03-25T17:06:59Z","created_by":"Ken Judy","updated_at":"2026-03-25T17:09:37Z","closed_at":"2026-03-25T17:09:37Z","close_reason":"Created package.json (jest@29, eslint@9, eslint-plugin-jest@28), jest.config.js (node env, 80% line/90% fn coverage thresholds), eslint.config.js (flat config). npm install succeeded, 0 vulnerabilities.","dependencies":[{"issue_id":"code-quality-metrics-jtn.1","depends_on_id":"code-quality-metrics-jtn","type":"parent-child","created_at":"2026-03-25T13:06:58Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"code-quality-metrics-jtn.2","title":"Step 2: Expand exports in local-code-metrics.js + @ts-check","description":"Add // @ts-check at top. Export parseGitLog, isTestFile, analyzeCommit, generateInsights in addition to existing collectLocalMetrics and CONFIG exports.","status":"closed","priority":2,"issue_type":"task","assignee":"Ken Judy","owner":"kjudy@stride.build","created_at":"2026-03-25T17:06:59Z","created_by":"Ken Judy","updated_at":"2026-03-25T17:13:40Z","closed_at":"2026-03-25T17:13:40Z","close_reason":"Added @ts-check, JSDoc typedefs (CommitInfo, CommitStats, CommitMetric), param/return annotations on all 5 functions, instanceof narrowing on 4 catch blocks, typed allCommits/branchCommitCounts. Expanded module.exports to include parseGitLog, isTestFile, analyzeCommit, generateInsights. All 6 export tests green.","dependencies":[{"issue_id":"code-quality-metrics-jtn.2","depends_on_id":"code-quality-metrics-jtn","type":"parent-child","created_at":"2026-03-25T13:06:59Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"code-quality-metrics-jtn","title":"Epic: Add test infrastructure for local-code-metrics.js","description":"Set up Jest test infrastructure with ESLint flat config and JSDoc type checking. Pure functions (parseGitLog, isTestFile, generateInsights) are tested directly; impure functions (runGitCommand, analyzeCommit, collectLocalMetrics) require child_process and fs mocks. Plan phase complete — see conversation for full analysis and file structure decisions.","notes":"## Retrospective\n\n**Session goal:** Add Jest test infrastructure, ESLint flat config, JSDoc type checking, 63 tests for local-code-metrics.js\n\n**What worked:**\n- Pure-function-first test ordering (parseGitLog → isTestFile → generateInsights before mocked tests)\n- mockExecSequence helper pattern for sequential execSync mocking\n- Beads auto-close on child completion gave clean confirmation signal\n- TDD caught generateInsights expectation mismatch immediately (3 insights not 2)\n\n**What to improve:**\n- PLAN underestimated @ts-check remediation: listed as 1-line change, was actually 8 edits + typedef block — scope annotation work explicitly in future PLAN phases\n- Lint not run until all 63 tests written — missing globals.node in ESLint flat config produced 103 errors at the end; should run lint after each step\n\n**Key insights:**\n- ESLint v9 flat config requires explicit globals.node — @eslint/js recommended does not assume Node.js runtime\n- Introducing @ts-check to untyped code is a full step, not a one-liner; cost = N functions * annotation effort\n- Defensive catch blocks wrapping runGitCommand are unreachable because runGitCommand absorbs all git errors — document rather than force coverage\n\n**Action items for next cycle:**\n1. Add lint as a per-step gate in DO checklist, not just a final check\n2. In PLAN, when introducing type system to untyped JS, list annotation work as explicit step with scope estimate\n3. Keep pure-function-first test ordering — it is always the right call","status":"closed","priority":2,"issue_type":"epic","owner":"kjudy@stride.build","created_at":"2026-03-25T17:06:18Z","created_by":"Ken Judy","updated_at":"2026-03-25T17:40:58Z","closed_at":"2026-03-25T17:38:39Z","close_reason":"all steps complete","dependency_count":0,"dependent_count":0,"comment_count":0} diff --git a/__tests__/config.test.js b/__tests__/config.test.js new file mode 100644 index 0000000..c3a8ced --- /dev/null +++ b/__tests__/config.test.js @@ -0,0 +1,21 @@ +'use strict'; + +const { CONFIG } = require('../lib/config'); + +describe('CONFIG duplicate detection defaults', () => { + test('CONFIG.DUPLICATE_MIN_LINES defaults to 5', () => { + expect(CONFIG.DUPLICATE_MIN_LINES).toBe(5); + }); + + test('CONFIG.DUPLICATE_MIN_TOKENS defaults to 50', () => { + expect(CONFIG.DUPLICATE_MIN_TOKENS).toBe(50); + }); + + test('CONFIG.DUPLICATE_IGNORE_PATTERNS is an array', () => { + expect(Array.isArray(CONFIG.DUPLICATE_IGNORE_PATTERNS)).toBe(true); + }); + + test('CONFIG.DUPLICATE_SCAN_PATHS is an array', () => { + expect(Array.isArray(CONFIG.DUPLICATE_SCAN_PATHS)).toBe(true); + }); +}); diff --git a/lib/config.js b/lib/config.js index c4d5973..bb195bf 100644 --- a/lib/config.js +++ b/lib/config.js @@ -12,6 +12,12 @@ const CONFIG = { AI_DIFF_MAX_CHARS: 4000, AI_RISK_ADDITIONS_RATIO: 3, + // Duplicate detection thresholds — customize for your language/framework + DUPLICATE_MIN_LINES: 5, + DUPLICATE_MIN_TOKENS: 50, + DUPLICATE_IGNORE_PATTERNS: [], + DUPLICATE_SCAN_PATHS: [], + // Test file patterns — customize for your language/framework TEST_FILE_PATTERNS: [ /\.(test|spec)\./i, // file.test.js, file.spec.ts From 9bce469500f25dc76d07c675be1ffb5bff112301 Mon Sep 17 00:00:00 2001 From: Ken Judy Date: Tue, 9 Jun 2026 11:29:52 -0400 Subject: [PATCH 2/9] feat: implement runDuplicateCheck in lib/duplicate.js MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds Layer 1 static duplicate detection: runs jscpd via child_process, reads the JSON report it writes to a temp dir, and returns a structured array of {firstFile, secondFile, lines, tokens}. Returns empty array on empty input, missing report file, or non-zero jscpd exit — no throws. Co-Authored-By: Claude Sonnet 4.6 --- .beads/issues.jsonl | 4 +-- __tests__/duplicate.test.js | 51 +++++++++++++++++++++++++++++++++++++ lib/duplicate.js | 46 +++++++++++++++++++++++++++++++++ 3 files changed, 99 insertions(+), 2 deletions(-) create mode 100644 __tests__/duplicate.test.js create mode 100644 lib/duplicate.js diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 1dbcace..b284545 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -1,8 +1,8 @@ -{"id":"code-quality-metrics-20o","title":"feat: implement runDuplicateCheck in lib/duplicate.js","description":"Create lib/duplicate.js with runDuplicateCheck(filePaths) function. Runs jscpd via child_process using CONFIG values, reads the JSON report it writes to CONFIG.DUPLICATE_OUTPUT_DIR (a temp path), parses it, and returns a structured array.\n\nRETURN SHAPE (mirrors jscpd JSON output directly):\n Array\u003c{\n firstFile: { name: string, start: number, end: number },\n secondFile: { name: string, start: number, end: number },\n lines: number,\n tokens: number\n }\u003e\n\nJSCPD INVOCATION:\n jscpd --min-lines CONFIG.DUPLICATE_MIN_LINES --min-tokens CONFIG.DUPLICATE_MIN_TOKENS\n --ignore-pattern CONFIG.DUPLICATE_IGNORE_PATTERNS\n --reporters json --output CONFIG.DUPLICATE_OUTPUT_DIR\n [filePaths...]\n\nMOCK STRATEGY: mock execSync (child_process) for the jscpd call; mock fs.readFileSync for reading the output JSON; mock fs.existsSync to control whether the output file exists.\n\nBEHAVIORAL TEST LIST (one RED test before each GREEN):\n1. 'runDuplicateCheck returns empty array when filePaths is empty without calling jscpd'\n Predicted failure: received undefined or thrown error\n2. 'runDuplicateCheck returns empty array when jscpd output file does not exist'\n Predicted failure: received undefined or fs.readFileSync called unexpectedly\n3. 'runDuplicateCheck parses jscpd JSON and returns firstFile/secondFile/lines/tokens for each duplicate'\n Predicted failure: received [] (not yet parsing)\n4. 'runDuplicateCheck returns empty array when jscpd exits non-zero'\n Predicted failure: thrown error propagated\n\nDefinition of done: all 4 tests green, no stubs, exports runDuplicateCheck from lib/duplicate.js.","status":"open","priority":1,"issue_type":"task","owner":"kjudy@stride.build","created_at":"2026-06-09T14:20:40Z","created_by":"Ken Judy","updated_at":"2026-06-09T14:58:31Z","dependencies":[{"issue_id":"code-quality-metrics-20o","depends_on_id":"code-quality-metrics-een","type":"blocks","created_at":"2026-06-09T10:20:50Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":1,"dependent_count":2,"comment_count":0} +{"id":"code-quality-metrics-20o","title":"feat: implement runDuplicateCheck in lib/duplicate.js","description":"Create lib/duplicate.js with runDuplicateCheck(filePaths) function. Runs jscpd via child_process using CONFIG values, reads the JSON report it writes to CONFIG.DUPLICATE_OUTPUT_DIR (a temp path), parses it, and returns a structured array.\n\nRETURN SHAPE (mirrors jscpd JSON output directly):\n Array\u003c{\n firstFile: { name: string, start: number, end: number },\n secondFile: { name: string, start: number, end: number },\n lines: number,\n tokens: number\n }\u003e\n\nJSCPD INVOCATION:\n jscpd --min-lines CONFIG.DUPLICATE_MIN_LINES --min-tokens CONFIG.DUPLICATE_MIN_TOKENS\n --ignore-pattern CONFIG.DUPLICATE_IGNORE_PATTERNS\n --reporters json --output CONFIG.DUPLICATE_OUTPUT_DIR\n [filePaths...]\n\nMOCK STRATEGY: mock execSync (child_process) for the jscpd call; mock fs.readFileSync for reading the output JSON; mock fs.existsSync to control whether the output file exists.\n\nBEHAVIORAL TEST LIST (one RED test before each GREEN):\n1. 'runDuplicateCheck returns empty array when filePaths is empty without calling jscpd'\n Predicted failure: received undefined or thrown error\n2. 'runDuplicateCheck returns empty array when jscpd output file does not exist'\n Predicted failure: received undefined or fs.readFileSync called unexpectedly\n3. 'runDuplicateCheck parses jscpd JSON and returns firstFile/secondFile/lines/tokens for each duplicate'\n Predicted failure: received [] (not yet parsing)\n4. 'runDuplicateCheck returns empty array when jscpd exits non-zero'\n Predicted failure: thrown error propagated\n\nDefinition of done: all 4 tests green, no stubs, exports runDuplicateCheck from lib/duplicate.js.","notes":"TDD ordering: test 3 (happy path parse) before test 1 (empty input). Reason: stub return [] makes tests 1, 2, 4 vacuously green — test 3 is the first genuine RED.","status":"in_progress","priority":1,"issue_type":"task","assignee":"Ken Judy","owner":"kjudy@stride.build","created_at":"2026-06-09T14:20:40Z","created_by":"Ken Judy","updated_at":"2026-06-09T15:17:16Z","started_at":"2026-06-09T15:17:16Z","dependencies":[{"issue_id":"code-quality-metrics-20o","depends_on_id":"code-quality-metrics-een","type":"blocks","created_at":"2026-06-09T10:20:50Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":1,"dependent_count":2,"comment_count":0} {"id":"code-quality-metrics-5o3","title":"feat: integrate jscpd findings into pr-metrics.yml PR comment","description":"Require lib/duplicate.js in pr-metrics.yml. Run runDuplicateCheck against the already-computed prodFiles array (map to filenames). Add a Duplication section to the PR comment when findings exist; omit the section silently when findings array is empty.\n\nPR COMMENT SECTION FORMAT (exact strings matter for CHECK):\n ## Duplicate Code\n Found N duplicate block(s) across the changed files:\n - `path/to/file.js:10-25` duplicates `path/to/other.js:5-20` (15 lines, 120 tokens)\n (one bullet per finding, capped at 10 — add 'and N more...' if truncated)\n\nWhen no findings: section is omitted entirely (no 'No duplicates found' line).\n\nVERIFICATION (no unit tests for YAML): grep audit confirms\n - require('./lib/duplicate') appears in the script block\n - runDuplicateCheck is called with prodFiles.map(f =\u003e f.filename)\n - '## Duplicate Code' header string is present\n - commentParts.filter(Boolean) already handles optional sections (no extra guard needed)\n\nDefinition of done: grep audit passes, PR comment renders section only when findings exist.","status":"open","priority":1,"issue_type":"task","owner":"kjudy@stride.build","created_at":"2026-06-09T14:20:40Z","created_by":"Ken Judy","updated_at":"2026-06-09T14:58:55Z","dependencies":[{"issue_id":"code-quality-metrics-5o3","depends_on_id":"code-quality-metrics-20o","type":"blocks","created_at":"2026-06-09T10:20:50Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} {"id":"code-quality-metrics-729","title":"feat: integrate semantic findings into pr-metrics.yml","description":"Extend pr-metrics.yml Duplication section to include Layer 2 semantic findings when ANTHROPIC_API_KEY is present. Degrade gracefully when key is absent — consistent with local-code-metrics.js pattern.\n\nLAYER INDICATOR STRINGS (exact, appended to Duplication section footer):\n When both layers ran:\n '_Layer 1 (static) and Layer 2 (semantic) ran._'\n When Layer 1 only (no API key):\n '_Layer 1 (static) ran. Set ANTHROPIC_API_KEY to enable semantic analysis._'\n\nSEMANTIC FINDINGS FORMAT (appended after static findings list):\n ### Semantic Similarity\n - `file1.js` / `file2.js`: similarity description (confidence: high)\n\nWhen semantic layer returns no findings: omit the Semantic Similarity subsection, still show layer indicator.\n\nVERIFICATION: grep audit confirms both indicator strings are present in the YAML; check conditional on ANTHROPIC_API_KEY mirrors existing pattern in the script block.\n\nDefinition of done: grep audit passes, both indicator strings are present, Layer 2 block is guarded by API key check.","status":"open","priority":1,"issue_type":"task","owner":"kjudy@stride.build","created_at":"2026-06-09T14:20:40Z","created_by":"Ken Judy","updated_at":"2026-06-09T14:59:30Z","dependencies":[{"issue_id":"code-quality-metrics-729","depends_on_id":"code-quality-metrics-5o3","type":"blocks","created_at":"2026-06-09T10:20:50Z","created_by":"Ken Judy","metadata":"{}"},{"issue_id":"code-quality-metrics-729","depends_on_id":"code-quality-metrics-790","type":"blocks","created_at":"2026-06-09T10:20:50Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":2,"dependent_count":1,"comment_count":0} {"id":"code-quality-metrics-790","title":"feat: add semantic duplicate detection via lib/claude.js","description":"Add analyzeDuplicatesWithClaude(client, filePaths, staticFindings) to lib/claude.js (same file as existing analyzeCommitWithClaude — consistent module boundary, no new file).\n\nRETURN SCHEMA:\n Array\u003c{\n file1: string, // relative path\n file2: string, // relative path\n similarity: string, // one-sentence description of what is similar\n concern: string, // why this matters (e.g. 'same error-handling pattern repeated')\n confidence: 'high' | 'medium' | 'low'\n }\u003e\n\nSYSTEM PROMPT INTENT: Given a set of changed files and their neighbors, identify semantically similar functions or logic blocks that suggest copy-paste or AI-generated duplication — even when variable names or formatting differ. Static analysis has already flagged token-level matches (passed as staticFindings); the agent focuses on structural/conceptual similarity that jscpd would miss.\n\nBEHAVIORAL TEST LIST:\n1. 'analyzeDuplicatesWithClaude returns empty array when client is null'\n Predicted failure: throws TypeError (null client not guarded)\n2. 'analyzeDuplicatesWithClaude constructs prompt including file contents and staticFindings count'\n Predicted failure: prompt does not mention staticFindings (not yet wired)\n3. 'analyzeDuplicatesWithClaude parses structured response into file1/file2/similarity/concern/confidence'\n Predicted failure: returns raw string instead of array\n4. 'analyzeDuplicatesWithClaude returns empty array when API response is malformed'\n Predicted failure: throws JSON parse error\n\nMOCK STRATEGY: mock Anthropic client.messages.create; return fixture JSON matching return schema.\n\nDefinition of done: all 4 tests green, function exported from lib/claude.js.","status":"open","priority":1,"issue_type":"task","owner":"kjudy@stride.build","created_at":"2026-06-09T14:20:40Z","created_by":"Ken Judy","updated_at":"2026-06-09T14:59:17Z","dependencies":[{"issue_id":"code-quality-metrics-790","depends_on_id":"code-quality-metrics-uhs","type":"blocks","created_at":"2026-06-09T10:20:50Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} -{"id":"code-quality-metrics-een","title":"refactor: add duplicate detection config keys to lib/config.js","description":"Add four keys to the CONFIG object in lib/config.js:\n DUPLICATE_MIN_LINES: 5 (number)\n DUPLICATE_MIN_TOKENS: 50 (number)\n DUPLICATE_IGNORE_PATTERNS: [] (array of glob strings — caller populates for their repo; TEST_FILE_PATTERNS provides the right starting reference but is not hard-coded here)\n DUPLICATE_SCAN_PATHS: [] (array of strings — empty means scan all provided file paths)\n\nNo behavioral change to any existing function.\n\nBEHAVIORAL TEST LIST (name + predicted failure):\n1. 'CONFIG.DUPLICATE_MIN_LINES defaults to 5'\n Predicted failure: received undefined (key missing)\n2. 'CONFIG.DUPLICATE_MIN_TOKENS defaults to 50'\n Predicted failure: received undefined (key missing)\n3. 'CONFIG.DUPLICATE_IGNORE_PATTERNS is an array'\n Predicted failure: received undefined (key missing)\n4. 'CONFIG.DUPLICATE_SCAN_PATHS is an array'\n Predicted failure: received undefined (key missing)\n\nDefinition of done: all 4 tests green; npm test passes with no threshold changes; no other file modified.","status":"open","priority":1,"issue_type":"task","owner":"kjudy@stride.build","created_at":"2026-06-09T14:20:40Z","created_by":"Ken Judy","updated_at":"2026-06-09T15:00:05Z","dependency_count":0,"dependent_count":2,"comment_count":0} +{"id":"code-quality-metrics-een","title":"refactor: add duplicate detection config keys to lib/config.js","description":"Add four keys to the CONFIG object in lib/config.js:\n DUPLICATE_MIN_LINES: 5 (number)\n DUPLICATE_MIN_TOKENS: 50 (number)\n DUPLICATE_IGNORE_PATTERNS: [] (array of glob strings — caller populates for their repo; TEST_FILE_PATTERNS provides the right starting reference but is not hard-coded here)\n DUPLICATE_SCAN_PATHS: [] (array of strings — empty means scan all provided file paths)\n\nNo behavioral change to any existing function.\n\nBEHAVIORAL TEST LIST (name + predicted failure):\n1. 'CONFIG.DUPLICATE_MIN_LINES defaults to 5'\n Predicted failure: received undefined (key missing)\n2. 'CONFIG.DUPLICATE_MIN_TOKENS defaults to 50'\n Predicted failure: received undefined (key missing)\n3. 'CONFIG.DUPLICATE_IGNORE_PATTERNS is an array'\n Predicted failure: received undefined (key missing)\n4. 'CONFIG.DUPLICATE_SCAN_PATHS is an array'\n Predicted failure: received undefined (key missing)\n\nDefinition of done: all 4 tests green; npm test passes with no threshold changes; no other file modified.","status":"closed","priority":1,"issue_type":"task","assignee":"Ken Judy","owner":"kjudy@stride.build","created_at":"2026-06-09T14:20:40Z","created_by":"Ken Judy","updated_at":"2026-06-09T15:15:13Z","started_at":"2026-06-09T15:06:36Z","closed_at":"2026-06-09T15:15:13Z","close_reason":"Closed","dependency_count":0,"dependent_count":2,"comment_count":0} {"id":"code-quality-metrics-uhs","title":"feat: implement resolveModuleNeighbors in lib/duplicate.js","description":"Add resolveModuleNeighbors(changedFilePaths) to lib/duplicate.js. Parses require()/import statements in each changed file using regex, resolves relative paths to absolute using path.resolve(path.dirname(sourceFile), importPath), appends .js if no extension, returns the union of changed files plus their direct (not transitive) local imports.\n\nEDGE CASE DECISIONS (made now, not during implementation):\n- Non-JS files (e.g. .yml, .json, .md): include in the returned set as-is, skip import parsing\n- Files that do not exist on disk (deleted in PR): skip with fs.existsSync check, do not throw\n- External/node_modules imports (no leading './'): ignore, do not resolve\n\nBEHAVIORAL TEST LIST:\n1. 'resolveModuleNeighbors returns changed files plus resolved local imports'\n Predicted failure: returns only the input files (import parsing not yet implemented)\n2. 'resolveModuleNeighbors skips import resolution for non-JS files'\n Predicted failure: throws or returns unexpected paths for .yml input\n3. 'resolveModuleNeighbors skips files that do not exist on disk'\n Predicted failure: throws ENOENT or returns broken paths\n4. 'resolveModuleNeighbors ignores external/node_modules imports'\n Predicted failure: includes node_modules paths or throws on resolution\n\nMOCK STRATEGY: mock fs.readFileSync for file contents; mock fs.existsSync for on-disk check.\n\nDefinition of done: all 4 tests green, function exported from lib/duplicate.js alongside runDuplicateCheck.","status":"open","priority":1,"issue_type":"task","owner":"kjudy@stride.build","created_at":"2026-06-09T14:20:40Z","created_by":"Ken Judy","updated_at":"2026-06-09T14:59:06Z","dependencies":[{"issue_id":"code-quality-metrics-uhs","depends_on_id":"code-quality-metrics-20o","type":"blocks","created_at":"2026-06-09T10:20:50Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} {"id":"code-quality-metrics-aat","title":"Feature: Two-layer duplicate code detection in PR check (#42)","description":"Add jscpd static detection (Layer 1, always-on) and agent-based semantic detection (Layer 2, conditional on ANTHROPIC_API_KEY) to pr-metrics.yml. All thresholds and ignore patterns live in lib/config.js as documented configurable parameters so the toolkit works across repos with different test conventions and directory structures.","notes":"Analysis: lib/config.js is single source of truth for all thresholds. lib/duplicate.js follows PR #33 pattern (require() in workflow). jscpd v5 uses --ignore-pattern flag, JSON output via --reporters json. Baseline: 0 duplicates in this repo production code at defaults. Other repos need to override DUPLICATE_IGNORE_PATTERNS. Layer 2 conditional on ANTHROPIC_API_KEY. Deliver Layer 1 first.","status":"open","priority":1,"issue_type":"epic","owner":"kjudy@stride.build","created_at":"2026-06-09T14:19:59Z","created_by":"Ken Judy","updated_at":"2026-06-09T14:20:18Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"code-quality-metrics-141","title":"Update churn distribution in local-code-metrics.js","description":"TDD: update collectLocalMetrics.test.js first to expect churn_intensity_median and churn_intensity_p90 instead of additions_ratio_median and additions_ratio_p90, then implement. Replace local-code-metrics.js:181 ratio array with: metrics.map(m =\u003e Math.min(m.total_additions, m.total_deletions) / Math.max(m.total_additions + m.total_deletions, 1)). Rename summary fields from additions_ratio_median/additions_ratio_p90 to churn_intensity_median/churn_intensity_p90.","status":"closed","priority":1,"issue_type":"task","assignee":"Ken Judy","owner":"kjudy@stride.build","created_at":"2026-06-08T20:36:14Z","created_by":"Ken Judy","updated_at":"2026-06-08T20:44:27Z","started_at":"2026-06-08T20:43:05Z","closed_at":"2026-06-08T20:44:27Z","close_reason":"Closed","dependencies":[{"issue_id":"code-quality-metrics-141","depends_on_id":"code-quality-metrics-4ck","type":"blocks","created_at":"2026-06-08T16:36:27Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":1,"dependent_count":3,"comment_count":0} diff --git a/__tests__/duplicate.test.js b/__tests__/duplicate.test.js new file mode 100644 index 0000000..580a82d --- /dev/null +++ b/__tests__/duplicate.test.js @@ -0,0 +1,51 @@ +'use strict'; + +jest.mock('child_process'); +jest.mock('fs'); + +const { execSync } = require('child_process'); +const fs = require('fs'); +const { runDuplicateCheck } = require('../lib/duplicate'); + +const FIXTURE_DUPLICATE = { + firstFile: { name: 'src/lib/git.js', start: 10, end: 25 }, + secondFile: { name: 'src/lib/metrics.js', start: 5, end: 20 }, + lines: 15, + tokens: 120 +}; + +beforeEach(() => { + jest.clearAllMocks(); + execSync.mockReturnValue(''); + fs.existsSync.mockReturnValue(true); + fs.readFileSync.mockReturnValue(JSON.stringify({ duplicates: [FIXTURE_DUPLICATE] })); +}); + +describe('runDuplicateCheck', () => { + test('returns empty array when filePaths is empty without calling jscpd', () => { + const result = runDuplicateCheck([]); + expect(result).toEqual([]); + expect(execSync).not.toHaveBeenCalled(); + }); + + test('returns empty array when jscpd output file does not exist', () => { + fs.existsSync.mockReturnValue(false); + const result = runDuplicateCheck(['src/lib/git.js']); + expect(result).toEqual([]); + }); + + test('parses jscpd JSON and returns firstFile/secondFile/lines/tokens for each duplicate', () => { + const result = runDuplicateCheck(['src/lib/git.js', 'src/lib/metrics.js']); + expect(result).toHaveLength(1); + expect(result[0].firstFile).toEqual(FIXTURE_DUPLICATE.firstFile); + expect(result[0].secondFile).toEqual(FIXTURE_DUPLICATE.secondFile); + expect(result[0].lines).toBe(15); + expect(result[0].tokens).toBe(120); + }); + + test('returns empty array when jscpd exits non-zero', () => { + execSync.mockImplementation(() => { throw new Error('exit code 1'); }); + const result = runDuplicateCheck(['src/lib/git.js']); + expect(result).toEqual([]); + }); +}); diff --git a/lib/duplicate.js b/lib/duplicate.js new file mode 100644 index 0000000..6bacf77 --- /dev/null +++ b/lib/duplicate.js @@ -0,0 +1,46 @@ +// @ts-nocheck +'use strict'; + +const { execSync } = require('child_process'); +const fs = require('fs'); +const path = require('path'); +const os = require('os'); +const { CONFIG } = require('./config'); + +const JSCPD_OUTPUT_DIR = path.join(os.tmpdir(), 'jscpd-output'); +const JSCPD_REPORT_FILE = path.join(JSCPD_OUTPUT_DIR, 'jscpd-report.json'); + +function runDuplicateCheck(filePaths) { + if (!filePaths || filePaths.length === 0) return []; + + const ignoreArg = CONFIG.DUPLICATE_IGNORE_PATTERNS.length > 0 + ? `--ignore-pattern "${CONFIG.DUPLICATE_IGNORE_PATTERNS.join(',')}"` : ''; + + const cmd = [ + 'npx jscpd', + `--min-lines ${CONFIG.DUPLICATE_MIN_LINES}`, + `--min-tokens ${CONFIG.DUPLICATE_MIN_TOKENS}`, + ignoreArg, + '--reporters json', + `--output "${JSCPD_OUTPUT_DIR}"`, + ...filePaths.map(f => `"${f}"`) + ].filter(Boolean).join(' '); + + try { + execSync(cmd, { stdio: 'pipe' }); + } catch { + return []; + } + + if (!fs.existsSync(JSCPD_REPORT_FILE)) return []; + + const report = JSON.parse(fs.readFileSync(JSCPD_REPORT_FILE, 'utf8')); + return (report.duplicates || []).map(d => ({ + firstFile: d.firstFile, + secondFile: d.secondFile, + lines: d.lines, + tokens: d.tokens + })); +} + +module.exports = { runDuplicateCheck }; From ebc90633b790c7131f30687aab7b65f824fc5bc9 Mon Sep 17 00:00:00 2001 From: Ken Judy Date: Fri, 12 Jun 2026 10:25:40 -0400 Subject: [PATCH 3/9] feat: implement resolveModuleNeighbors in lib/duplicate.js Parses require()/import statements in JS/TS files, resolves relative paths to absolute, and returns the union of changed files plus their direct local imports. Non-JS files are passed through without parsing; files not on disk and external/node_modules imports are skipped. Co-Authored-By: Claude Sonnet 4.6 --- .beads/interactions.jsonl | 2 ++ .beads/issues.jsonl | 2 +- __tests__/duplicate.test.js | 51 ++++++++++++++++++++++++++++++++++++- lib/duplicate.js | 29 ++++++++++++++++++++- 4 files changed, 81 insertions(+), 3 deletions(-) diff --git a/.beads/interactions.jsonl b/.beads/interactions.jsonl index 93869f1..6b428d8 100644 --- a/.beads/interactions.jsonl +++ b/.beads/interactions.jsonl @@ -22,3 +22,5 @@ {"id":"int-67abf2c2","kind":"field_change","created_at":"2026-04-07T15:19:31.62249Z","actor":"Ken Judy","issue_id":"code-quality-metrics-38c","extra":{"field":"status","new_value":"closed","old_value":"in_progress"}} {"id":"int-1177d69a","kind":"field_change","created_at":"2026-04-07T15:23:05.721325Z","actor":"Ken Judy","issue_id":"code-quality-metrics-moo","extra":{"field":"status","new_value":"closed","old_value":"in_progress"}} {"id":"int-ac0d33c7","kind":"field_change","created_at":"2026-04-07T15:23:06.286044Z","actor":"Ken Judy","issue_id":"code-quality-metrics-cek","extra":{"field":"status","new_value":"closed","old_value":"in_progress"}} +{"id":"int-938d7ec1","kind":"field_change","created_at":"2026-06-09T15:15:13.208542Z","actor":"Ken Judy","issue_id":"code-quality-metrics-een","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}} +{"id":"int-0076bf03","kind":"field_change","created_at":"2026-06-09T15:30:02.111356Z","actor":"Ken Judy","issue_id":"code-quality-metrics-20o","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}} diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index b284545..1183142 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -1,4 +1,4 @@ -{"id":"code-quality-metrics-20o","title":"feat: implement runDuplicateCheck in lib/duplicate.js","description":"Create lib/duplicate.js with runDuplicateCheck(filePaths) function. Runs jscpd via child_process using CONFIG values, reads the JSON report it writes to CONFIG.DUPLICATE_OUTPUT_DIR (a temp path), parses it, and returns a structured array.\n\nRETURN SHAPE (mirrors jscpd JSON output directly):\n Array\u003c{\n firstFile: { name: string, start: number, end: number },\n secondFile: { name: string, start: number, end: number },\n lines: number,\n tokens: number\n }\u003e\n\nJSCPD INVOCATION:\n jscpd --min-lines CONFIG.DUPLICATE_MIN_LINES --min-tokens CONFIG.DUPLICATE_MIN_TOKENS\n --ignore-pattern CONFIG.DUPLICATE_IGNORE_PATTERNS\n --reporters json --output CONFIG.DUPLICATE_OUTPUT_DIR\n [filePaths...]\n\nMOCK STRATEGY: mock execSync (child_process) for the jscpd call; mock fs.readFileSync for reading the output JSON; mock fs.existsSync to control whether the output file exists.\n\nBEHAVIORAL TEST LIST (one RED test before each GREEN):\n1. 'runDuplicateCheck returns empty array when filePaths is empty without calling jscpd'\n Predicted failure: received undefined or thrown error\n2. 'runDuplicateCheck returns empty array when jscpd output file does not exist'\n Predicted failure: received undefined or fs.readFileSync called unexpectedly\n3. 'runDuplicateCheck parses jscpd JSON and returns firstFile/secondFile/lines/tokens for each duplicate'\n Predicted failure: received [] (not yet parsing)\n4. 'runDuplicateCheck returns empty array when jscpd exits non-zero'\n Predicted failure: thrown error propagated\n\nDefinition of done: all 4 tests green, no stubs, exports runDuplicateCheck from lib/duplicate.js.","notes":"TDD ordering: test 3 (happy path parse) before test 1 (empty input). Reason: stub return [] makes tests 1, 2, 4 vacuously green — test 3 is the first genuine RED.","status":"in_progress","priority":1,"issue_type":"task","assignee":"Ken Judy","owner":"kjudy@stride.build","created_at":"2026-06-09T14:20:40Z","created_by":"Ken Judy","updated_at":"2026-06-09T15:17:16Z","started_at":"2026-06-09T15:17:16Z","dependencies":[{"issue_id":"code-quality-metrics-20o","depends_on_id":"code-quality-metrics-een","type":"blocks","created_at":"2026-06-09T10:20:50Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":1,"dependent_count":2,"comment_count":0} +{"id":"code-quality-metrics-20o","title":"feat: implement runDuplicateCheck in lib/duplicate.js","description":"Create lib/duplicate.js with runDuplicateCheck(filePaths) function. Runs jscpd via child_process using CONFIG values, reads the JSON report it writes to CONFIG.DUPLICATE_OUTPUT_DIR (a temp path), parses it, and returns a structured array.\n\nRETURN SHAPE (mirrors jscpd JSON output directly):\n Array\u003c{\n firstFile: { name: string, start: number, end: number },\n secondFile: { name: string, start: number, end: number },\n lines: number,\n tokens: number\n }\u003e\n\nJSCPD INVOCATION:\n jscpd --min-lines CONFIG.DUPLICATE_MIN_LINES --min-tokens CONFIG.DUPLICATE_MIN_TOKENS\n --ignore-pattern CONFIG.DUPLICATE_IGNORE_PATTERNS\n --reporters json --output CONFIG.DUPLICATE_OUTPUT_DIR\n [filePaths...]\n\nMOCK STRATEGY: mock execSync (child_process) for the jscpd call; mock fs.readFileSync for reading the output JSON; mock fs.existsSync to control whether the output file exists.\n\nBEHAVIORAL TEST LIST (one RED test before each GREEN):\n1. 'runDuplicateCheck returns empty array when filePaths is empty without calling jscpd'\n Predicted failure: received undefined or thrown error\n2. 'runDuplicateCheck returns empty array when jscpd output file does not exist'\n Predicted failure: received undefined or fs.readFileSync called unexpectedly\n3. 'runDuplicateCheck parses jscpd JSON and returns firstFile/secondFile/lines/tokens for each duplicate'\n Predicted failure: received [] (not yet parsing)\n4. 'runDuplicateCheck returns empty array when jscpd exits non-zero'\n Predicted failure: thrown error propagated\n\nDefinition of done: all 4 tests green, no stubs, exports runDuplicateCheck from lib/duplicate.js.","notes":"TDD ordering: test 3 (happy path parse) before test 1 (empty input). Reason: stub return [] makes tests 1, 2, 4 vacuously green — test 3 is the first genuine RED.","status":"closed","priority":1,"issue_type":"task","assignee":"Ken Judy","owner":"kjudy@stride.build","created_at":"2026-06-09T14:20:40Z","created_by":"Ken Judy","updated_at":"2026-06-09T15:30:02Z","started_at":"2026-06-09T15:17:16Z","closed_at":"2026-06-09T15:30:02Z","close_reason":"Closed","dependencies":[{"issue_id":"code-quality-metrics-20o","depends_on_id":"code-quality-metrics-een","type":"blocks","created_at":"2026-06-09T10:20:50Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":1,"dependent_count":2,"comment_count":0} {"id":"code-quality-metrics-5o3","title":"feat: integrate jscpd findings into pr-metrics.yml PR comment","description":"Require lib/duplicate.js in pr-metrics.yml. Run runDuplicateCheck against the already-computed prodFiles array (map to filenames). Add a Duplication section to the PR comment when findings exist; omit the section silently when findings array is empty.\n\nPR COMMENT SECTION FORMAT (exact strings matter for CHECK):\n ## Duplicate Code\n Found N duplicate block(s) across the changed files:\n - `path/to/file.js:10-25` duplicates `path/to/other.js:5-20` (15 lines, 120 tokens)\n (one bullet per finding, capped at 10 — add 'and N more...' if truncated)\n\nWhen no findings: section is omitted entirely (no 'No duplicates found' line).\n\nVERIFICATION (no unit tests for YAML): grep audit confirms\n - require('./lib/duplicate') appears in the script block\n - runDuplicateCheck is called with prodFiles.map(f =\u003e f.filename)\n - '## Duplicate Code' header string is present\n - commentParts.filter(Boolean) already handles optional sections (no extra guard needed)\n\nDefinition of done: grep audit passes, PR comment renders section only when findings exist.","status":"open","priority":1,"issue_type":"task","owner":"kjudy@stride.build","created_at":"2026-06-09T14:20:40Z","created_by":"Ken Judy","updated_at":"2026-06-09T14:58:55Z","dependencies":[{"issue_id":"code-quality-metrics-5o3","depends_on_id":"code-quality-metrics-20o","type":"blocks","created_at":"2026-06-09T10:20:50Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} {"id":"code-quality-metrics-729","title":"feat: integrate semantic findings into pr-metrics.yml","description":"Extend pr-metrics.yml Duplication section to include Layer 2 semantic findings when ANTHROPIC_API_KEY is present. Degrade gracefully when key is absent — consistent with local-code-metrics.js pattern.\n\nLAYER INDICATOR STRINGS (exact, appended to Duplication section footer):\n When both layers ran:\n '_Layer 1 (static) and Layer 2 (semantic) ran._'\n When Layer 1 only (no API key):\n '_Layer 1 (static) ran. Set ANTHROPIC_API_KEY to enable semantic analysis._'\n\nSEMANTIC FINDINGS FORMAT (appended after static findings list):\n ### Semantic Similarity\n - `file1.js` / `file2.js`: similarity description (confidence: high)\n\nWhen semantic layer returns no findings: omit the Semantic Similarity subsection, still show layer indicator.\n\nVERIFICATION: grep audit confirms both indicator strings are present in the YAML; check conditional on ANTHROPIC_API_KEY mirrors existing pattern in the script block.\n\nDefinition of done: grep audit passes, both indicator strings are present, Layer 2 block is guarded by API key check.","status":"open","priority":1,"issue_type":"task","owner":"kjudy@stride.build","created_at":"2026-06-09T14:20:40Z","created_by":"Ken Judy","updated_at":"2026-06-09T14:59:30Z","dependencies":[{"issue_id":"code-quality-metrics-729","depends_on_id":"code-quality-metrics-5o3","type":"blocks","created_at":"2026-06-09T10:20:50Z","created_by":"Ken Judy","metadata":"{}"},{"issue_id":"code-quality-metrics-729","depends_on_id":"code-quality-metrics-790","type":"blocks","created_at":"2026-06-09T10:20:50Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":2,"dependent_count":1,"comment_count":0} {"id":"code-quality-metrics-790","title":"feat: add semantic duplicate detection via lib/claude.js","description":"Add analyzeDuplicatesWithClaude(client, filePaths, staticFindings) to lib/claude.js (same file as existing analyzeCommitWithClaude — consistent module boundary, no new file).\n\nRETURN SCHEMA:\n Array\u003c{\n file1: string, // relative path\n file2: string, // relative path\n similarity: string, // one-sentence description of what is similar\n concern: string, // why this matters (e.g. 'same error-handling pattern repeated')\n confidence: 'high' | 'medium' | 'low'\n }\u003e\n\nSYSTEM PROMPT INTENT: Given a set of changed files and their neighbors, identify semantically similar functions or logic blocks that suggest copy-paste or AI-generated duplication — even when variable names or formatting differ. Static analysis has already flagged token-level matches (passed as staticFindings); the agent focuses on structural/conceptual similarity that jscpd would miss.\n\nBEHAVIORAL TEST LIST:\n1. 'analyzeDuplicatesWithClaude returns empty array when client is null'\n Predicted failure: throws TypeError (null client not guarded)\n2. 'analyzeDuplicatesWithClaude constructs prompt including file contents and staticFindings count'\n Predicted failure: prompt does not mention staticFindings (not yet wired)\n3. 'analyzeDuplicatesWithClaude parses structured response into file1/file2/similarity/concern/confidence'\n Predicted failure: returns raw string instead of array\n4. 'analyzeDuplicatesWithClaude returns empty array when API response is malformed'\n Predicted failure: throws JSON parse error\n\nMOCK STRATEGY: mock Anthropic client.messages.create; return fixture JSON matching return schema.\n\nDefinition of done: all 4 tests green, function exported from lib/claude.js.","status":"open","priority":1,"issue_type":"task","owner":"kjudy@stride.build","created_at":"2026-06-09T14:20:40Z","created_by":"Ken Judy","updated_at":"2026-06-09T14:59:17Z","dependencies":[{"issue_id":"code-quality-metrics-790","depends_on_id":"code-quality-metrics-uhs","type":"blocks","created_at":"2026-06-09T10:20:50Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} diff --git a/__tests__/duplicate.test.js b/__tests__/duplicate.test.js index 580a82d..668d1c9 100644 --- a/__tests__/duplicate.test.js +++ b/__tests__/duplicate.test.js @@ -5,7 +5,7 @@ jest.mock('fs'); const { execSync } = require('child_process'); const fs = require('fs'); -const { runDuplicateCheck } = require('../lib/duplicate'); +const { runDuplicateCheck, resolveModuleNeighbors } = require('../lib/duplicate'); const FIXTURE_DUPLICATE = { firstFile: { name: 'src/lib/git.js', start: 10, end: 25 }, @@ -49,3 +49,52 @@ describe('runDuplicateCheck', () => { expect(result).toEqual([]); }); }); + +describe('resolveModuleNeighbors', () => { + beforeEach(() => { + jest.clearAllMocks(); + fs.existsSync.mockReturnValue(true); + }); + + test('returns only the input files when they have no local imports', () => { + fs.readFileSync.mockReturnValue('const x = 1;'); + const input = ['/project/src/lib/git.js']; + const result = resolveModuleNeighbors(input); + expect(result).toContain('/project/src/lib/git.js'); + expect(result).toHaveLength(1); + }); + + test('returns changed files plus resolved local imports', () => { + fs.readFileSync.mockReturnValue("const { CONFIG } = require('./config');"); + const input = ['/project/src/lib/git.js']; + const result = resolveModuleNeighbors(input); + expect(result).toContain('/project/src/lib/git.js'); + expect(result).toContain('/project/src/lib/config.js'); + expect(result).toHaveLength(2); + }); + + test('skips import resolution for non-JS files and includes them as-is', () => { + const input = ['/project/.github/workflows/pr-metrics.yml']; + const result = resolveModuleNeighbors(input); + expect(result).toContain('/project/.github/workflows/pr-metrics.yml'); + expect(fs.readFileSync).not.toHaveBeenCalled(); + }); + + test('skips files that do not exist on disk', () => { + fs.existsSync.mockReturnValue(false); + const input = ['/project/src/deleted.js']; + const result = resolveModuleNeighbors(input); + expect(result).toEqual([]); + }); + + test('ignores external and node_modules imports', () => { + fs.readFileSync.mockReturnValue( + "const fs = require('fs');\nconst x = require('lodash');\nconst y = require('./local');" + ); + const input = ['/project/src/lib/git.js']; + const result = resolveModuleNeighbors(input); + expect(result).toContain('/project/src/lib/local.js'); + expect(result.some(p => p.includes('lodash'))).toBe(false); + expect(result.some(p => p.includes('node_modules'))).toBe(false); + }); +}); diff --git a/lib/duplicate.js b/lib/duplicate.js index 6bacf77..30a766b 100644 --- a/lib/duplicate.js +++ b/lib/duplicate.js @@ -43,4 +43,31 @@ function runDuplicateCheck(filePaths) { })); } -module.exports = { runDuplicateCheck }; +const JS_EXTENSIONS = new Set(['.js', '.ts', '.jsx', '.tsx', '.mjs', '.cjs']); +const IMPORT_RE = /(?:require|import)\s*\(\s*['"](\.[^'"]+)['"]\s*\)|import\s+.*?from\s+['"](\.[^'"]+)['"]/g; + +function resolveModuleNeighbors(filePaths) { + const neighbors = new Set(); + + for (const filePath of filePaths) { + if (!fs.existsSync(filePath)) continue; + neighbors.add(filePath); + + if (!JS_EXTENSIONS.has(path.extname(filePath))) continue; + + const content = fs.readFileSync(filePath, 'utf8'); + const dir = path.dirname(filePath); + let match; + IMPORT_RE.lastIndex = 0; + while ((match = IMPORT_RE.exec(content)) !== null) { + const importPath = match[1] || match[2]; + let resolved = path.resolve(dir, importPath); + if (!path.extname(resolved)) resolved += '.js'; + neighbors.add(resolved); + } + } + + return [...neighbors]; +} + +module.exports = { runDuplicateCheck, resolveModuleNeighbors }; From bf1eac4fa237d12f256140bfa615ae73c00b4948 Mon Sep 17 00:00:00 2001 From: Ken Judy Date: Fri, 12 Jun 2026 10:29:32 -0400 Subject: [PATCH 4/9] feat: add semantic duplicate detection via lib/claude.js Adds analyzeDuplicatesWithClaude(client, filePaths, staticFindings) which reads file contents, passes them to Claude with a duplication- focused system prompt, and returns structured findings with file1/file2/ similarity/concern/confidence. Degrades to [] when client is null or the API response is malformed. Co-Authored-By: Claude Sonnet 4.6 --- .beads/issues.jsonl | 2 +- __tests__/claudeAnalysis.test.js | 57 ++++++++++++++++++++++++++++++++ lib/claude.js | 56 ++++++++++++++++++++++++++++++- 3 files changed, 113 insertions(+), 2 deletions(-) diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 1183142..638e919 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -3,7 +3,7 @@ {"id":"code-quality-metrics-729","title":"feat: integrate semantic findings into pr-metrics.yml","description":"Extend pr-metrics.yml Duplication section to include Layer 2 semantic findings when ANTHROPIC_API_KEY is present. Degrade gracefully when key is absent — consistent with local-code-metrics.js pattern.\n\nLAYER INDICATOR STRINGS (exact, appended to Duplication section footer):\n When both layers ran:\n '_Layer 1 (static) and Layer 2 (semantic) ran._'\n When Layer 1 only (no API key):\n '_Layer 1 (static) ran. Set ANTHROPIC_API_KEY to enable semantic analysis._'\n\nSEMANTIC FINDINGS FORMAT (appended after static findings list):\n ### Semantic Similarity\n - `file1.js` / `file2.js`: similarity description (confidence: high)\n\nWhen semantic layer returns no findings: omit the Semantic Similarity subsection, still show layer indicator.\n\nVERIFICATION: grep audit confirms both indicator strings are present in the YAML; check conditional on ANTHROPIC_API_KEY mirrors existing pattern in the script block.\n\nDefinition of done: grep audit passes, both indicator strings are present, Layer 2 block is guarded by API key check.","status":"open","priority":1,"issue_type":"task","owner":"kjudy@stride.build","created_at":"2026-06-09T14:20:40Z","created_by":"Ken Judy","updated_at":"2026-06-09T14:59:30Z","dependencies":[{"issue_id":"code-quality-metrics-729","depends_on_id":"code-quality-metrics-5o3","type":"blocks","created_at":"2026-06-09T10:20:50Z","created_by":"Ken Judy","metadata":"{}"},{"issue_id":"code-quality-metrics-729","depends_on_id":"code-quality-metrics-790","type":"blocks","created_at":"2026-06-09T10:20:50Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":2,"dependent_count":1,"comment_count":0} {"id":"code-quality-metrics-790","title":"feat: add semantic duplicate detection via lib/claude.js","description":"Add analyzeDuplicatesWithClaude(client, filePaths, staticFindings) to lib/claude.js (same file as existing analyzeCommitWithClaude — consistent module boundary, no new file).\n\nRETURN SCHEMA:\n Array\u003c{\n file1: string, // relative path\n file2: string, // relative path\n similarity: string, // one-sentence description of what is similar\n concern: string, // why this matters (e.g. 'same error-handling pattern repeated')\n confidence: 'high' | 'medium' | 'low'\n }\u003e\n\nSYSTEM PROMPT INTENT: Given a set of changed files and their neighbors, identify semantically similar functions or logic blocks that suggest copy-paste or AI-generated duplication — even when variable names or formatting differ. Static analysis has already flagged token-level matches (passed as staticFindings); the agent focuses on structural/conceptual similarity that jscpd would miss.\n\nBEHAVIORAL TEST LIST:\n1. 'analyzeDuplicatesWithClaude returns empty array when client is null'\n Predicted failure: throws TypeError (null client not guarded)\n2. 'analyzeDuplicatesWithClaude constructs prompt including file contents and staticFindings count'\n Predicted failure: prompt does not mention staticFindings (not yet wired)\n3. 'analyzeDuplicatesWithClaude parses structured response into file1/file2/similarity/concern/confidence'\n Predicted failure: returns raw string instead of array\n4. 'analyzeDuplicatesWithClaude returns empty array when API response is malformed'\n Predicted failure: throws JSON parse error\n\nMOCK STRATEGY: mock Anthropic client.messages.create; return fixture JSON matching return schema.\n\nDefinition of done: all 4 tests green, function exported from lib/claude.js.","status":"open","priority":1,"issue_type":"task","owner":"kjudy@stride.build","created_at":"2026-06-09T14:20:40Z","created_by":"Ken Judy","updated_at":"2026-06-09T14:59:17Z","dependencies":[{"issue_id":"code-quality-metrics-790","depends_on_id":"code-quality-metrics-uhs","type":"blocks","created_at":"2026-06-09T10:20:50Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} {"id":"code-quality-metrics-een","title":"refactor: add duplicate detection config keys to lib/config.js","description":"Add four keys to the CONFIG object in lib/config.js:\n DUPLICATE_MIN_LINES: 5 (number)\n DUPLICATE_MIN_TOKENS: 50 (number)\n DUPLICATE_IGNORE_PATTERNS: [] (array of glob strings — caller populates for their repo; TEST_FILE_PATTERNS provides the right starting reference but is not hard-coded here)\n DUPLICATE_SCAN_PATHS: [] (array of strings — empty means scan all provided file paths)\n\nNo behavioral change to any existing function.\n\nBEHAVIORAL TEST LIST (name + predicted failure):\n1. 'CONFIG.DUPLICATE_MIN_LINES defaults to 5'\n Predicted failure: received undefined (key missing)\n2. 'CONFIG.DUPLICATE_MIN_TOKENS defaults to 50'\n Predicted failure: received undefined (key missing)\n3. 'CONFIG.DUPLICATE_IGNORE_PATTERNS is an array'\n Predicted failure: received undefined (key missing)\n4. 'CONFIG.DUPLICATE_SCAN_PATHS is an array'\n Predicted failure: received undefined (key missing)\n\nDefinition of done: all 4 tests green; npm test passes with no threshold changes; no other file modified.","status":"closed","priority":1,"issue_type":"task","assignee":"Ken Judy","owner":"kjudy@stride.build","created_at":"2026-06-09T14:20:40Z","created_by":"Ken Judy","updated_at":"2026-06-09T15:15:13Z","started_at":"2026-06-09T15:06:36Z","closed_at":"2026-06-09T15:15:13Z","close_reason":"Closed","dependency_count":0,"dependent_count":2,"comment_count":0} -{"id":"code-quality-metrics-uhs","title":"feat: implement resolveModuleNeighbors in lib/duplicate.js","description":"Add resolveModuleNeighbors(changedFilePaths) to lib/duplicate.js. Parses require()/import statements in each changed file using regex, resolves relative paths to absolute using path.resolve(path.dirname(sourceFile), importPath), appends .js if no extension, returns the union of changed files plus their direct (not transitive) local imports.\n\nEDGE CASE DECISIONS (made now, not during implementation):\n- Non-JS files (e.g. .yml, .json, .md): include in the returned set as-is, skip import parsing\n- Files that do not exist on disk (deleted in PR): skip with fs.existsSync check, do not throw\n- External/node_modules imports (no leading './'): ignore, do not resolve\n\nBEHAVIORAL TEST LIST:\n1. 'resolveModuleNeighbors returns changed files plus resolved local imports'\n Predicted failure: returns only the input files (import parsing not yet implemented)\n2. 'resolveModuleNeighbors skips import resolution for non-JS files'\n Predicted failure: throws or returns unexpected paths for .yml input\n3. 'resolveModuleNeighbors skips files that do not exist on disk'\n Predicted failure: throws ENOENT or returns broken paths\n4. 'resolveModuleNeighbors ignores external/node_modules imports'\n Predicted failure: includes node_modules paths or throws on resolution\n\nMOCK STRATEGY: mock fs.readFileSync for file contents; mock fs.existsSync for on-disk check.\n\nDefinition of done: all 4 tests green, function exported from lib/duplicate.js alongside runDuplicateCheck.","status":"open","priority":1,"issue_type":"task","owner":"kjudy@stride.build","created_at":"2026-06-09T14:20:40Z","created_by":"Ken Judy","updated_at":"2026-06-09T14:59:06Z","dependencies":[{"issue_id":"code-quality-metrics-uhs","depends_on_id":"code-quality-metrics-20o","type":"blocks","created_at":"2026-06-09T10:20:50Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} +{"id":"code-quality-metrics-uhs","title":"feat: implement resolveModuleNeighbors in lib/duplicate.js","description":"Add resolveModuleNeighbors(changedFilePaths) to lib/duplicate.js. Parses require()/import statements in each changed file using regex, resolves relative paths to absolute using path.resolve(path.dirname(sourceFile), importPath), appends .js if no extension, returns the union of changed files plus their direct (not transitive) local imports.\n\nEDGE CASE DECISIONS (made now, not during implementation):\n- Non-JS files (e.g. .yml, .json, .md): include in the returned set as-is, skip import parsing\n- Files that do not exist on disk (deleted in PR): skip with fs.existsSync check, do not throw\n- External/node_modules imports (no leading './'): ignore, do not resolve\n\nBEHAVIORAL TEST LIST:\n1. 'resolveModuleNeighbors returns changed files plus resolved local imports'\n Predicted failure: returns only the input files (import parsing not yet implemented)\n2. 'resolveModuleNeighbors skips import resolution for non-JS files'\n Predicted failure: throws or returns unexpected paths for .yml input\n3. 'resolveModuleNeighbors skips files that do not exist on disk'\n Predicted failure: throws ENOENT or returns broken paths\n4. 'resolveModuleNeighbors ignores external/node_modules imports'\n Predicted failure: includes node_modules paths or throws on resolution\n\nMOCK STRATEGY: mock fs.readFileSync for file contents; mock fs.existsSync for on-disk check.\n\nDefinition of done: all 4 tests green, function exported from lib/duplicate.js alongside runDuplicateCheck.","status":"closed","priority":1,"issue_type":"task","assignee":"Ken Judy","owner":"kjudy@stride.build","created_at":"2026-06-09T14:20:40Z","created_by":"Ken Judy","updated_at":"2026-06-12T14:25:50Z","started_at":"2026-06-09T15:30:18Z","closed_at":"2026-06-12T14:25:50Z","close_reason":"Closed","dependencies":[{"issue_id":"code-quality-metrics-uhs","depends_on_id":"code-quality-metrics-20o","type":"blocks","created_at":"2026-06-09T10:20:50Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} {"id":"code-quality-metrics-aat","title":"Feature: Two-layer duplicate code detection in PR check (#42)","description":"Add jscpd static detection (Layer 1, always-on) and agent-based semantic detection (Layer 2, conditional on ANTHROPIC_API_KEY) to pr-metrics.yml. All thresholds and ignore patterns live in lib/config.js as documented configurable parameters so the toolkit works across repos with different test conventions and directory structures.","notes":"Analysis: lib/config.js is single source of truth for all thresholds. lib/duplicate.js follows PR #33 pattern (require() in workflow). jscpd v5 uses --ignore-pattern flag, JSON output via --reporters json. Baseline: 0 duplicates in this repo production code at defaults. Other repos need to override DUPLICATE_IGNORE_PATTERNS. Layer 2 conditional on ANTHROPIC_API_KEY. Deliver Layer 1 first.","status":"open","priority":1,"issue_type":"epic","owner":"kjudy@stride.build","created_at":"2026-06-09T14:19:59Z","created_by":"Ken Judy","updated_at":"2026-06-09T14:20:18Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"code-quality-metrics-141","title":"Update churn distribution in local-code-metrics.js","description":"TDD: update collectLocalMetrics.test.js first to expect churn_intensity_median and churn_intensity_p90 instead of additions_ratio_median and additions_ratio_p90, then implement. Replace local-code-metrics.js:181 ratio array with: metrics.map(m =\u003e Math.min(m.total_additions, m.total_deletions) / Math.max(m.total_additions + m.total_deletions, 1)). Rename summary fields from additions_ratio_median/additions_ratio_p90 to churn_intensity_median/churn_intensity_p90.","status":"closed","priority":1,"issue_type":"task","assignee":"Ken Judy","owner":"kjudy@stride.build","created_at":"2026-06-08T20:36:14Z","created_by":"Ken Judy","updated_at":"2026-06-08T20:44:27Z","started_at":"2026-06-08T20:43:05Z","closed_at":"2026-06-08T20:44:27Z","close_reason":"Closed","dependencies":[{"issue_id":"code-quality-metrics-141","depends_on_id":"code-quality-metrics-4ck","type":"blocks","created_at":"2026-06-08T16:36:27Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":1,"dependent_count":3,"comment_count":0} {"id":"code-quality-metrics-4ck","title":"Replace change_ratio with churn_intensity in lib/git.js","description":"TDD: write failing tests first, then implement. Replace the string field change_ratio (which emits 'inf' for zero-deletion commits) with a numeric churn_intensity field using the formula: min(additions, deletions) / max(additions + deletions, 1). This is bounded [0.0, 0.5] and is always a number. Zero means pure addition or pure deletion; 0.5 means equal adds and deletes (maximum rework). Update __tests__/analyzeCommit.test.js: replace the two tests that assert change_ratio='inf' and change_ratio='2.00' with tests for churn_intensity. Add cases: no-deletions yields 0.0; equal adds/deletes yields 0.5; typical 10 adds / 5 deletes yields 0.333.","status":"closed","priority":1,"issue_type":"task","assignee":"Ken Judy","owner":"kjudy@stride.build","created_at":"2026-06-08T20:36:14Z","created_by":"Ken Judy","updated_at":"2026-06-08T20:42:20Z","started_at":"2026-06-08T20:40:00Z","closed_at":"2026-06-08T20:42:20Z","close_reason":"Closed","dependencies":[{"issue_id":"code-quality-metrics-4ck","depends_on_id":"code-quality-metrics-di7","type":"blocks","created_at":"2026-06-08T16:36:27Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} diff --git a/__tests__/claudeAnalysis.test.js b/__tests__/claudeAnalysis.test.js index ece2049..3460d7c 100644 --- a/__tests__/claudeAnalysis.test.js +++ b/__tests__/claudeAnalysis.test.js @@ -20,6 +20,7 @@ jest.mock('child_process'); jest.mock('fs'); const { execSync } = require('child_process'); +const fs = require('fs'); const { getAnthropicClient, selectClaudeCommits, @@ -27,6 +28,7 @@ const { analyzeWithClaude, CONFIG, } = require('../local-code-metrics'); +const { analyzeDuplicatesWithClaude } = require('../lib/claude'); beforeEach(() => { jest.clearAllMocks(); @@ -213,3 +215,58 @@ describe('selectClaudeCommits', () => { expect(result[0].sha).toBe('large'); }); }); + +// --------------------------------------------------------------------------- +// analyzeDuplicatesWithClaude +// --------------------------------------------------------------------------- + +describe('analyzeDuplicatesWithClaude', () => { + const FIXTURE_FINDING = { + file1: 'src/lib/git.js', + file2: 'src/lib/metrics.js', + similarity: 'both parse line-delimited data with the same loop structure', + concern: 'copy-paste risk in parsing logic', + confidence: 'high' + }; + + function makeClient(responsePayload) { + const mockCreate = jest.fn().mockResolvedValue({ + content: [{ type: 'text', text: JSON.stringify(responsePayload || [FIXTURE_FINDING]) }] + }); + return { messages: { create: mockCreate } }; + } + + test('returns empty array when client is null', async () => { + const result = await analyzeDuplicatesWithClaude(null, ['src/lib/git.js'], []); + expect(result).toEqual([]); + }); + + test('calls messages.create with file contents and staticFindings count', async () => { + const client = makeClient(); + fs.readFileSync.mockReturnValue('function parse(line) {}'); + await analyzeDuplicatesWithClaude(client, ['src/lib/git.js'], [{ lines: 10 }]); + expect(client.messages.create).toHaveBeenCalled(); + const call = client.messages.create.mock.calls[0][0]; + expect(call.messages[0].content).toContain('src/lib/git.js'); + expect(call.messages[0].content).toContain('1 static finding'); + }); + + test('parses structured response into file1/file2/similarity/concern/confidence', async () => { + const client = makeClient(); + fs.readFileSync.mockReturnValue('function parse(line) {}'); + const result = await analyzeDuplicatesWithClaude(client, ['src/lib/git.js'], []); + expect(result).toHaveLength(1); + expect(result[0].file1).toBe('src/lib/git.js'); + expect(result[0].confidence).toBe('high'); + }); + + test('returns empty array when API response is malformed', async () => { + const client = makeClient('not-json-array'); + client.messages.create.mockResolvedValue({ + content: [{ type: 'text', text: 'not valid json at all {{{' }] + }); + fs.readFileSync.mockReturnValue('function parse(line) {}'); + const result = await analyzeDuplicatesWithClaude(client, ['src/lib/git.js'], []); + expect(result).toEqual([]); + }); +}); diff --git a/lib/claude.js b/lib/claude.js index 316fb2b..a7075cd 100644 --- a/lib/claude.js +++ b/lib/claude.js @@ -1,6 +1,7 @@ // @ts-nocheck 'use strict'; +const fs = require('fs'); const { CONFIG } = require('./config'); const { getCommitDiff } = require('./git'); @@ -98,4 +99,57 @@ async function analyzeWithClaude(client, commits) { return results; } -module.exports = { CLAUDE_SYSTEM_PROMPT, getAnthropicClient, selectClaudeCommits, analyzeWithClaude }; +const DUPLICATE_SYSTEM_PROMPT = `You are a code quality analyst specializing in detecting copy-paste and AI-generated duplication. Given a set of files and any static duplicate findings already detected, identify semantically similar functions or logic blocks — even when variable names or formatting differ. + +Respond ONLY with valid JSON: an array of findings, or an empty array if none. Each finding: +{ + "file1": "", + "file2": "", + "similarity": "", + "concern": "", + "confidence": "high" | "medium" | "low" +}`; + +/** + * Analyze a set of files for semantic duplicate patterns using Claude. + * Returns empty array when client is null or on API/parse errors. + * @param {object|null} client - Anthropic client instance + * @param {string[]} filePaths + * @param {Array} staticFindings - findings from jscpd (passed as context) + * @returns {Promise>} + */ +async function analyzeDuplicatesWithClaude(client, filePaths, staticFindings) { + if (!client) return []; + + const fileSnippets = filePaths.map(p => { + try { + const content = fs.readFileSync(p, 'utf8'); + return `=== ${p} ===\n${content.substring(0, CONFIG.AI_DIFF_MAX_CHARS)}`; + } catch { + return `=== ${p} ===\n(unreadable)`; + } + }).join('\n\n'); + + const findingsNote = `${staticFindings.length} static finding${staticFindings.length === 1 ? '' : 's'} already detected by jscpd.`; + + try { + const response = await client.messages.create({ + model: 'claude-sonnet-4-6', + max_tokens: 1024, + system: DUPLICATE_SYSTEM_PROMPT, + messages: [{ + role: 'user', + content: `${findingsNote} Analyze these files for semantic duplication:\n\n${fileSnippets}` + }] + }); + + const raw = response.content[0].type === 'text' ? response.content[0].text : ''; + const json = raw.replace(/^```(?:json)?\n?/, '').replace(/\n?```$/, '').trim(); + const parsed = JSON.parse(json); + return Array.isArray(parsed) ? parsed : []; + } catch { + return []; + } +} + +module.exports = { CLAUDE_SYSTEM_PROMPT, DUPLICATE_SYSTEM_PROMPT, getAnthropicClient, selectClaudeCommits, analyzeWithClaude, analyzeDuplicatesWithClaude }; From 13e2db96599d85ca68ecca6912d0650f04ab0d5e Mon Sep 17 00:00:00 2001 From: Ken Judy Date: Fri, 12 Jun 2026 11:27:29 -0400 Subject: [PATCH 5/9] feat: integrate jscpd findings into pr-metrics.yml PR comment Runs runDuplicateCheck against PR production files after checkout. Adds a 'Duplicate Code' section to the PR comment when findings exist, showing file:start-end pairs with line/token counts (capped at 10). Section is omitted silently when jscpd finds no duplicates. Co-Authored-By: Claude Sonnet 4.6 --- .beads/issues.jsonl | 4 ++-- .github/workflows/pr-metrics.yml | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 638e919..afcd83d 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -1,7 +1,7 @@ {"id":"code-quality-metrics-20o","title":"feat: implement runDuplicateCheck in lib/duplicate.js","description":"Create lib/duplicate.js with runDuplicateCheck(filePaths) function. Runs jscpd via child_process using CONFIG values, reads the JSON report it writes to CONFIG.DUPLICATE_OUTPUT_DIR (a temp path), parses it, and returns a structured array.\n\nRETURN SHAPE (mirrors jscpd JSON output directly):\n Array\u003c{\n firstFile: { name: string, start: number, end: number },\n secondFile: { name: string, start: number, end: number },\n lines: number,\n tokens: number\n }\u003e\n\nJSCPD INVOCATION:\n jscpd --min-lines CONFIG.DUPLICATE_MIN_LINES --min-tokens CONFIG.DUPLICATE_MIN_TOKENS\n --ignore-pattern CONFIG.DUPLICATE_IGNORE_PATTERNS\n --reporters json --output CONFIG.DUPLICATE_OUTPUT_DIR\n [filePaths...]\n\nMOCK STRATEGY: mock execSync (child_process) for the jscpd call; mock fs.readFileSync for reading the output JSON; mock fs.existsSync to control whether the output file exists.\n\nBEHAVIORAL TEST LIST (one RED test before each GREEN):\n1. 'runDuplicateCheck returns empty array when filePaths is empty without calling jscpd'\n Predicted failure: received undefined or thrown error\n2. 'runDuplicateCheck returns empty array when jscpd output file does not exist'\n Predicted failure: received undefined or fs.readFileSync called unexpectedly\n3. 'runDuplicateCheck parses jscpd JSON and returns firstFile/secondFile/lines/tokens for each duplicate'\n Predicted failure: received [] (not yet parsing)\n4. 'runDuplicateCheck returns empty array when jscpd exits non-zero'\n Predicted failure: thrown error propagated\n\nDefinition of done: all 4 tests green, no stubs, exports runDuplicateCheck from lib/duplicate.js.","notes":"TDD ordering: test 3 (happy path parse) before test 1 (empty input). Reason: stub return [] makes tests 1, 2, 4 vacuously green — test 3 is the first genuine RED.","status":"closed","priority":1,"issue_type":"task","assignee":"Ken Judy","owner":"kjudy@stride.build","created_at":"2026-06-09T14:20:40Z","created_by":"Ken Judy","updated_at":"2026-06-09T15:30:02Z","started_at":"2026-06-09T15:17:16Z","closed_at":"2026-06-09T15:30:02Z","close_reason":"Closed","dependencies":[{"issue_id":"code-quality-metrics-20o","depends_on_id":"code-quality-metrics-een","type":"blocks","created_at":"2026-06-09T10:20:50Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":1,"dependent_count":2,"comment_count":0} -{"id":"code-quality-metrics-5o3","title":"feat: integrate jscpd findings into pr-metrics.yml PR comment","description":"Require lib/duplicate.js in pr-metrics.yml. Run runDuplicateCheck against the already-computed prodFiles array (map to filenames). Add a Duplication section to the PR comment when findings exist; omit the section silently when findings array is empty.\n\nPR COMMENT SECTION FORMAT (exact strings matter for CHECK):\n ## Duplicate Code\n Found N duplicate block(s) across the changed files:\n - `path/to/file.js:10-25` duplicates `path/to/other.js:5-20` (15 lines, 120 tokens)\n (one bullet per finding, capped at 10 — add 'and N more...' if truncated)\n\nWhen no findings: section is omitted entirely (no 'No duplicates found' line).\n\nVERIFICATION (no unit tests for YAML): grep audit confirms\n - require('./lib/duplicate') appears in the script block\n - runDuplicateCheck is called with prodFiles.map(f =\u003e f.filename)\n - '## Duplicate Code' header string is present\n - commentParts.filter(Boolean) already handles optional sections (no extra guard needed)\n\nDefinition of done: grep audit passes, PR comment renders section only when findings exist.","status":"open","priority":1,"issue_type":"task","owner":"kjudy@stride.build","created_at":"2026-06-09T14:20:40Z","created_by":"Ken Judy","updated_at":"2026-06-09T14:58:55Z","dependencies":[{"issue_id":"code-quality-metrics-5o3","depends_on_id":"code-quality-metrics-20o","type":"blocks","created_at":"2026-06-09T10:20:50Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} +{"id":"code-quality-metrics-5o3","title":"feat: integrate jscpd findings into pr-metrics.yml PR comment","description":"Require lib/duplicate.js in pr-metrics.yml. Run runDuplicateCheck against the already-computed prodFiles array (map to filenames). Add a Duplication section to the PR comment when findings exist; omit the section silently when findings array is empty.\n\nPR COMMENT SECTION FORMAT (exact strings matter for CHECK):\n ## Duplicate Code\n Found N duplicate block(s) across the changed files:\n - `path/to/file.js:10-25` duplicates `path/to/other.js:5-20` (15 lines, 120 tokens)\n (one bullet per finding, capped at 10 — add 'and N more...' if truncated)\n\nWhen no findings: section is omitted entirely (no 'No duplicates found' line).\n\nVERIFICATION (no unit tests for YAML): grep audit confirms\n - require('./lib/duplicate') appears in the script block\n - runDuplicateCheck is called with prodFiles.map(f =\u003e f.filename)\n - '## Duplicate Code' header string is present\n - commentParts.filter(Boolean) already handles optional sections (no extra guard needed)\n\nDefinition of done: grep audit passes, PR comment renders section only when findings exist.","status":"in_progress","priority":1,"issue_type":"task","assignee":"Ken Judy","owner":"kjudy@stride.build","created_at":"2026-06-09T14:20:40Z","created_by":"Ken Judy","updated_at":"2026-06-12T14:30:08Z","started_at":"2026-06-12T14:30:08Z","dependencies":[{"issue_id":"code-quality-metrics-5o3","depends_on_id":"code-quality-metrics-20o","type":"blocks","created_at":"2026-06-09T10:20:50Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} {"id":"code-quality-metrics-729","title":"feat: integrate semantic findings into pr-metrics.yml","description":"Extend pr-metrics.yml Duplication section to include Layer 2 semantic findings when ANTHROPIC_API_KEY is present. Degrade gracefully when key is absent — consistent with local-code-metrics.js pattern.\n\nLAYER INDICATOR STRINGS (exact, appended to Duplication section footer):\n When both layers ran:\n '_Layer 1 (static) and Layer 2 (semantic) ran._'\n When Layer 1 only (no API key):\n '_Layer 1 (static) ran. Set ANTHROPIC_API_KEY to enable semantic analysis._'\n\nSEMANTIC FINDINGS FORMAT (appended after static findings list):\n ### Semantic Similarity\n - `file1.js` / `file2.js`: similarity description (confidence: high)\n\nWhen semantic layer returns no findings: omit the Semantic Similarity subsection, still show layer indicator.\n\nVERIFICATION: grep audit confirms both indicator strings are present in the YAML; check conditional on ANTHROPIC_API_KEY mirrors existing pattern in the script block.\n\nDefinition of done: grep audit passes, both indicator strings are present, Layer 2 block is guarded by API key check.","status":"open","priority":1,"issue_type":"task","owner":"kjudy@stride.build","created_at":"2026-06-09T14:20:40Z","created_by":"Ken Judy","updated_at":"2026-06-09T14:59:30Z","dependencies":[{"issue_id":"code-quality-metrics-729","depends_on_id":"code-quality-metrics-5o3","type":"blocks","created_at":"2026-06-09T10:20:50Z","created_by":"Ken Judy","metadata":"{}"},{"issue_id":"code-quality-metrics-729","depends_on_id":"code-quality-metrics-790","type":"blocks","created_at":"2026-06-09T10:20:50Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":2,"dependent_count":1,"comment_count":0} -{"id":"code-quality-metrics-790","title":"feat: add semantic duplicate detection via lib/claude.js","description":"Add analyzeDuplicatesWithClaude(client, filePaths, staticFindings) to lib/claude.js (same file as existing analyzeCommitWithClaude — consistent module boundary, no new file).\n\nRETURN SCHEMA:\n Array\u003c{\n file1: string, // relative path\n file2: string, // relative path\n similarity: string, // one-sentence description of what is similar\n concern: string, // why this matters (e.g. 'same error-handling pattern repeated')\n confidence: 'high' | 'medium' | 'low'\n }\u003e\n\nSYSTEM PROMPT INTENT: Given a set of changed files and their neighbors, identify semantically similar functions or logic blocks that suggest copy-paste or AI-generated duplication — even when variable names or formatting differ. Static analysis has already flagged token-level matches (passed as staticFindings); the agent focuses on structural/conceptual similarity that jscpd would miss.\n\nBEHAVIORAL TEST LIST:\n1. 'analyzeDuplicatesWithClaude returns empty array when client is null'\n Predicted failure: throws TypeError (null client not guarded)\n2. 'analyzeDuplicatesWithClaude constructs prompt including file contents and staticFindings count'\n Predicted failure: prompt does not mention staticFindings (not yet wired)\n3. 'analyzeDuplicatesWithClaude parses structured response into file1/file2/similarity/concern/confidence'\n Predicted failure: returns raw string instead of array\n4. 'analyzeDuplicatesWithClaude returns empty array when API response is malformed'\n Predicted failure: throws JSON parse error\n\nMOCK STRATEGY: mock Anthropic client.messages.create; return fixture JSON matching return schema.\n\nDefinition of done: all 4 tests green, function exported from lib/claude.js.","status":"open","priority":1,"issue_type":"task","owner":"kjudy@stride.build","created_at":"2026-06-09T14:20:40Z","created_by":"Ken Judy","updated_at":"2026-06-09T14:59:17Z","dependencies":[{"issue_id":"code-quality-metrics-790","depends_on_id":"code-quality-metrics-uhs","type":"blocks","created_at":"2026-06-09T10:20:50Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} +{"id":"code-quality-metrics-790","title":"feat: add semantic duplicate detection via lib/claude.js","description":"Add analyzeDuplicatesWithClaude(client, filePaths, staticFindings) to lib/claude.js (same file as existing analyzeCommitWithClaude — consistent module boundary, no new file).\n\nRETURN SCHEMA:\n Array\u003c{\n file1: string, // relative path\n file2: string, // relative path\n similarity: string, // one-sentence description of what is similar\n concern: string, // why this matters (e.g. 'same error-handling pattern repeated')\n confidence: 'high' | 'medium' | 'low'\n }\u003e\n\nSYSTEM PROMPT INTENT: Given a set of changed files and their neighbors, identify semantically similar functions or logic blocks that suggest copy-paste or AI-generated duplication — even when variable names or formatting differ. Static analysis has already flagged token-level matches (passed as staticFindings); the agent focuses on structural/conceptual similarity that jscpd would miss.\n\nBEHAVIORAL TEST LIST:\n1. 'analyzeDuplicatesWithClaude returns empty array when client is null'\n Predicted failure: throws TypeError (null client not guarded)\n2. 'analyzeDuplicatesWithClaude constructs prompt including file contents and staticFindings count'\n Predicted failure: prompt does not mention staticFindings (not yet wired)\n3. 'analyzeDuplicatesWithClaude parses structured response into file1/file2/similarity/concern/confidence'\n Predicted failure: returns raw string instead of array\n4. 'analyzeDuplicatesWithClaude returns empty array when API response is malformed'\n Predicted failure: throws JSON parse error\n\nMOCK STRATEGY: mock Anthropic client.messages.create; return fixture JSON matching return schema.\n\nDefinition of done: all 4 tests green, function exported from lib/claude.js.","status":"closed","priority":1,"issue_type":"task","owner":"kjudy@stride.build","created_at":"2026-06-09T14:20:40Z","created_by":"Ken Judy","updated_at":"2026-06-12T14:29:40Z","closed_at":"2026-06-12T14:29:40Z","close_reason":"Closed","dependencies":[{"issue_id":"code-quality-metrics-790","depends_on_id":"code-quality-metrics-uhs","type":"blocks","created_at":"2026-06-09T10:20:50Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} {"id":"code-quality-metrics-een","title":"refactor: add duplicate detection config keys to lib/config.js","description":"Add four keys to the CONFIG object in lib/config.js:\n DUPLICATE_MIN_LINES: 5 (number)\n DUPLICATE_MIN_TOKENS: 50 (number)\n DUPLICATE_IGNORE_PATTERNS: [] (array of glob strings — caller populates for their repo; TEST_FILE_PATTERNS provides the right starting reference but is not hard-coded here)\n DUPLICATE_SCAN_PATHS: [] (array of strings — empty means scan all provided file paths)\n\nNo behavioral change to any existing function.\n\nBEHAVIORAL TEST LIST (name + predicted failure):\n1. 'CONFIG.DUPLICATE_MIN_LINES defaults to 5'\n Predicted failure: received undefined (key missing)\n2. 'CONFIG.DUPLICATE_MIN_TOKENS defaults to 50'\n Predicted failure: received undefined (key missing)\n3. 'CONFIG.DUPLICATE_IGNORE_PATTERNS is an array'\n Predicted failure: received undefined (key missing)\n4. 'CONFIG.DUPLICATE_SCAN_PATHS is an array'\n Predicted failure: received undefined (key missing)\n\nDefinition of done: all 4 tests green; npm test passes with no threshold changes; no other file modified.","status":"closed","priority":1,"issue_type":"task","assignee":"Ken Judy","owner":"kjudy@stride.build","created_at":"2026-06-09T14:20:40Z","created_by":"Ken Judy","updated_at":"2026-06-09T15:15:13Z","started_at":"2026-06-09T15:06:36Z","closed_at":"2026-06-09T15:15:13Z","close_reason":"Closed","dependency_count":0,"dependent_count":2,"comment_count":0} {"id":"code-quality-metrics-uhs","title":"feat: implement resolveModuleNeighbors in lib/duplicate.js","description":"Add resolveModuleNeighbors(changedFilePaths) to lib/duplicate.js. Parses require()/import statements in each changed file using regex, resolves relative paths to absolute using path.resolve(path.dirname(sourceFile), importPath), appends .js if no extension, returns the union of changed files plus their direct (not transitive) local imports.\n\nEDGE CASE DECISIONS (made now, not during implementation):\n- Non-JS files (e.g. .yml, .json, .md): include in the returned set as-is, skip import parsing\n- Files that do not exist on disk (deleted in PR): skip with fs.existsSync check, do not throw\n- External/node_modules imports (no leading './'): ignore, do not resolve\n\nBEHAVIORAL TEST LIST:\n1. 'resolveModuleNeighbors returns changed files plus resolved local imports'\n Predicted failure: returns only the input files (import parsing not yet implemented)\n2. 'resolveModuleNeighbors skips import resolution for non-JS files'\n Predicted failure: throws or returns unexpected paths for .yml input\n3. 'resolveModuleNeighbors skips files that do not exist on disk'\n Predicted failure: throws ENOENT or returns broken paths\n4. 'resolveModuleNeighbors ignores external/node_modules imports'\n Predicted failure: includes node_modules paths or throws on resolution\n\nMOCK STRATEGY: mock fs.readFileSync for file contents; mock fs.existsSync for on-disk check.\n\nDefinition of done: all 4 tests green, function exported from lib/duplicate.js alongside runDuplicateCheck.","status":"closed","priority":1,"issue_type":"task","assignee":"Ken Judy","owner":"kjudy@stride.build","created_at":"2026-06-09T14:20:40Z","created_by":"Ken Judy","updated_at":"2026-06-12T14:25:50Z","started_at":"2026-06-09T15:30:18Z","closed_at":"2026-06-12T14:25:50Z","close_reason":"Closed","dependencies":[{"issue_id":"code-quality-metrics-uhs","depends_on_id":"code-quality-metrics-20o","type":"blocks","created_at":"2026-06-09T10:20:50Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} {"id":"code-quality-metrics-aat","title":"Feature: Two-layer duplicate code detection in PR check (#42)","description":"Add jscpd static detection (Layer 1, always-on) and agent-based semantic detection (Layer 2, conditional on ANTHROPIC_API_KEY) to pr-metrics.yml. All thresholds and ignore patterns live in lib/config.js as documented configurable parameters so the toolkit works across repos with different test conventions and directory structures.","notes":"Analysis: lib/config.js is single source of truth for all thresholds. lib/duplicate.js follows PR #33 pattern (require() in workflow). jscpd v5 uses --ignore-pattern flag, JSON output via --reporters json. Baseline: 0 duplicates in this repo production code at defaults. Other repos need to override DUPLICATE_IGNORE_PATTERNS. Layer 2 conditional on ANTHROPIC_API_KEY. Deliver Layer 1 first.","status":"open","priority":1,"issue_type":"epic","owner":"kjudy@stride.build","created_at":"2026-06-09T14:19:59Z","created_by":"Ken Judy","updated_at":"2026-06-09T14:20:18Z","dependency_count":0,"dependent_count":0,"comment_count":0} diff --git a/.github/workflows/pr-metrics.yml b/.github/workflows/pr-metrics.yml index bd6d5ec..817e15a 100644 --- a/.github/workflows/pr-metrics.yml +++ b/.github/workflows/pr-metrics.yml @@ -24,6 +24,7 @@ jobs: // lib/ modules are available after checkout — require() resolves from $GITHUB_WORKSPACE const { CONFIG } = require('./lib/config'); const { scoreMessageQuality, classifyDoraArchetype, isTestFile } = require('./lib/metrics'); + const { runDuplicateCheck } = require('./lib/duplicate'); // --------------------------------------------------------------- // Fetch PR data @@ -114,6 +115,8 @@ jobs: const prodChanges = prodAdditions + prodDeletions; const prodFilesCount = prodFiles.length; + const staticDuplicates = runDuplicateCheck(prodFiles.map(f => f.filename)); + const testAdditions = testFiles.reduce((sum, f) => sum + f.additions, 0); const testDeletions = testFiles.reduce((sum, f) => sum + f.deletions, 0); const testChanges = testAdditions + testDeletions; @@ -304,6 +307,16 @@ jobs: `| AI Risk Signal | Net additions ratio (median) | ${medianAdditionsRatio.toFixed(2)} | <0.50 |` ].join('\n') : ''; + const duplicationSection = staticDuplicates.length > 0 ? [ + '## Duplicate Code', + '', + `Found ${staticDuplicates.length} duplicate block(s) across the changed files:`, + ...staticDuplicates.slice(0, 10).map(d => + `- \`${d.firstFile.name}:${d.firstFile.start}-${d.firstFile.end}\` duplicates \`${d.secondFile.name}:${d.secondFile.start}-${d.secondFile.end}\` (${d.lines} lines, ${d.tokens} tokens)` + ), + ...(staticDuplicates.length > 10 ? [`and ${staticDuplicates.length - 10} more...`] : []) + ].join('\n') : ''; + const commentParts = [ '## PR Analysis', '', @@ -350,6 +363,8 @@ jobs: '', doraSection, '', + duplicationSection, + '', '_Automated by Code Metrics Workflow_' ].filter(s => s !== '').join('\n'); From 23664a68f91b58b23c1c85616c98e93485ca8025 Mon Sep 17 00:00:00 2001 From: Ken Judy Date: Fri, 12 Jun 2026 11:34:37 -0400 Subject: [PATCH 6/9] feat: integrate semantic findings into pr-metrics.yml Adds Layer 2 semantic duplicate detection when ANTHROPIC_API_KEY is set. Uses resolveModuleNeighbors to scope analysis to changed files plus their direct imports, then passes results to analyzeDuplicatesWithClaude. PR comment Duplication section now shows static findings, semantic findings (when found), and a layer indicator confirming which layers ran. Degrades to Layer 1 only when API key is absent. Co-Authored-By: Claude Sonnet 4.6 --- .beads/issues.jsonl | 4 ++-- .github/workflows/pr-metrics.yml | 36 ++++++++++++++++++++++++++++---- 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index afcd83d..5f2d26d 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -1,6 +1,6 @@ {"id":"code-quality-metrics-20o","title":"feat: implement runDuplicateCheck in lib/duplicate.js","description":"Create lib/duplicate.js with runDuplicateCheck(filePaths) function. Runs jscpd via child_process using CONFIG values, reads the JSON report it writes to CONFIG.DUPLICATE_OUTPUT_DIR (a temp path), parses it, and returns a structured array.\n\nRETURN SHAPE (mirrors jscpd JSON output directly):\n Array\u003c{\n firstFile: { name: string, start: number, end: number },\n secondFile: { name: string, start: number, end: number },\n lines: number,\n tokens: number\n }\u003e\n\nJSCPD INVOCATION:\n jscpd --min-lines CONFIG.DUPLICATE_MIN_LINES --min-tokens CONFIG.DUPLICATE_MIN_TOKENS\n --ignore-pattern CONFIG.DUPLICATE_IGNORE_PATTERNS\n --reporters json --output CONFIG.DUPLICATE_OUTPUT_DIR\n [filePaths...]\n\nMOCK STRATEGY: mock execSync (child_process) for the jscpd call; mock fs.readFileSync for reading the output JSON; mock fs.existsSync to control whether the output file exists.\n\nBEHAVIORAL TEST LIST (one RED test before each GREEN):\n1. 'runDuplicateCheck returns empty array when filePaths is empty without calling jscpd'\n Predicted failure: received undefined or thrown error\n2. 'runDuplicateCheck returns empty array when jscpd output file does not exist'\n Predicted failure: received undefined or fs.readFileSync called unexpectedly\n3. 'runDuplicateCheck parses jscpd JSON and returns firstFile/secondFile/lines/tokens for each duplicate'\n Predicted failure: received [] (not yet parsing)\n4. 'runDuplicateCheck returns empty array when jscpd exits non-zero'\n Predicted failure: thrown error propagated\n\nDefinition of done: all 4 tests green, no stubs, exports runDuplicateCheck from lib/duplicate.js.","notes":"TDD ordering: test 3 (happy path parse) before test 1 (empty input). Reason: stub return [] makes tests 1, 2, 4 vacuously green — test 3 is the first genuine RED.","status":"closed","priority":1,"issue_type":"task","assignee":"Ken Judy","owner":"kjudy@stride.build","created_at":"2026-06-09T14:20:40Z","created_by":"Ken Judy","updated_at":"2026-06-09T15:30:02Z","started_at":"2026-06-09T15:17:16Z","closed_at":"2026-06-09T15:30:02Z","close_reason":"Closed","dependencies":[{"issue_id":"code-quality-metrics-20o","depends_on_id":"code-quality-metrics-een","type":"blocks","created_at":"2026-06-09T10:20:50Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":1,"dependent_count":2,"comment_count":0} -{"id":"code-quality-metrics-5o3","title":"feat: integrate jscpd findings into pr-metrics.yml PR comment","description":"Require lib/duplicate.js in pr-metrics.yml. Run runDuplicateCheck against the already-computed prodFiles array (map to filenames). Add a Duplication section to the PR comment when findings exist; omit the section silently when findings array is empty.\n\nPR COMMENT SECTION FORMAT (exact strings matter for CHECK):\n ## Duplicate Code\n Found N duplicate block(s) across the changed files:\n - `path/to/file.js:10-25` duplicates `path/to/other.js:5-20` (15 lines, 120 tokens)\n (one bullet per finding, capped at 10 — add 'and N more...' if truncated)\n\nWhen no findings: section is omitted entirely (no 'No duplicates found' line).\n\nVERIFICATION (no unit tests for YAML): grep audit confirms\n - require('./lib/duplicate') appears in the script block\n - runDuplicateCheck is called with prodFiles.map(f =\u003e f.filename)\n - '## Duplicate Code' header string is present\n - commentParts.filter(Boolean) already handles optional sections (no extra guard needed)\n\nDefinition of done: grep audit passes, PR comment renders section only when findings exist.","status":"in_progress","priority":1,"issue_type":"task","assignee":"Ken Judy","owner":"kjudy@stride.build","created_at":"2026-06-09T14:20:40Z","created_by":"Ken Judy","updated_at":"2026-06-12T14:30:08Z","started_at":"2026-06-12T14:30:08Z","dependencies":[{"issue_id":"code-quality-metrics-5o3","depends_on_id":"code-quality-metrics-20o","type":"blocks","created_at":"2026-06-09T10:20:50Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} -{"id":"code-quality-metrics-729","title":"feat: integrate semantic findings into pr-metrics.yml","description":"Extend pr-metrics.yml Duplication section to include Layer 2 semantic findings when ANTHROPIC_API_KEY is present. Degrade gracefully when key is absent — consistent with local-code-metrics.js pattern.\n\nLAYER INDICATOR STRINGS (exact, appended to Duplication section footer):\n When both layers ran:\n '_Layer 1 (static) and Layer 2 (semantic) ran._'\n When Layer 1 only (no API key):\n '_Layer 1 (static) ran. Set ANTHROPIC_API_KEY to enable semantic analysis._'\n\nSEMANTIC FINDINGS FORMAT (appended after static findings list):\n ### Semantic Similarity\n - `file1.js` / `file2.js`: similarity description (confidence: high)\n\nWhen semantic layer returns no findings: omit the Semantic Similarity subsection, still show layer indicator.\n\nVERIFICATION: grep audit confirms both indicator strings are present in the YAML; check conditional on ANTHROPIC_API_KEY mirrors existing pattern in the script block.\n\nDefinition of done: grep audit passes, both indicator strings are present, Layer 2 block is guarded by API key check.","status":"open","priority":1,"issue_type":"task","owner":"kjudy@stride.build","created_at":"2026-06-09T14:20:40Z","created_by":"Ken Judy","updated_at":"2026-06-09T14:59:30Z","dependencies":[{"issue_id":"code-quality-metrics-729","depends_on_id":"code-quality-metrics-5o3","type":"blocks","created_at":"2026-06-09T10:20:50Z","created_by":"Ken Judy","metadata":"{}"},{"issue_id":"code-quality-metrics-729","depends_on_id":"code-quality-metrics-790","type":"blocks","created_at":"2026-06-09T10:20:50Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":2,"dependent_count":1,"comment_count":0} +{"id":"code-quality-metrics-5o3","title":"feat: integrate jscpd findings into pr-metrics.yml PR comment","description":"Require lib/duplicate.js in pr-metrics.yml. Run runDuplicateCheck against the already-computed prodFiles array (map to filenames). Add a Duplication section to the PR comment when findings exist; omit the section silently when findings array is empty.\n\nPR COMMENT SECTION FORMAT (exact strings matter for CHECK):\n ## Duplicate Code\n Found N duplicate block(s) across the changed files:\n - `path/to/file.js:10-25` duplicates `path/to/other.js:5-20` (15 lines, 120 tokens)\n (one bullet per finding, capped at 10 — add 'and N more...' if truncated)\n\nWhen no findings: section is omitted entirely (no 'No duplicates found' line).\n\nVERIFICATION (no unit tests for YAML): grep audit confirms\n - require('./lib/duplicate') appears in the script block\n - runDuplicateCheck is called with prodFiles.map(f =\u003e f.filename)\n - '## Duplicate Code' header string is present\n - commentParts.filter(Boolean) already handles optional sections (no extra guard needed)\n\nDefinition of done: grep audit passes, PR comment renders section only when findings exist.","status":"closed","priority":1,"issue_type":"task","assignee":"Ken Judy","owner":"kjudy@stride.build","created_at":"2026-06-09T14:20:40Z","created_by":"Ken Judy","updated_at":"2026-06-12T15:27:46Z","started_at":"2026-06-12T14:30:08Z","closed_at":"2026-06-12T15:27:46Z","close_reason":"Closed","dependencies":[{"issue_id":"code-quality-metrics-5o3","depends_on_id":"code-quality-metrics-20o","type":"blocks","created_at":"2026-06-09T10:20:50Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} +{"id":"code-quality-metrics-729","title":"feat: integrate semantic findings into pr-metrics.yml","description":"Extend pr-metrics.yml Duplication section to include Layer 2 semantic findings when ANTHROPIC_API_KEY is present. Degrade gracefully when key is absent — consistent with local-code-metrics.js pattern.\n\nLAYER INDICATOR STRINGS (exact, appended to Duplication section footer):\n When both layers ran:\n '_Layer 1 (static) and Layer 2 (semantic) ran._'\n When Layer 1 only (no API key):\n '_Layer 1 (static) ran. Set ANTHROPIC_API_KEY to enable semantic analysis._'\n\nSEMANTIC FINDINGS FORMAT (appended after static findings list):\n ### Semantic Similarity\n - `file1.js` / `file2.js`: similarity description (confidence: high)\n\nWhen semantic layer returns no findings: omit the Semantic Similarity subsection, still show layer indicator.\n\nVERIFICATION: grep audit confirms both indicator strings are present in the YAML; check conditional on ANTHROPIC_API_KEY mirrors existing pattern in the script block.\n\nDefinition of done: grep audit passes, both indicator strings are present, Layer 2 block is guarded by API key check.","status":"in_progress","priority":1,"issue_type":"task","assignee":"Ken Judy","owner":"kjudy@stride.build","created_at":"2026-06-09T14:20:40Z","created_by":"Ken Judy","updated_at":"2026-06-12T15:28:43Z","started_at":"2026-06-12T15:28:43Z","dependencies":[{"issue_id":"code-quality-metrics-729","depends_on_id":"code-quality-metrics-5o3","type":"blocks","created_at":"2026-06-09T10:20:50Z","created_by":"Ken Judy","metadata":"{}"},{"issue_id":"code-quality-metrics-729","depends_on_id":"code-quality-metrics-790","type":"blocks","created_at":"2026-06-09T10:20:50Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":2,"dependent_count":1,"comment_count":0} {"id":"code-quality-metrics-790","title":"feat: add semantic duplicate detection via lib/claude.js","description":"Add analyzeDuplicatesWithClaude(client, filePaths, staticFindings) to lib/claude.js (same file as existing analyzeCommitWithClaude — consistent module boundary, no new file).\n\nRETURN SCHEMA:\n Array\u003c{\n file1: string, // relative path\n file2: string, // relative path\n similarity: string, // one-sentence description of what is similar\n concern: string, // why this matters (e.g. 'same error-handling pattern repeated')\n confidence: 'high' | 'medium' | 'low'\n }\u003e\n\nSYSTEM PROMPT INTENT: Given a set of changed files and their neighbors, identify semantically similar functions or logic blocks that suggest copy-paste or AI-generated duplication — even when variable names or formatting differ. Static analysis has already flagged token-level matches (passed as staticFindings); the agent focuses on structural/conceptual similarity that jscpd would miss.\n\nBEHAVIORAL TEST LIST:\n1. 'analyzeDuplicatesWithClaude returns empty array when client is null'\n Predicted failure: throws TypeError (null client not guarded)\n2. 'analyzeDuplicatesWithClaude constructs prompt including file contents and staticFindings count'\n Predicted failure: prompt does not mention staticFindings (not yet wired)\n3. 'analyzeDuplicatesWithClaude parses structured response into file1/file2/similarity/concern/confidence'\n Predicted failure: returns raw string instead of array\n4. 'analyzeDuplicatesWithClaude returns empty array when API response is malformed'\n Predicted failure: throws JSON parse error\n\nMOCK STRATEGY: mock Anthropic client.messages.create; return fixture JSON matching return schema.\n\nDefinition of done: all 4 tests green, function exported from lib/claude.js.","status":"closed","priority":1,"issue_type":"task","owner":"kjudy@stride.build","created_at":"2026-06-09T14:20:40Z","created_by":"Ken Judy","updated_at":"2026-06-12T14:29:40Z","closed_at":"2026-06-12T14:29:40Z","close_reason":"Closed","dependencies":[{"issue_id":"code-quality-metrics-790","depends_on_id":"code-quality-metrics-uhs","type":"blocks","created_at":"2026-06-09T10:20:50Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} {"id":"code-quality-metrics-een","title":"refactor: add duplicate detection config keys to lib/config.js","description":"Add four keys to the CONFIG object in lib/config.js:\n DUPLICATE_MIN_LINES: 5 (number)\n DUPLICATE_MIN_TOKENS: 50 (number)\n DUPLICATE_IGNORE_PATTERNS: [] (array of glob strings — caller populates for their repo; TEST_FILE_PATTERNS provides the right starting reference but is not hard-coded here)\n DUPLICATE_SCAN_PATHS: [] (array of strings — empty means scan all provided file paths)\n\nNo behavioral change to any existing function.\n\nBEHAVIORAL TEST LIST (name + predicted failure):\n1. 'CONFIG.DUPLICATE_MIN_LINES defaults to 5'\n Predicted failure: received undefined (key missing)\n2. 'CONFIG.DUPLICATE_MIN_TOKENS defaults to 50'\n Predicted failure: received undefined (key missing)\n3. 'CONFIG.DUPLICATE_IGNORE_PATTERNS is an array'\n Predicted failure: received undefined (key missing)\n4. 'CONFIG.DUPLICATE_SCAN_PATHS is an array'\n Predicted failure: received undefined (key missing)\n\nDefinition of done: all 4 tests green; npm test passes with no threshold changes; no other file modified.","status":"closed","priority":1,"issue_type":"task","assignee":"Ken Judy","owner":"kjudy@stride.build","created_at":"2026-06-09T14:20:40Z","created_by":"Ken Judy","updated_at":"2026-06-09T15:15:13Z","started_at":"2026-06-09T15:06:36Z","closed_at":"2026-06-09T15:15:13Z","close_reason":"Closed","dependency_count":0,"dependent_count":2,"comment_count":0} {"id":"code-quality-metrics-uhs","title":"feat: implement resolveModuleNeighbors in lib/duplicate.js","description":"Add resolveModuleNeighbors(changedFilePaths) to lib/duplicate.js. Parses require()/import statements in each changed file using regex, resolves relative paths to absolute using path.resolve(path.dirname(sourceFile), importPath), appends .js if no extension, returns the union of changed files plus their direct (not transitive) local imports.\n\nEDGE CASE DECISIONS (made now, not during implementation):\n- Non-JS files (e.g. .yml, .json, .md): include in the returned set as-is, skip import parsing\n- Files that do not exist on disk (deleted in PR): skip with fs.existsSync check, do not throw\n- External/node_modules imports (no leading './'): ignore, do not resolve\n\nBEHAVIORAL TEST LIST:\n1. 'resolveModuleNeighbors returns changed files plus resolved local imports'\n Predicted failure: returns only the input files (import parsing not yet implemented)\n2. 'resolveModuleNeighbors skips import resolution for non-JS files'\n Predicted failure: throws or returns unexpected paths for .yml input\n3. 'resolveModuleNeighbors skips files that do not exist on disk'\n Predicted failure: throws ENOENT or returns broken paths\n4. 'resolveModuleNeighbors ignores external/node_modules imports'\n Predicted failure: includes node_modules paths or throws on resolution\n\nMOCK STRATEGY: mock fs.readFileSync for file contents; mock fs.existsSync for on-disk check.\n\nDefinition of done: all 4 tests green, function exported from lib/duplicate.js alongside runDuplicateCheck.","status":"closed","priority":1,"issue_type":"task","assignee":"Ken Judy","owner":"kjudy@stride.build","created_at":"2026-06-09T14:20:40Z","created_by":"Ken Judy","updated_at":"2026-06-12T14:25:50Z","started_at":"2026-06-09T15:30:18Z","closed_at":"2026-06-12T14:25:50Z","close_reason":"Closed","dependencies":[{"issue_id":"code-quality-metrics-uhs","depends_on_id":"code-quality-metrics-20o","type":"blocks","created_at":"2026-06-09T10:20:50Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} diff --git a/.github/workflows/pr-metrics.yml b/.github/workflows/pr-metrics.yml index 817e15a..ca5d020 100644 --- a/.github/workflows/pr-metrics.yml +++ b/.github/workflows/pr-metrics.yml @@ -24,7 +24,8 @@ jobs: // lib/ modules are available after checkout — require() resolves from $GITHUB_WORKSPACE const { CONFIG } = require('./lib/config'); const { scoreMessageQuality, classifyDoraArchetype, isTestFile } = require('./lib/metrics'); - const { runDuplicateCheck } = require('./lib/duplicate'); + const { runDuplicateCheck, resolveModuleNeighbors } = require('./lib/duplicate'); + const { getAnthropicClient, analyzeDuplicatesWithClaude } = require('./lib/claude'); // --------------------------------------------------------------- // Fetch PR data @@ -116,6 +117,14 @@ jobs: const prodFilesCount = prodFiles.length; const staticDuplicates = runDuplicateCheck(prodFiles.map(f => f.filename)); + const anthropicClient = await getAnthropicClient(); + const semanticFindings = anthropicClient + ? await analyzeDuplicatesWithClaude( + anthropicClient, + resolveModuleNeighbors(prodFiles.map(f => f.filename)), + staticDuplicates + ) + : []; const testAdditions = testFiles.reduce((sum, f) => sum + f.additions, 0); const testDeletions = testFiles.reduce((sum, f) => sum + f.deletions, 0); @@ -307,16 +316,35 @@ jobs: `| AI Risk Signal | Net additions ratio (median) | ${medianAdditionsRatio.toFixed(2)} | <0.50 |` ].join('\n') : ''; - const duplicationSection = staticDuplicates.length > 0 ? [ - '## Duplicate Code', - '', + const layerIndicator = anthropicClient + ? '_Layer 1 (static) and Layer 2 (semantic) ran._' + : '_Layer 1 (static) ran. Set ANTHROPIC_API_KEY to enable semantic analysis._'; + + const showDuplicationSection = staticDuplicates.length > 0 || semanticFindings.length > 0 || anthropicClient !== null; + + const staticBlock = staticDuplicates.length > 0 ? [ `Found ${staticDuplicates.length} duplicate block(s) across the changed files:`, ...staticDuplicates.slice(0, 10).map(d => `- \`${d.firstFile.name}:${d.firstFile.start}-${d.firstFile.end}\` duplicates \`${d.secondFile.name}:${d.secondFile.start}-${d.secondFile.end}\` (${d.lines} lines, ${d.tokens} tokens)` ), ...(staticDuplicates.length > 10 ? [`and ${staticDuplicates.length - 10} more...`] : []) + ].join('\n') : 'No static duplicates found.'; + + const semanticBlock = semanticFindings.length > 0 ? [ + '### Semantic Similarity', + ...semanticFindings.map(f => `- \`${f.file1}\` / \`${f.file2}\`: ${f.similarity} (confidence: ${f.confidence})`) ].join('\n') : ''; + const duplicationSection = showDuplicationSection ? [ + '## Duplicate Code', + '', + staticBlock, + '', + semanticBlock, + '', + layerIndicator + ].filter(s => s !== '').join('\n') : ''; + const commentParts = [ '## PR Analysis', '', From d320a659de3a17bfa91df62779ff5e0f3ef4f61e Mon Sep 17 00:00:00 2001 From: Ken Judy Date: Fri, 12 Jun 2026 11:46:57 -0400 Subject: [PATCH 7/9] chore: replace hardcoded PDCA skill path with env var in demo/temp-reset.sh Uses PDCA_SKILL_DIR environment variable instead of an absolute path, with a guard that warns and skips the install if the directory is absent. Co-Authored-By: Claude Sonnet 4.6 --- .beads/issues.jsonl | 4 ++-- demo/temp-reset.sh | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 5f2d26d..7fc8e6a 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -1,6 +1,6 @@ {"id":"code-quality-metrics-20o","title":"feat: implement runDuplicateCheck in lib/duplicate.js","description":"Create lib/duplicate.js with runDuplicateCheck(filePaths) function. Runs jscpd via child_process using CONFIG values, reads the JSON report it writes to CONFIG.DUPLICATE_OUTPUT_DIR (a temp path), parses it, and returns a structured array.\n\nRETURN SHAPE (mirrors jscpd JSON output directly):\n Array\u003c{\n firstFile: { name: string, start: number, end: number },\n secondFile: { name: string, start: number, end: number },\n lines: number,\n tokens: number\n }\u003e\n\nJSCPD INVOCATION:\n jscpd --min-lines CONFIG.DUPLICATE_MIN_LINES --min-tokens CONFIG.DUPLICATE_MIN_TOKENS\n --ignore-pattern CONFIG.DUPLICATE_IGNORE_PATTERNS\n --reporters json --output CONFIG.DUPLICATE_OUTPUT_DIR\n [filePaths...]\n\nMOCK STRATEGY: mock execSync (child_process) for the jscpd call; mock fs.readFileSync for reading the output JSON; mock fs.existsSync to control whether the output file exists.\n\nBEHAVIORAL TEST LIST (one RED test before each GREEN):\n1. 'runDuplicateCheck returns empty array when filePaths is empty without calling jscpd'\n Predicted failure: received undefined or thrown error\n2. 'runDuplicateCheck returns empty array when jscpd output file does not exist'\n Predicted failure: received undefined or fs.readFileSync called unexpectedly\n3. 'runDuplicateCheck parses jscpd JSON and returns firstFile/secondFile/lines/tokens for each duplicate'\n Predicted failure: received [] (not yet parsing)\n4. 'runDuplicateCheck returns empty array when jscpd exits non-zero'\n Predicted failure: thrown error propagated\n\nDefinition of done: all 4 tests green, no stubs, exports runDuplicateCheck from lib/duplicate.js.","notes":"TDD ordering: test 3 (happy path parse) before test 1 (empty input). Reason: stub return [] makes tests 1, 2, 4 vacuously green — test 3 is the first genuine RED.","status":"closed","priority":1,"issue_type":"task","assignee":"Ken Judy","owner":"kjudy@stride.build","created_at":"2026-06-09T14:20:40Z","created_by":"Ken Judy","updated_at":"2026-06-09T15:30:02Z","started_at":"2026-06-09T15:17:16Z","closed_at":"2026-06-09T15:30:02Z","close_reason":"Closed","dependencies":[{"issue_id":"code-quality-metrics-20o","depends_on_id":"code-quality-metrics-een","type":"blocks","created_at":"2026-06-09T10:20:50Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":1,"dependent_count":2,"comment_count":0} {"id":"code-quality-metrics-5o3","title":"feat: integrate jscpd findings into pr-metrics.yml PR comment","description":"Require lib/duplicate.js in pr-metrics.yml. Run runDuplicateCheck against the already-computed prodFiles array (map to filenames). Add a Duplication section to the PR comment when findings exist; omit the section silently when findings array is empty.\n\nPR COMMENT SECTION FORMAT (exact strings matter for CHECK):\n ## Duplicate Code\n Found N duplicate block(s) across the changed files:\n - `path/to/file.js:10-25` duplicates `path/to/other.js:5-20` (15 lines, 120 tokens)\n (one bullet per finding, capped at 10 — add 'and N more...' if truncated)\n\nWhen no findings: section is omitted entirely (no 'No duplicates found' line).\n\nVERIFICATION (no unit tests for YAML): grep audit confirms\n - require('./lib/duplicate') appears in the script block\n - runDuplicateCheck is called with prodFiles.map(f =\u003e f.filename)\n - '## Duplicate Code' header string is present\n - commentParts.filter(Boolean) already handles optional sections (no extra guard needed)\n\nDefinition of done: grep audit passes, PR comment renders section only when findings exist.","status":"closed","priority":1,"issue_type":"task","assignee":"Ken Judy","owner":"kjudy@stride.build","created_at":"2026-06-09T14:20:40Z","created_by":"Ken Judy","updated_at":"2026-06-12T15:27:46Z","started_at":"2026-06-12T14:30:08Z","closed_at":"2026-06-12T15:27:46Z","close_reason":"Closed","dependencies":[{"issue_id":"code-quality-metrics-5o3","depends_on_id":"code-quality-metrics-20o","type":"blocks","created_at":"2026-06-09T10:20:50Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} -{"id":"code-quality-metrics-729","title":"feat: integrate semantic findings into pr-metrics.yml","description":"Extend pr-metrics.yml Duplication section to include Layer 2 semantic findings when ANTHROPIC_API_KEY is present. Degrade gracefully when key is absent — consistent with local-code-metrics.js pattern.\n\nLAYER INDICATOR STRINGS (exact, appended to Duplication section footer):\n When both layers ran:\n '_Layer 1 (static) and Layer 2 (semantic) ran._'\n When Layer 1 only (no API key):\n '_Layer 1 (static) ran. Set ANTHROPIC_API_KEY to enable semantic analysis._'\n\nSEMANTIC FINDINGS FORMAT (appended after static findings list):\n ### Semantic Similarity\n - `file1.js` / `file2.js`: similarity description (confidence: high)\n\nWhen semantic layer returns no findings: omit the Semantic Similarity subsection, still show layer indicator.\n\nVERIFICATION: grep audit confirms both indicator strings are present in the YAML; check conditional on ANTHROPIC_API_KEY mirrors existing pattern in the script block.\n\nDefinition of done: grep audit passes, both indicator strings are present, Layer 2 block is guarded by API key check.","status":"in_progress","priority":1,"issue_type":"task","assignee":"Ken Judy","owner":"kjudy@stride.build","created_at":"2026-06-09T14:20:40Z","created_by":"Ken Judy","updated_at":"2026-06-12T15:28:43Z","started_at":"2026-06-12T15:28:43Z","dependencies":[{"issue_id":"code-quality-metrics-729","depends_on_id":"code-quality-metrics-5o3","type":"blocks","created_at":"2026-06-09T10:20:50Z","created_by":"Ken Judy","metadata":"{}"},{"issue_id":"code-quality-metrics-729","depends_on_id":"code-quality-metrics-790","type":"blocks","created_at":"2026-06-09T10:20:50Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":2,"dependent_count":1,"comment_count":0} +{"id":"code-quality-metrics-729","title":"feat: integrate semantic findings into pr-metrics.yml","description":"Extend pr-metrics.yml Duplication section to include Layer 2 semantic findings when ANTHROPIC_API_KEY is present. Degrade gracefully when key is absent — consistent with local-code-metrics.js pattern.\n\nLAYER INDICATOR STRINGS (exact, appended to Duplication section footer):\n When both layers ran:\n '_Layer 1 (static) and Layer 2 (semantic) ran._'\n When Layer 1 only (no API key):\n '_Layer 1 (static) ran. Set ANTHROPIC_API_KEY to enable semantic analysis._'\n\nSEMANTIC FINDINGS FORMAT (appended after static findings list):\n ### Semantic Similarity\n - `file1.js` / `file2.js`: similarity description (confidence: high)\n\nWhen semantic layer returns no findings: omit the Semantic Similarity subsection, still show layer indicator.\n\nVERIFICATION: grep audit confirms both indicator strings are present in the YAML; check conditional on ANTHROPIC_API_KEY mirrors existing pattern in the script block.\n\nDefinition of done: grep audit passes, both indicator strings are present, Layer 2 block is guarded by API key check.","status":"closed","priority":1,"issue_type":"task","assignee":"Ken Judy","owner":"kjudy@stride.build","created_at":"2026-06-09T14:20:40Z","created_by":"Ken Judy","updated_at":"2026-06-12T15:34:46Z","started_at":"2026-06-12T15:28:43Z","closed_at":"2026-06-12T15:34:46Z","close_reason":"Closed","dependencies":[{"issue_id":"code-quality-metrics-729","depends_on_id":"code-quality-metrics-5o3","type":"blocks","created_at":"2026-06-09T10:20:50Z","created_by":"Ken Judy","metadata":"{}"},{"issue_id":"code-quality-metrics-729","depends_on_id":"code-quality-metrics-790","type":"blocks","created_at":"2026-06-09T10:20:50Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":2,"dependent_count":1,"comment_count":0} {"id":"code-quality-metrics-790","title":"feat: add semantic duplicate detection via lib/claude.js","description":"Add analyzeDuplicatesWithClaude(client, filePaths, staticFindings) to lib/claude.js (same file as existing analyzeCommitWithClaude — consistent module boundary, no new file).\n\nRETURN SCHEMA:\n Array\u003c{\n file1: string, // relative path\n file2: string, // relative path\n similarity: string, // one-sentence description of what is similar\n concern: string, // why this matters (e.g. 'same error-handling pattern repeated')\n confidence: 'high' | 'medium' | 'low'\n }\u003e\n\nSYSTEM PROMPT INTENT: Given a set of changed files and their neighbors, identify semantically similar functions or logic blocks that suggest copy-paste or AI-generated duplication — even when variable names or formatting differ. Static analysis has already flagged token-level matches (passed as staticFindings); the agent focuses on structural/conceptual similarity that jscpd would miss.\n\nBEHAVIORAL TEST LIST:\n1. 'analyzeDuplicatesWithClaude returns empty array when client is null'\n Predicted failure: throws TypeError (null client not guarded)\n2. 'analyzeDuplicatesWithClaude constructs prompt including file contents and staticFindings count'\n Predicted failure: prompt does not mention staticFindings (not yet wired)\n3. 'analyzeDuplicatesWithClaude parses structured response into file1/file2/similarity/concern/confidence'\n Predicted failure: returns raw string instead of array\n4. 'analyzeDuplicatesWithClaude returns empty array when API response is malformed'\n Predicted failure: throws JSON parse error\n\nMOCK STRATEGY: mock Anthropic client.messages.create; return fixture JSON matching return schema.\n\nDefinition of done: all 4 tests green, function exported from lib/claude.js.","status":"closed","priority":1,"issue_type":"task","owner":"kjudy@stride.build","created_at":"2026-06-09T14:20:40Z","created_by":"Ken Judy","updated_at":"2026-06-12T14:29:40Z","closed_at":"2026-06-12T14:29:40Z","close_reason":"Closed","dependencies":[{"issue_id":"code-quality-metrics-790","depends_on_id":"code-quality-metrics-uhs","type":"blocks","created_at":"2026-06-09T10:20:50Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} {"id":"code-quality-metrics-een","title":"refactor: add duplicate detection config keys to lib/config.js","description":"Add four keys to the CONFIG object in lib/config.js:\n DUPLICATE_MIN_LINES: 5 (number)\n DUPLICATE_MIN_TOKENS: 50 (number)\n DUPLICATE_IGNORE_PATTERNS: [] (array of glob strings — caller populates for their repo; TEST_FILE_PATTERNS provides the right starting reference but is not hard-coded here)\n DUPLICATE_SCAN_PATHS: [] (array of strings — empty means scan all provided file paths)\n\nNo behavioral change to any existing function.\n\nBEHAVIORAL TEST LIST (name + predicted failure):\n1. 'CONFIG.DUPLICATE_MIN_LINES defaults to 5'\n Predicted failure: received undefined (key missing)\n2. 'CONFIG.DUPLICATE_MIN_TOKENS defaults to 50'\n Predicted failure: received undefined (key missing)\n3. 'CONFIG.DUPLICATE_IGNORE_PATTERNS is an array'\n Predicted failure: received undefined (key missing)\n4. 'CONFIG.DUPLICATE_SCAN_PATHS is an array'\n Predicted failure: received undefined (key missing)\n\nDefinition of done: all 4 tests green; npm test passes with no threshold changes; no other file modified.","status":"closed","priority":1,"issue_type":"task","assignee":"Ken Judy","owner":"kjudy@stride.build","created_at":"2026-06-09T14:20:40Z","created_by":"Ken Judy","updated_at":"2026-06-09T15:15:13Z","started_at":"2026-06-09T15:06:36Z","closed_at":"2026-06-09T15:15:13Z","close_reason":"Closed","dependency_count":0,"dependent_count":2,"comment_count":0} {"id":"code-quality-metrics-uhs","title":"feat: implement resolveModuleNeighbors in lib/duplicate.js","description":"Add resolveModuleNeighbors(changedFilePaths) to lib/duplicate.js. Parses require()/import statements in each changed file using regex, resolves relative paths to absolute using path.resolve(path.dirname(sourceFile), importPath), appends .js if no extension, returns the union of changed files plus their direct (not transitive) local imports.\n\nEDGE CASE DECISIONS (made now, not during implementation):\n- Non-JS files (e.g. .yml, .json, .md): include in the returned set as-is, skip import parsing\n- Files that do not exist on disk (deleted in PR): skip with fs.existsSync check, do not throw\n- External/node_modules imports (no leading './'): ignore, do not resolve\n\nBEHAVIORAL TEST LIST:\n1. 'resolveModuleNeighbors returns changed files plus resolved local imports'\n Predicted failure: returns only the input files (import parsing not yet implemented)\n2. 'resolveModuleNeighbors skips import resolution for non-JS files'\n Predicted failure: throws or returns unexpected paths for .yml input\n3. 'resolveModuleNeighbors skips files that do not exist on disk'\n Predicted failure: throws ENOENT or returns broken paths\n4. 'resolveModuleNeighbors ignores external/node_modules imports'\n Predicted failure: includes node_modules paths or throws on resolution\n\nMOCK STRATEGY: mock fs.readFileSync for file contents; mock fs.existsSync for on-disk check.\n\nDefinition of done: all 4 tests green, function exported from lib/duplicate.js alongside runDuplicateCheck.","status":"closed","priority":1,"issue_type":"task","assignee":"Ken Judy","owner":"kjudy@stride.build","created_at":"2026-06-09T14:20:40Z","created_by":"Ken Judy","updated_at":"2026-06-12T14:25:50Z","started_at":"2026-06-09T15:30:18Z","closed_at":"2026-06-12T14:25:50Z","close_reason":"Closed","dependencies":[{"issue_id":"code-quality-metrics-uhs","depends_on_id":"code-quality-metrics-20o","type":"blocks","created_at":"2026-06-09T10:20:50Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} @@ -31,7 +31,7 @@ {"id":"code-quality-metrics-bko","title":"Step 2: Replace ratio formula in local-code-metrics.js","description":"Change line 181 from: const ratios = metrics.map(m =\u003e m.total_additions / (m.total_deletions || 1)); To: const ratios = metrics.map(m =\u003e (m.total_additions - m.total_deletions) / ((m.total_additions + m.total_deletions) || 1)); New formula is bounded [-1, +1]. Value 1.0 = all net-new code. Value 0.0 = balanced. Value -0.33 = cleanup. Human review gate after this step: Step 1 failing test must now pass.","status":"closed","priority":1,"issue_type":"task","assignee":"Ken Judy","owner":"kjudy@stride.build","created_at":"2026-04-07T12:27:24Z","created_by":"Ken Judy","updated_at":"2026-04-07T12:35:01Z","closed_at":"2026-04-07T12:35:01Z","close_reason":"Formula replaced: bounded (additions-deletions)/(additions+deletions). Zero-total guard added.","dependencies":[{"issue_id":"code-quality-metrics-bko","depends_on_id":"code-quality-metrics-zbj","type":"blocks","created_at":"2026-04-07T09:27:23Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} {"id":"code-quality-metrics-zbj","title":"Step 1: Write failing test documenting additions ratio bug","description":"TDD red phase. In __tests__/statisticalDistribution.test.js (or new additionsRatio.test.js), write a test that passes an array of all-zero-deletion commits through the ratio computation and asserts median=1.0 (or close to it), not a large number like 500. This test must FAIL before the fix is applied. It defines the bug and the fix target. Files: __tests__/statisticalDistribution.test.js, local-code-metrics.js:181","status":"closed","priority":1,"issue_type":"task","assignee":"Ken Judy","owner":"kjudy@stride.build","created_at":"2026-04-07T12:27:10Z","created_by":"Ken Judy","updated_at":"2026-04-07T12:34:26Z","closed_at":"2026-04-07T12:34:26Z","close_reason":"Failing test written and confirmed red: net_additions_ratio_median is undefined (field doesn't exist yet)","dependencies":[{"issue_id":"code-quality-metrics-zbj","depends_on_id":"code-quality-metrics-oga","type":"discovered-from","created_at":"2026-04-07T09:27:10Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":0,"dependent_count":1,"comment_count":0} {"id":"code-quality-metrics-oga","title":"Fix additions ratio metric distortion from net-new-file commits","description":"Epic: The additions ratio metric uses additions / max(deletions, 1), which yields values approaching infinity for commits with zero deletions (new files). This distorts both median and p90 of the distribution. The RETROSPECTIVE documented p90=446x as almost certainly dominated by net-new-file commits. Fix by replacing the formula with (additions - deletions) / (additions + deletions), which is bounded [-1, +1] and immune to the distortion. Rename summary fields from additions_ratio_* to net_additions_ratio_* to signal the semantic change. Update thresholds: old 3.0 warning maps to new 0.50.","status":"closed","priority":1,"issue_type":"epic","owner":"kjudy@stride.build","created_at":"2026-04-07T12:26:57Z","created_by":"Ken Judy","updated_at":"2026-04-07T12:38:17Z","closed_at":"2026-04-07T12:38:17Z","close_reason":"All 7 implementation steps complete. Formula fixed, fields renamed, spec updated, tests green.","dependency_count":0,"dependent_count":0,"comment_count":0} -{"id":"code-quality-metrics-3pa","title":"docs: update CLAUDE.md with duplicate detection config parameters","description":"Add DUPLICATE_MIN_LINES, DUPLICATE_MIN_TOKENS, DUPLICATE_IGNORE_PATTERNS, DUPLICATE_SCAN_PATHS to the Key Metrics and Configuration tables in CLAUDE.md. Include example override values for common repo conventions (Java src/test/, Python tests/, Go _test.go). Note that defaults were validated against this repo baseline (0 production duplicates).","status":"open","priority":2,"issue_type":"task","owner":"kjudy@stride.build","created_at":"2026-06-09T14:20:40Z","created_by":"Ken Judy","updated_at":"2026-06-09T14:20:40Z","dependencies":[{"issue_id":"code-quality-metrics-3pa","depends_on_id":"code-quality-metrics-een","type":"blocks","created_at":"2026-06-09T10:20:50Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} +{"id":"code-quality-metrics-3pa","title":"docs: update CLAUDE.md with duplicate detection config parameters","description":"Add DUPLICATE_MIN_LINES, DUPLICATE_MIN_TOKENS, DUPLICATE_IGNORE_PATTERNS, DUPLICATE_SCAN_PATHS to the Key Metrics and Configuration tables in CLAUDE.md. Include example override values for common repo conventions (Java src/test/, Python tests/, Go _test.go). Note that defaults were validated against this repo baseline (0 production duplicates).","status":"in_progress","priority":2,"issue_type":"task","assignee":"Ken Judy","owner":"kjudy@stride.build","created_at":"2026-06-09T14:20:40Z","created_by":"Ken Judy","updated_at":"2026-06-12T15:35:41Z","started_at":"2026-06-12T15:35:41Z","dependencies":[{"issue_id":"code-quality-metrics-3pa","depends_on_id":"code-quality-metrics-een","type":"blocks","created_at":"2026-06-09T10:20:50Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} {"id":"code-quality-metrics-a28","title":"CHECK: Verify duplicate detection against acceptance criteria","description":"Verify all acceptance criteria: config keys present with correct defaults, runDuplicateCheck returns structured findings, PR comment includes Duplication section, Layer 2 conditional on API key, CLAUDE.md updated, all tests passing, no regressions.","status":"open","priority":2,"issue_type":"task","owner":"kjudy@stride.build","created_at":"2026-06-09T14:20:40Z","created_by":"Ken Judy","updated_at":"2026-06-09T14:20:40Z","dependencies":[{"issue_id":"code-quality-metrics-a28","depends_on_id":"code-quality-metrics-3pa","type":"blocks","created_at":"2026-06-09T10:20:50Z","created_by":"Ken Judy","metadata":"{}"},{"issue_id":"code-quality-metrics-a28","depends_on_id":"code-quality-metrics-729","type":"blocks","created_at":"2026-06-09T10:20:50Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":2,"dependent_count":1,"comment_count":0} {"id":"code-quality-metrics-qsg","title":"ACT: Retrospective for duplicate detection PDCA cycle","description":"Facilitate retrospective on the duplicate detection implementation cycle using the /act skill. Store insights to the beads epic.\n\nRETROSPECTIVE FOCUS AREAS:\n1. Threshold calibration — DUPLICATE_MIN_LINES=5/DUPLICATE_MIN_TOKENS=50 produced 8 false positives on this repo at defaults; were the thresholds right for other repos (Java/Python codebases with more boilerplate)? Should defaults be higher?\n2. jscpd temp file strategy — jscpd writes to DUPLICATE_OUTPUT_DIR; is /tmp adequate or should this be a genuinely configurable path for CI environments with restricted tmp access?\n3. Layer 2 scope boundary — resolveModuleNeighbors uses direct imports only (not transitive). Was that the right call for semantic detection, or does it miss too much context?\n4. Config key documentation quality — are the four config keys (MIN_LINES, MIN_TOKENS, IGNORE_PATTERNS, SCAN_PATHS) named and described clearly enough for teams using this on unfamiliar repos?\n5. TDD discipline — note any RED tests that failed differently than predicted and what that indicated.\n\nOutputs: retrospective stored on the epic via 'bd update \u003cepic-id\u003e --notes'; any config adjustments captured as follow-on issues if warranted.\n\nDefinition of done: /act skill facilitated, insights stored to epic, any new issues created for follow-on work.","status":"open","priority":2,"issue_type":"task","owner":"kjudy@stride.build","created_at":"2026-06-09T14:20:40Z","created_by":"Ken Judy","updated_at":"2026-06-09T14:59:41Z","dependencies":[{"issue_id":"code-quality-metrics-qsg","depends_on_id":"code-quality-metrics-a28","type":"blocks","created_at":"2026-06-09T10:20:50Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} {"id":"code-quality-metrics-8vg","title":"Update code-metrics.yml churn calculation","description":"Mirror the local-code-metrics.js change in the GitHub Actions workflow. In code-metrics.yml: replace the ratio formula (line ~234: m.total_additions / (m.total_deletions || 1)) with the bounded churn_intensity formula. Rename output fields additions_ratio_median/additions_ratio_p90 to churn_intensity_median/churn_intensity_p90. Update the threshold check (currently \u003c 3.0) to reflect the new [0, 0.5] range.","status":"closed","priority":2,"issue_type":"task","assignee":"Ken Judy","owner":"kjudy@stride.build","created_at":"2026-06-08T20:36:14Z","created_by":"Ken Judy","updated_at":"2026-06-08T20:47:44Z","started_at":"2026-06-08T20:45:01Z","closed_at":"2026-06-08T20:47:44Z","close_reason":"Closed","dependencies":[{"issue_id":"code-quality-metrics-8vg","depends_on_id":"code-quality-metrics-141","type":"blocks","created_at":"2026-06-08T16:36:27Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} diff --git a/demo/temp-reset.sh b/demo/temp-reset.sh index ed5f0de..08d7e09 100644 --- a/demo/temp-reset.sh +++ b/demo/temp-reset.sh @@ -4,4 +4,8 @@ DEMO_SHA="267fa1d19840b5a7215d6aac59dacb42df223bea" git stash git branch -f "$DEMO_BRANCH" "$DEMO_SHA" git switch "$DEMO_BRANCH" -echo "y" | "/Users/kenjudy/ObsidianVaults/PDCA Process/claude-skill/install-skill.sh" personal +if [[ -d "${PDCA_SKILL_DIR}" ]]; then + echo "y" | "${PDCA_SKILL_DIR}/install-skill.sh" personal +else + echo "Warning: PDCA_SKILL_DIR not found (${PDCA_SKILL_DIR}), skipping skill install." +fi From d60131470f02a979937ef852561730dd9441daec Mon Sep 17 00:00:00 2001 From: Ken Judy Date: Fri, 12 Jun 2026 11:48:06 -0400 Subject: [PATCH 8/9] docs: update CLAUDE.md with duplicate detection config parameters Adds four DUPLICATE_* keys to the configuration table with defaults and descriptions. Includes a tuning guide with example overrides for Java, Python, and Go repos where boilerplate inflates false positives. Updates the pr-metrics.yml architecture description to reflect the lib/duplicate.js and lib/claude.js dependencies added in this cycle. Co-Authored-By: Claude Sonnet 4.6 --- CLAUDE.md | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index e22804e..9bbf99b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -53,7 +53,7 @@ Three public components sharing pure-computation logic via `lib/`: 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. 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. +3. **`.github/workflows/pr-metrics.yml`**: Per-PR GitHub Actions workflow. Requires `lib/config.js`, `lib/metrics.js`, `lib/duplicate.js`, and `lib/claude.js` via `require()`. Posts a detailed comment on each PR with commit-by-commit analysis, test adequacy, development pattern detection, and two-layer duplicate code detection (Layer 1 jscpd always-on; Layer 2 semantic via Claude when `ANTHROPIC_API_KEY` is set). ## Key Metrics and Thresholds @@ -101,9 +101,34 @@ Thresholds are configured in the `CONFIG` object in `lib/config.js`, which is th | `AI_ANALYSIS_MAX_COMMITS` | 5 | Max commits sent to Claude per run | | `AI_DIFF_MAX_CHARS` | 4000 | Diff truncation limit for Claude API calls | | `AI_RISK_ADDITIONS_RATIO` | 3 | Additions/deletions multiplier for Claude pre-filter | +| `DUPLICATE_MIN_LINES` | 5 | Minimum lines for jscpd to flag a duplicate block | +| `DUPLICATE_MIN_TOKENS` | 50 | Minimum tokens for jscpd to flag a duplicate block | +| `DUPLICATE_IGNORE_PATTERNS` | `[]` | Glob patterns for jscpd to ignore (e.g. generated files) | +| `DUPLICATE_SCAN_PATHS` | `[]` | Paths jscpd scans; empty means all provided file paths | 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. +### Duplicate Detection Tuning + +The defaults (`DUPLICATE_MIN_LINES: 5`, `DUPLICATE_MIN_TOKENS: 50`) are calibrated for JavaScript. Other languages typically need higher thresholds to suppress boilerplate false positives: + +```js +// Java — longer method signatures and boilerplate +DUPLICATE_MIN_LINES: 10, +DUPLICATE_MIN_TOKENS: 100, +DUPLICATE_IGNORE_PATTERNS: ['**/*Test.java', '**/generated/**'], + +// Python — decorators and docstrings inflate token counts +DUPLICATE_MIN_LINES: 8, +DUPLICATE_MIN_TOKENS: 80, +DUPLICATE_IGNORE_PATTERNS: ['**/migrations/**', '**/__pycache__/**'], + +// Go — interface implementations repeat predictably +DUPLICATE_MIN_LINES: 8, +DUPLICATE_MIN_TOKENS: 75, +DUPLICATE_IGNORE_PATTERNS: ['**/*_test.go', '**/vendor/**'], +``` + ## Workflow Permissions The GitHub workflows require: From d5d839ce8c840ec946fc5345f9a7763f9acb3dcf Mon Sep 17 00:00:00 2001 From: Ken Judy Date: Fri, 12 Jun 2026 12:00:44 -0400 Subject: [PATCH 9/9] chore: sync beads task state after PDCA cycle completion Co-Authored-By: Claude Sonnet 4.6 --- .beads/interactions.jsonl | 8 ++++++++ .beads/issues.jsonl | 8 ++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/.beads/interactions.jsonl b/.beads/interactions.jsonl index 6b428d8..4d5bef9 100644 --- a/.beads/interactions.jsonl +++ b/.beads/interactions.jsonl @@ -24,3 +24,11 @@ {"id":"int-ac0d33c7","kind":"field_change","created_at":"2026-04-07T15:23:06.286044Z","actor":"Ken Judy","issue_id":"code-quality-metrics-cek","extra":{"field":"status","new_value":"closed","old_value":"in_progress"}} {"id":"int-938d7ec1","kind":"field_change","created_at":"2026-06-09T15:15:13.208542Z","actor":"Ken Judy","issue_id":"code-quality-metrics-een","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}} {"id":"int-0076bf03","kind":"field_change","created_at":"2026-06-09T15:30:02.111356Z","actor":"Ken Judy","issue_id":"code-quality-metrics-20o","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}} +{"id":"int-38edcf94","kind":"field_change","created_at":"2026-06-12T14:25:49.983553Z","actor":"Ken Judy","issue_id":"code-quality-metrics-uhs","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}} +{"id":"int-ba832b82","kind":"field_change","created_at":"2026-06-12T14:29:40.015721Z","actor":"Ken Judy","issue_id":"code-quality-metrics-790","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Closed"}} +{"id":"int-b34a5661","kind":"field_change","created_at":"2026-06-12T15:27:45.761818Z","actor":"Ken Judy","issue_id":"code-quality-metrics-5o3","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}} +{"id":"int-1aadd2c0","kind":"field_change","created_at":"2026-06-12T15:34:45.826228Z","actor":"Ken Judy","issue_id":"code-quality-metrics-729","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}} +{"id":"int-9340d4cf","kind":"field_change","created_at":"2026-06-12T15:48:14.57581Z","actor":"Ken Judy","issue_id":"code-quality-metrics-3pa","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}} +{"id":"int-b4942b1f","kind":"field_change","created_at":"2026-06-12T15:52:30.533678Z","actor":"Ken Judy","issue_id":"code-quality-metrics-a28","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}} +{"id":"int-4ff6dea3","kind":"field_change","created_at":"2026-06-12T16:00:15.448713Z","actor":"Ken Judy","issue_id":"code-quality-metrics-qsg","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}} +{"id":"int-efeb5ce0","kind":"field_change","created_at":"2026-06-12T16:00:17.634271Z","actor":"Ken Judy","issue_id":"code-quality-metrics-aat","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Closed"}} diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 7fc8e6a..9eb70fb 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -4,7 +4,7 @@ {"id":"code-quality-metrics-790","title":"feat: add semantic duplicate detection via lib/claude.js","description":"Add analyzeDuplicatesWithClaude(client, filePaths, staticFindings) to lib/claude.js (same file as existing analyzeCommitWithClaude — consistent module boundary, no new file).\n\nRETURN SCHEMA:\n Array\u003c{\n file1: string, // relative path\n file2: string, // relative path\n similarity: string, // one-sentence description of what is similar\n concern: string, // why this matters (e.g. 'same error-handling pattern repeated')\n confidence: 'high' | 'medium' | 'low'\n }\u003e\n\nSYSTEM PROMPT INTENT: Given a set of changed files and their neighbors, identify semantically similar functions or logic blocks that suggest copy-paste or AI-generated duplication — even when variable names or formatting differ. Static analysis has already flagged token-level matches (passed as staticFindings); the agent focuses on structural/conceptual similarity that jscpd would miss.\n\nBEHAVIORAL TEST LIST:\n1. 'analyzeDuplicatesWithClaude returns empty array when client is null'\n Predicted failure: throws TypeError (null client not guarded)\n2. 'analyzeDuplicatesWithClaude constructs prompt including file contents and staticFindings count'\n Predicted failure: prompt does not mention staticFindings (not yet wired)\n3. 'analyzeDuplicatesWithClaude parses structured response into file1/file2/similarity/concern/confidence'\n Predicted failure: returns raw string instead of array\n4. 'analyzeDuplicatesWithClaude returns empty array when API response is malformed'\n Predicted failure: throws JSON parse error\n\nMOCK STRATEGY: mock Anthropic client.messages.create; return fixture JSON matching return schema.\n\nDefinition of done: all 4 tests green, function exported from lib/claude.js.","status":"closed","priority":1,"issue_type":"task","owner":"kjudy@stride.build","created_at":"2026-06-09T14:20:40Z","created_by":"Ken Judy","updated_at":"2026-06-12T14:29:40Z","closed_at":"2026-06-12T14:29:40Z","close_reason":"Closed","dependencies":[{"issue_id":"code-quality-metrics-790","depends_on_id":"code-quality-metrics-uhs","type":"blocks","created_at":"2026-06-09T10:20:50Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} {"id":"code-quality-metrics-een","title":"refactor: add duplicate detection config keys to lib/config.js","description":"Add four keys to the CONFIG object in lib/config.js:\n DUPLICATE_MIN_LINES: 5 (number)\n DUPLICATE_MIN_TOKENS: 50 (number)\n DUPLICATE_IGNORE_PATTERNS: [] (array of glob strings — caller populates for their repo; TEST_FILE_PATTERNS provides the right starting reference but is not hard-coded here)\n DUPLICATE_SCAN_PATHS: [] (array of strings — empty means scan all provided file paths)\n\nNo behavioral change to any existing function.\n\nBEHAVIORAL TEST LIST (name + predicted failure):\n1. 'CONFIG.DUPLICATE_MIN_LINES defaults to 5'\n Predicted failure: received undefined (key missing)\n2. 'CONFIG.DUPLICATE_MIN_TOKENS defaults to 50'\n Predicted failure: received undefined (key missing)\n3. 'CONFIG.DUPLICATE_IGNORE_PATTERNS is an array'\n Predicted failure: received undefined (key missing)\n4. 'CONFIG.DUPLICATE_SCAN_PATHS is an array'\n Predicted failure: received undefined (key missing)\n\nDefinition of done: all 4 tests green; npm test passes with no threshold changes; no other file modified.","status":"closed","priority":1,"issue_type":"task","assignee":"Ken Judy","owner":"kjudy@stride.build","created_at":"2026-06-09T14:20:40Z","created_by":"Ken Judy","updated_at":"2026-06-09T15:15:13Z","started_at":"2026-06-09T15:06:36Z","closed_at":"2026-06-09T15:15:13Z","close_reason":"Closed","dependency_count":0,"dependent_count":2,"comment_count":0} {"id":"code-quality-metrics-uhs","title":"feat: implement resolveModuleNeighbors in lib/duplicate.js","description":"Add resolveModuleNeighbors(changedFilePaths) to lib/duplicate.js. Parses require()/import statements in each changed file using regex, resolves relative paths to absolute using path.resolve(path.dirname(sourceFile), importPath), appends .js if no extension, returns the union of changed files plus their direct (not transitive) local imports.\n\nEDGE CASE DECISIONS (made now, not during implementation):\n- Non-JS files (e.g. .yml, .json, .md): include in the returned set as-is, skip import parsing\n- Files that do not exist on disk (deleted in PR): skip with fs.existsSync check, do not throw\n- External/node_modules imports (no leading './'): ignore, do not resolve\n\nBEHAVIORAL TEST LIST:\n1. 'resolveModuleNeighbors returns changed files plus resolved local imports'\n Predicted failure: returns only the input files (import parsing not yet implemented)\n2. 'resolveModuleNeighbors skips import resolution for non-JS files'\n Predicted failure: throws or returns unexpected paths for .yml input\n3. 'resolveModuleNeighbors skips files that do not exist on disk'\n Predicted failure: throws ENOENT or returns broken paths\n4. 'resolveModuleNeighbors ignores external/node_modules imports'\n Predicted failure: includes node_modules paths or throws on resolution\n\nMOCK STRATEGY: mock fs.readFileSync for file contents; mock fs.existsSync for on-disk check.\n\nDefinition of done: all 4 tests green, function exported from lib/duplicate.js alongside runDuplicateCheck.","status":"closed","priority":1,"issue_type":"task","assignee":"Ken Judy","owner":"kjudy@stride.build","created_at":"2026-06-09T14:20:40Z","created_by":"Ken Judy","updated_at":"2026-06-12T14:25:50Z","started_at":"2026-06-09T15:30:18Z","closed_at":"2026-06-12T14:25:50Z","close_reason":"Closed","dependencies":[{"issue_id":"code-quality-metrics-uhs","depends_on_id":"code-quality-metrics-20o","type":"blocks","created_at":"2026-06-09T10:20:50Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} -{"id":"code-quality-metrics-aat","title":"Feature: Two-layer duplicate code detection in PR check (#42)","description":"Add jscpd static detection (Layer 1, always-on) and agent-based semantic detection (Layer 2, conditional on ANTHROPIC_API_KEY) to pr-metrics.yml. All thresholds and ignore patterns live in lib/config.js as documented configurable parameters so the toolkit works across repos with different test conventions and directory structures.","notes":"Analysis: lib/config.js is single source of truth for all thresholds. lib/duplicate.js follows PR #33 pattern (require() in workflow). jscpd v5 uses --ignore-pattern flag, JSON output via --reporters json. Baseline: 0 duplicates in this repo production code at defaults. Other repos need to override DUPLICATE_IGNORE_PATTERNS. Layer 2 conditional on ANTHROPIC_API_KEY. Deliver Layer 1 first.","status":"open","priority":1,"issue_type":"epic","owner":"kjudy@stride.build","created_at":"2026-06-09T14:19:59Z","created_by":"Ken Judy","updated_at":"2026-06-09T14:20:18Z","dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"code-quality-metrics-aat","title":"Feature: Two-layer duplicate code detection in PR check (#42)","description":"Add jscpd static detection (Layer 1, always-on) and agent-based semantic detection (Layer 2, conditional on ANTHROPIC_API_KEY) to pr-metrics.yml. All thresholds and ignore patterns live in lib/config.js as documented configurable parameters so the toolkit works across repos with different test conventions and directory structures.","notes":"Analysis: lib/config.js is single source of truth for all thresholds. lib/duplicate.js follows PR #33 pattern (require() in workflow). jscpd v5 uses --ignore-pattern flag, JSON output via --reporters json. Baseline: 0 duplicates in this repo production code at defaults. Other repos need to override DUPLICATE_IGNORE_PATTERNS. Layer 2 conditional on ANTHROPIC_API_KEY. Deliver Layer 1 first.\n## ACT Retrospective — Two-Layer Duplicate Detection\n\n**Key insight:** Task creation and task specification were collapsed into one pass during PLAN. The design questions (test lists, edge-case decisions, return schemas) were deferred until a mid-PLAN review question surfaced them. Without that question, DO would have started with underspecified tasks.\n\n**Root cause:** The plan-beads-addon.md guidance described creating tasks but did not require behavioral acceptance criteria at creation time.\n\n**Committed change:** Added 'Create Implementation Tasks with Behavioral Acceptance Criteria' section to plan-beads-addon.md in both installed skill and source repo. Each task now requires --acceptance (behavioral test list + edge-case decisions + definition of done) and --validate at creation time.\n\n**Threshold calibration note:** DUPLICATE_MIN_LINES=5/DUPLICATE_MIN_TOKENS=50 correct for JS. Java/Python/Go need higher defaults per CLAUDE.md tuning guide.\n\n**Design trade-off flagged:** showDuplicationSection evaluates true whenever anthropicClient is not null, meaning the Duplication section renders for clean PRs when API key is set. Deliberate — confirms the layer ran — but creates per-PR noise. Revisit if teams find it distracting.","status":"open","priority":1,"issue_type":"epic","owner":"kjudy@stride.build","created_at":"2026-06-09T14:19:59Z","created_by":"Ken Judy","updated_at":"2026-06-12T16:00:11Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"code-quality-metrics-141","title":"Update churn distribution in local-code-metrics.js","description":"TDD: update collectLocalMetrics.test.js first to expect churn_intensity_median and churn_intensity_p90 instead of additions_ratio_median and additions_ratio_p90, then implement. Replace local-code-metrics.js:181 ratio array with: metrics.map(m =\u003e Math.min(m.total_additions, m.total_deletions) / Math.max(m.total_additions + m.total_deletions, 1)). Rename summary fields from additions_ratio_median/additions_ratio_p90 to churn_intensity_median/churn_intensity_p90.","status":"closed","priority":1,"issue_type":"task","assignee":"Ken Judy","owner":"kjudy@stride.build","created_at":"2026-06-08T20:36:14Z","created_by":"Ken Judy","updated_at":"2026-06-08T20:44:27Z","started_at":"2026-06-08T20:43:05Z","closed_at":"2026-06-08T20:44:27Z","close_reason":"Closed","dependencies":[{"issue_id":"code-quality-metrics-141","depends_on_id":"code-quality-metrics-4ck","type":"blocks","created_at":"2026-06-08T16:36:27Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":1,"dependent_count":3,"comment_count":0} {"id":"code-quality-metrics-4ck","title":"Replace change_ratio with churn_intensity in lib/git.js","description":"TDD: write failing tests first, then implement. Replace the string field change_ratio (which emits 'inf' for zero-deletion commits) with a numeric churn_intensity field using the formula: min(additions, deletions) / max(additions + deletions, 1). This is bounded [0.0, 0.5] and is always a number. Zero means pure addition or pure deletion; 0.5 means equal adds and deletes (maximum rework). Update __tests__/analyzeCommit.test.js: replace the two tests that assert change_ratio='inf' and change_ratio='2.00' with tests for churn_intensity. Add cases: no-deletions yields 0.0; equal adds/deletes yields 0.5; typical 10 adds / 5 deletes yields 0.333.","status":"closed","priority":1,"issue_type":"task","assignee":"Ken Judy","owner":"kjudy@stride.build","created_at":"2026-06-08T20:36:14Z","created_by":"Ken Judy","updated_at":"2026-06-08T20:42:20Z","started_at":"2026-06-08T20:40:00Z","closed_at":"2026-06-08T20:42:20Z","close_reason":"Closed","dependencies":[{"issue_id":"code-quality-metrics-4ck","depends_on_id":"code-quality-metrics-di7","type":"blocks","created_at":"2026-06-08T16:36:27Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} {"id":"code-quality-metrics-di7","title":"Fix unbounded churn ratio for statistically valid p50/p90","description":"The additions/deletions ratio (additions / max(deletions, 1)) is unbounded. A commit of entirely new files yields ratio = additions/1, making p50/p90 meaningless (observed p90=446x). Replace with churn_intensity = min(additions, deletions) / max(additions + deletions, 1), which is bounded [0, 0.5] and measures rework fraction directly. This is more important than backwards compatibility with existing field names.","status":"closed","priority":1,"issue_type":"feature","assignee":"Ken Judy","owner":"kjudy@stride.build","created_at":"2026-06-08T20:35:53Z","created_by":"Ken Judy","updated_at":"2026-06-08T20:47:45Z","started_at":"2026-06-08T20:39:59Z","closed_at":"2026-06-08T20:47:45Z","close_reason":"Closed","dependency_count":0,"dependent_count":1,"comment_count":0} @@ -31,9 +31,9 @@ {"id":"code-quality-metrics-bko","title":"Step 2: Replace ratio formula in local-code-metrics.js","description":"Change line 181 from: const ratios = metrics.map(m =\u003e m.total_additions / (m.total_deletions || 1)); To: const ratios = metrics.map(m =\u003e (m.total_additions - m.total_deletions) / ((m.total_additions + m.total_deletions) || 1)); New formula is bounded [-1, +1]. Value 1.0 = all net-new code. Value 0.0 = balanced. Value -0.33 = cleanup. Human review gate after this step: Step 1 failing test must now pass.","status":"closed","priority":1,"issue_type":"task","assignee":"Ken Judy","owner":"kjudy@stride.build","created_at":"2026-04-07T12:27:24Z","created_by":"Ken Judy","updated_at":"2026-04-07T12:35:01Z","closed_at":"2026-04-07T12:35:01Z","close_reason":"Formula replaced: bounded (additions-deletions)/(additions+deletions). Zero-total guard added.","dependencies":[{"issue_id":"code-quality-metrics-bko","depends_on_id":"code-quality-metrics-zbj","type":"blocks","created_at":"2026-04-07T09:27:23Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} {"id":"code-quality-metrics-zbj","title":"Step 1: Write failing test documenting additions ratio bug","description":"TDD red phase. In __tests__/statisticalDistribution.test.js (or new additionsRatio.test.js), write a test that passes an array of all-zero-deletion commits through the ratio computation and asserts median=1.0 (or close to it), not a large number like 500. This test must FAIL before the fix is applied. It defines the bug and the fix target. Files: __tests__/statisticalDistribution.test.js, local-code-metrics.js:181","status":"closed","priority":1,"issue_type":"task","assignee":"Ken Judy","owner":"kjudy@stride.build","created_at":"2026-04-07T12:27:10Z","created_by":"Ken Judy","updated_at":"2026-04-07T12:34:26Z","closed_at":"2026-04-07T12:34:26Z","close_reason":"Failing test written and confirmed red: net_additions_ratio_median is undefined (field doesn't exist yet)","dependencies":[{"issue_id":"code-quality-metrics-zbj","depends_on_id":"code-quality-metrics-oga","type":"discovered-from","created_at":"2026-04-07T09:27:10Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":0,"dependent_count":1,"comment_count":0} {"id":"code-quality-metrics-oga","title":"Fix additions ratio metric distortion from net-new-file commits","description":"Epic: The additions ratio metric uses additions / max(deletions, 1), which yields values approaching infinity for commits with zero deletions (new files). This distorts both median and p90 of the distribution. The RETROSPECTIVE documented p90=446x as almost certainly dominated by net-new-file commits. Fix by replacing the formula with (additions - deletions) / (additions + deletions), which is bounded [-1, +1] and immune to the distortion. Rename summary fields from additions_ratio_* to net_additions_ratio_* to signal the semantic change. Update thresholds: old 3.0 warning maps to new 0.50.","status":"closed","priority":1,"issue_type":"epic","owner":"kjudy@stride.build","created_at":"2026-04-07T12:26:57Z","created_by":"Ken Judy","updated_at":"2026-04-07T12:38:17Z","closed_at":"2026-04-07T12:38:17Z","close_reason":"All 7 implementation steps complete. Formula fixed, fields renamed, spec updated, tests green.","dependency_count":0,"dependent_count":0,"comment_count":0} -{"id":"code-quality-metrics-3pa","title":"docs: update CLAUDE.md with duplicate detection config parameters","description":"Add DUPLICATE_MIN_LINES, DUPLICATE_MIN_TOKENS, DUPLICATE_IGNORE_PATTERNS, DUPLICATE_SCAN_PATHS to the Key Metrics and Configuration tables in CLAUDE.md. Include example override values for common repo conventions (Java src/test/, Python tests/, Go _test.go). Note that defaults were validated against this repo baseline (0 production duplicates).","status":"in_progress","priority":2,"issue_type":"task","assignee":"Ken Judy","owner":"kjudy@stride.build","created_at":"2026-06-09T14:20:40Z","created_by":"Ken Judy","updated_at":"2026-06-12T15:35:41Z","started_at":"2026-06-12T15:35:41Z","dependencies":[{"issue_id":"code-quality-metrics-3pa","depends_on_id":"code-quality-metrics-een","type":"blocks","created_at":"2026-06-09T10:20:50Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} -{"id":"code-quality-metrics-a28","title":"CHECK: Verify duplicate detection against acceptance criteria","description":"Verify all acceptance criteria: config keys present with correct defaults, runDuplicateCheck returns structured findings, PR comment includes Duplication section, Layer 2 conditional on API key, CLAUDE.md updated, all tests passing, no regressions.","status":"open","priority":2,"issue_type":"task","owner":"kjudy@stride.build","created_at":"2026-06-09T14:20:40Z","created_by":"Ken Judy","updated_at":"2026-06-09T14:20:40Z","dependencies":[{"issue_id":"code-quality-metrics-a28","depends_on_id":"code-quality-metrics-3pa","type":"blocks","created_at":"2026-06-09T10:20:50Z","created_by":"Ken Judy","metadata":"{}"},{"issue_id":"code-quality-metrics-a28","depends_on_id":"code-quality-metrics-729","type":"blocks","created_at":"2026-06-09T10:20:50Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":2,"dependent_count":1,"comment_count":0} -{"id":"code-quality-metrics-qsg","title":"ACT: Retrospective for duplicate detection PDCA cycle","description":"Facilitate retrospective on the duplicate detection implementation cycle using the /act skill. Store insights to the beads epic.\n\nRETROSPECTIVE FOCUS AREAS:\n1. Threshold calibration — DUPLICATE_MIN_LINES=5/DUPLICATE_MIN_TOKENS=50 produced 8 false positives on this repo at defaults; were the thresholds right for other repos (Java/Python codebases with more boilerplate)? Should defaults be higher?\n2. jscpd temp file strategy — jscpd writes to DUPLICATE_OUTPUT_DIR; is /tmp adequate or should this be a genuinely configurable path for CI environments with restricted tmp access?\n3. Layer 2 scope boundary — resolveModuleNeighbors uses direct imports only (not transitive). Was that the right call for semantic detection, or does it miss too much context?\n4. Config key documentation quality — are the four config keys (MIN_LINES, MIN_TOKENS, IGNORE_PATTERNS, SCAN_PATHS) named and described clearly enough for teams using this on unfamiliar repos?\n5. TDD discipline — note any RED tests that failed differently than predicted and what that indicated.\n\nOutputs: retrospective stored on the epic via 'bd update \u003cepic-id\u003e --notes'; any config adjustments captured as follow-on issues if warranted.\n\nDefinition of done: /act skill facilitated, insights stored to epic, any new issues created for follow-on work.","status":"open","priority":2,"issue_type":"task","owner":"kjudy@stride.build","created_at":"2026-06-09T14:20:40Z","created_by":"Ken Judy","updated_at":"2026-06-09T14:59:41Z","dependencies":[{"issue_id":"code-quality-metrics-qsg","depends_on_id":"code-quality-metrics-a28","type":"blocks","created_at":"2026-06-09T10:20:50Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} +{"id":"code-quality-metrics-3pa","title":"docs: update CLAUDE.md with duplicate detection config parameters","description":"Add DUPLICATE_MIN_LINES, DUPLICATE_MIN_TOKENS, DUPLICATE_IGNORE_PATTERNS, DUPLICATE_SCAN_PATHS to the Key Metrics and Configuration tables in CLAUDE.md. Include example override values for common repo conventions (Java src/test/, Python tests/, Go _test.go). Note that defaults were validated against this repo baseline (0 production duplicates).","status":"closed","priority":2,"issue_type":"task","assignee":"Ken Judy","owner":"kjudy@stride.build","created_at":"2026-06-09T14:20:40Z","created_by":"Ken Judy","updated_at":"2026-06-12T15:48:15Z","started_at":"2026-06-12T15:35:41Z","closed_at":"2026-06-12T15:48:15Z","close_reason":"Closed","dependencies":[{"issue_id":"code-quality-metrics-3pa","depends_on_id":"code-quality-metrics-een","type":"blocks","created_at":"2026-06-09T10:20:50Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} +{"id":"code-quality-metrics-a28","title":"CHECK: Verify duplicate detection against acceptance criteria","description":"Verify all acceptance criteria: config keys present with correct defaults, runDuplicateCheck returns structured findings, PR comment includes Duplication section, Layer 2 conditional on API key, CLAUDE.md updated, all tests passing, no regressions.","status":"closed","priority":2,"issue_type":"task","assignee":"Ken Judy","owner":"kjudy@stride.build","created_at":"2026-06-09T14:20:40Z","created_by":"Ken Judy","updated_at":"2026-06-12T15:52:31Z","started_at":"2026-06-12T15:48:33Z","closed_at":"2026-06-12T15:52:31Z","close_reason":"Closed","dependencies":[{"issue_id":"code-quality-metrics-a28","depends_on_id":"code-quality-metrics-3pa","type":"blocks","created_at":"2026-06-09T10:20:50Z","created_by":"Ken Judy","metadata":"{}"},{"issue_id":"code-quality-metrics-a28","depends_on_id":"code-quality-metrics-729","type":"blocks","created_at":"2026-06-09T10:20:50Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":2,"dependent_count":1,"comment_count":0} +{"id":"code-quality-metrics-qsg","title":"ACT: Retrospective for duplicate detection PDCA cycle","description":"Facilitate retrospective on the duplicate detection implementation cycle using the /act skill. Store insights to the beads epic.\n\nRETROSPECTIVE FOCUS AREAS:\n1. Threshold calibration — DUPLICATE_MIN_LINES=5/DUPLICATE_MIN_TOKENS=50 produced 8 false positives on this repo at defaults; were the thresholds right for other repos (Java/Python codebases with more boilerplate)? Should defaults be higher?\n2. jscpd temp file strategy — jscpd writes to DUPLICATE_OUTPUT_DIR; is /tmp adequate or should this be a genuinely configurable path for CI environments with restricted tmp access?\n3. Layer 2 scope boundary — resolveModuleNeighbors uses direct imports only (not transitive). Was that the right call for semantic detection, or does it miss too much context?\n4. Config key documentation quality — are the four config keys (MIN_LINES, MIN_TOKENS, IGNORE_PATTERNS, SCAN_PATHS) named and described clearly enough for teams using this on unfamiliar repos?\n5. TDD discipline — note any RED tests that failed differently than predicted and what that indicated.\n\nOutputs: retrospective stored on the epic via 'bd update \u003cepic-id\u003e --notes'; any config adjustments captured as follow-on issues if warranted.\n\nDefinition of done: /act skill facilitated, insights stored to epic, any new issues created for follow-on work.","status":"in_progress","priority":2,"issue_type":"task","assignee":"Ken Judy","owner":"kjudy@stride.build","created_at":"2026-06-09T14:20:40Z","created_by":"Ken Judy","updated_at":"2026-06-12T15:53:15Z","started_at":"2026-06-12T15:53:15Z","dependencies":[{"issue_id":"code-quality-metrics-qsg","depends_on_id":"code-quality-metrics-a28","type":"blocks","created_at":"2026-06-09T10:20:50Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} {"id":"code-quality-metrics-8vg","title":"Update code-metrics.yml churn calculation","description":"Mirror the local-code-metrics.js change in the GitHub Actions workflow. In code-metrics.yml: replace the ratio formula (line ~234: m.total_additions / (m.total_deletions || 1)) with the bounded churn_intensity formula. Rename output fields additions_ratio_median/additions_ratio_p90 to churn_intensity_median/churn_intensity_p90. Update the threshold check (currently \u003c 3.0) to reflect the new [0, 0.5] range.","status":"closed","priority":2,"issue_type":"task","assignee":"Ken Judy","owner":"kjudy@stride.build","created_at":"2026-06-08T20:36:14Z","created_by":"Ken Judy","updated_at":"2026-06-08T20:47:44Z","started_at":"2026-06-08T20:45:01Z","closed_at":"2026-06-08T20:47:44Z","close_reason":"Closed","dependencies":[{"issue_id":"code-quality-metrics-8vg","depends_on_id":"code-quality-metrics-141","type":"blocks","created_at":"2026-06-08T16:36:27Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} {"id":"code-quality-metrics-ac9","title":"Update pr-metrics.yml churn calculation","description":"Mirror the change in pr-metrics.yml. Replace per-commit additions_ratio field with churn_intensity. Update the median computation (~line 160) and all threshold checks (currently \u003e 3.0). Rename all display references from additions ratio to churn intensity in the PR comment output.","status":"closed","priority":2,"issue_type":"task","assignee":"Ken Judy","owner":"kjudy@stride.build","created_at":"2026-06-08T20:36:14Z","created_by":"Ken Judy","updated_at":"2026-06-08T20:47:44Z","started_at":"2026-06-08T20:45:01Z","closed_at":"2026-06-08T20:47:44Z","close_reason":"Closed","dependencies":[{"issue_id":"code-quality-metrics-ac9","depends_on_id":"code-quality-metrics-141","type":"blocks","created_at":"2026-06-08T16:36:27Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} {"id":"code-quality-metrics-aww","title":"Update CLAUDE.md health threshold table for churn_intensity","description":"Update the health threshold table row: rename 'Additions-to-deletions ratio (median)' to 'Churn intensity (median)' and change the threshold from '\u003c3.0' to '\u003e0.05' (low churn = mostly net-new code = AI drift signal; high churn = rework = healthy refactoring). Also update the Key Metrics section description if present.","status":"closed","priority":2,"issue_type":"task","assignee":"Ken Judy","owner":"kjudy@stride.build","created_at":"2026-06-08T20:36:14Z","created_by":"Ken Judy","updated_at":"2026-06-08T20:47:44Z","started_at":"2026-06-08T20:45:01Z","closed_at":"2026-06-08T20:47:44Z","close_reason":"Closed","dependencies":[{"issue_id":"code-quality-metrics-aww","depends_on_id":"code-quality-metrics-141","type":"blocks","created_at":"2026-06-08T16:36:27Z","created_by":"Ken Judy","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0}