From f60cc537bf52d463d9c8ae4fc7b7f884cc98fa63 Mon Sep 17 00:00:00 2001 From: Doug Fennell Date: Sun, 9 Nov 2025 15:19:00 -0600 Subject: [PATCH 1/5] docs(context): add telemetry guide; ci: add non-blocking context-aware telemetry step (burn-in) --- .github/workflows/ci-ratchet.yml | 10 + analysis-current.json | 862 +++++++++++++++++++++++++++++++ docs/CONTEXT-AWARE-LINTING.md | 37 ++ 3 files changed, 909 insertions(+) create mode 100644 analysis-current.json create mode 100644 docs/CONTEXT-AWARE-LINTING.md diff --git a/.github/workflows/ci-ratchet.yml b/.github/workflows/ci-ratchet.yml index 7ae52a2..261b885 100644 --- a/.github/workflows/ci-ratchet.yml +++ b/.github/workflows/ci-ratchet.yml @@ -28,6 +28,16 @@ jobs: - name: Ratchet (fail on new debt) run: npm run ratchet + - name: Context-aware ratchet (telemetry only) + continue-on-error: true + run: | + # Prefer script if available, otherwise fallback to direct invocation + if npm run -s | grep -q "ratchet:context"; then + npm run ratchet:context || true + else + node scripts/ratchet.js --mode=context --baseline=analysis-baseline.json --current=analysis-current.json || true + fi + - name: Run tests run: npm test diff --git a/analysis-current.json b/analysis-current.json new file mode 100644 index 0000000..2a9daf4 --- /dev/null +++ b/analysis-current.json @@ -0,0 +1,862 @@ +{ + "categories": { + "magicNumbers": [], + "complexity": [ + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/bin/cli.js", + "message": "Function 'main' has a complexity of 16. Maximum allowed is 10.", + "ruleId": "complexity", + "line": 20, + "column": 1 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/docs/sample-rule-no-redundant-calculations.js", + "message": "Function 'evaluateExpression' has a complexity of 11. Maximum allowed is 10.", + "ruleId": "complexity", + "line": 17, + "column": 1 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/docs/sample-rule-no-redundant-calculations.js", + "message": "Function 'evaluateTree' has a complexity of 11. Maximum allowed is 10.", + "ruleId": "complexity", + "line": 71, + "column": 1 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/commands/analyze/categorizer.js", + "message": "Function 'categorizeViolations' has a complexity of 27. Maximum allowed is 10.", + "ruleId": "complexity", + "line": 8, + "column": 1 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/commands/analyze/domain.js", + "message": "Function 'inferDomainForMessage' has a complexity of 12. Maximum allowed is 10.", + "ruleId": "complexity", + "line": 28, + "column": 1 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/commands/analyze/estimator.js", + "message": "Function 'estimateEffort' has a complexity of 17. Maximum allowed is 10.", + "ruleId": "complexity", + "line": 46, + "column": 1 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/commands/analyze/index.js", + "message": "Function 'analyzeCommand' has a complexity of 17. Maximum allowed is 10.", + "ruleId": "complexity", + "line": 13, + "column": 1 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/commands/analyze/reporter.js", + "message": "Function 'writeAnalysisReport' has a complexity of 29. Maximum allowed is 10.", + "ruleId": "complexity", + "line": 6, + "column": 1 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/commands/create-issues/index.js", + "message": "Function 'createIssuesCommand' has a complexity of 16. Maximum allowed is 10.", + "ruleId": "complexity", + "line": 12, + "column": 1 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/commands/create-issues/markdown.js", + "message": "Function 'buildMarkdownSections' has a complexity of 85. Maximum allowed is 10.", + "ruleId": "complexity", + "line": 70, + "column": 1 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/commands/create-issues/markdown.js", + "message": "Function 'pickList' has a complexity of 13. Maximum allowed is 10.", + "ruleId": "complexity", + "line": 102, + "column": 3 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/commands/create-issues/markdown.js", + "message": "Arrow function has a complexity of 13. Maximum allowed is 10.", + "ruleId": "complexity", + "line": 113, + "column": 24 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/commands/create-issues/markdown.js", + "message": "Function 'suggest' has a complexity of 16. Maximum allowed is 10.", + "ruleId": "complexity", + "line": 191, + "column": 9 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/commands/init/index.js", + "message": "Function 'initCommand' has a complexity of 40. Maximum allowed is 10.", + "ruleId": "complexity", + "line": 34, + "column": 1 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/commands/init/index.js", + "message": "Async function 'initInteractiveCommand' has a complexity of 46. Maximum allowed is 10.", + "ruleId": "complexity", + "line": 151, + "column": 1 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/commands/learn/index.js", + "message": "Async function 'learnInteractiveCommand' has a complexity of 69. Maximum allowed is 10.", + "ruleId": "complexity", + "line": 12, + "column": 1 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/commands/learn/index.js", + "message": "Function 'learnCommand' has a complexity of 22. Maximum allowed is 10.", + "ruleId": "complexity", + "line": 188, + "column": 1 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/commands/plan/index.js", + "message": "Function 'planCommand' has a complexity of 12. Maximum allowed is 10.", + "ruleId": "complexity", + "line": 12, + "column": 1 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/commands/setup/index.js", + "message": "Async function 'setupCommand' has a complexity of 15. Maximum allowed is 10.", + "ruleId": "complexity", + "line": 5, + "column": 1 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/generators/agents-md.js", + "message": "Function 'writeAgentsMd' has a complexity of 40. Maximum allowed is 10.", + "ruleId": "complexity", + "line": 32, + "column": 1 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/generators/config-json.js", + "message": "Function 'enrichConfigWithDomains' has a complexity of 15. Maximum allowed is 10.", + "ruleId": "complexity", + "line": 27, + "column": 1 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/generators/eslint-arch-config.js", + "message": "Function 'generateArchitectureRules' has a complexity of 14. Maximum allowed is 10.", + "ruleId": "complexity", + "line": 10, + "column": 1 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/generators/guide-md.js", + "message": "Function 'writeGuideMd' has a complexity of 11. Maximum allowed is 10.", + "ruleId": "complexity", + "line": 6, + "column": 1 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/rules/enforce-domain-terms.js", + "message": "Function 'collectDomainTerms' has a complexity of 29. Maximum allowed is 10.", + "ruleId": "complexity", + "line": 29, + "column": 1 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/rules/enforce-domain-terms.js", + "message": "Function 'collectExemptions' has a complexity of 13. Maximum allowed is 10.", + "ruleId": "complexity", + "line": 59, + "column": 1 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/rules/enforce-naming-conventions.js", + "message": "Method 'create' has a complexity of 15. Maximum allowed is 10.", + "ruleId": "complexity", + "line": 114, + "column": 9 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/rules/no-equivalent-branches.js", + "message": "Function 'areNodesEquivalent' has a complexity of 30. Maximum allowed is 10.", + "ruleId": "complexity", + "line": 17, + "column": 1 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/rules/no-generic-names.js", + "message": "Function 'checkIdentifier' has a complexity of 11. Maximum allowed is 10.", + "ruleId": "complexity", + "line": 41, + "column": 5 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/rules/no-redundant-calculations.js", + "message": "Function 'evaluateTree' has a complexity of 15. Maximum allowed is 10.", + "ruleId": "complexity", + "line": 100, + "column": 1 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/rules/no-redundant-calculations.js", + "message": "Function 'shouldSkipScientificCalculation' has a complexity of 18. Maximum allowed is 10.", + "ruleId": "complexity", + "line": 256, + "column": 5 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/rules/no-redundant-calculations.js", + "message": "Function 'buildEffectiveConstantsMap' has a complexity of 19. Maximum allowed is 10.", + "ruleId": "complexity", + "line": 311, + "column": 5 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/rules/no-redundant-calculations.js", + "message": "Function 'matchDomainsForValue' has a complexity of 11. Maximum allowed is 10.", + "ruleId": "complexity", + "line": 354, + "column": 5 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/rules/no-redundant-calculations.js", + "message": "Function 'shouldSkipByDomainResolution' has a complexity of 14. Maximum allowed is 10.", + "ruleId": "complexity", + "line": 380, + "column": 5 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/rules/no-redundant-calculations.js", + "message": "Method 'BinaryExpression' has a complexity of 15. Maximum allowed is 10.", + "ruleId": "complexity", + "line": 412, + "column": 23 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/rules/no-redundant-conditionals.js", + "message": "Function 'isConstant' has a complexity of 12. Maximum allowed is 10.", + "ruleId": "complexity", + "line": 41, + "column": 5 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/rules/no-redundant-conditionals.js", + "message": "Function 'getBooleanValue' has a complexity of 16. Maximum allowed is 10.", + "ruleId": "complexity", + "line": 73, + "column": 5 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/rules/no-redundant-conditionals.js", + "message": "Function 'isRedundantBooleanComparison' has a complexity of 26. Maximum allowed is 10.", + "ruleId": "complexity", + "line": 119, + "column": 5 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/rules/no-redundant-conditionals.js", + "message": "Function 'evaluateConstant' has a complexity of 35. Maximum allowed is 10.", + "ruleId": "complexity", + "line": 167, + "column": 5 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/rules/no-redundant-conditionals.js", + "message": "Function 'simplifyLogicalExpression' has a complexity of 22. Maximum allowed is 10.", + "ruleId": "complexity", + "line": 228, + "column": 5 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/rules/no-redundant-conditionals.js", + "message": "Method 'ConditionalExpression' has a complexity of 14. Maximum allowed is 10.", + "ruleId": "complexity", + "line": 542, + "column": 28 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/rules/no-unnecessary-abstraction.js", + "message": "Function 'getTrivialWrapperInfo' has a complexity of 13. Maximum allowed is 10.", + "ruleId": "complexity", + "line": 42, + "column": 5 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/rules/no-unnecessary-abstraction.js", + "message": "Method 'Program:exit' has a complexity of 11. Maximum allowed is 10.", + "ruleId": "complexity", + "line": 185, + "column": 21 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/rules/prefer-simpler-logic.js", + "message": "Method 'BinaryExpression' has a complexity of 17. Maximum allowed is 10.", + "ruleId": "complexity", + "line": 48, + "column": 23 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/scanner/extract.js", + "message": "Function 'walk' has a complexity of 14. Maximum allowed is 10.", + "ruleId": "complexity", + "line": 10, + "column": 3 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/scanner/extract.js", + "message": "Function 'aggregateSummaries' has a complexity of 17. Maximum allowed is 10.", + "ruleId": "complexity", + "line": 135, + "column": 1 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/scanner/reconcile.js", + "message": "Function 'reconcileNaming' has a complexity of 15. Maximum allowed is 10.", + "ruleId": "complexity", + "line": 24, + "column": 1 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/scanner/reconcile.js", + "message": "Function 'reconcileConstants' has a complexity of 25. Maximum allowed is 10.", + "ruleId": "complexity", + "line": 70, + "column": 1 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/scanner/reconcile.js", + "message": "Function 'reconcile' has a complexity of 11. Maximum allowed is 10.", + "ruleId": "complexity", + "line": 128, + "column": 1 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/utils/arch-defaults.js", + "message": "Function 'mergeArchitecture' has a complexity of 13. Maximum allowed is 10.", + "ruleId": "complexity", + "line": 52, + "column": 1 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/utils/arch-switch.js", + "message": "Function 'normalizeBoolean' has a complexity of 16. Maximum allowed is 10.", + "ruleId": "complexity", + "line": 3, + "column": 1 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/utils/discover-constants.js", + "message": "Function 'discoverNpmPackages' has a complexity of 26. Maximum allowed is 10.", + "ruleId": "complexity", + "line": 31, + "column": 1 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/utils/domain-annotations.js", + "message": "Function 'getNearestSectionDomain' has a complexity of 11. Maximum allowed is 10.", + "ruleId": "complexity", + "line": 25, + "column": 1 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/utils/fingerprint.js", + "message": "Function 'applyFingerprintToConfig' has a complexity of 11. Maximum allowed is 10.", + "ruleId": "complexity", + "line": 16, + "column": 1 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/utils/merge-constants.js", + "message": "Function 'mergeConstants' has a complexity of 23. Maximum allowed is 10.", + "ruleId": "complexity", + "line": 3, + "column": 1 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/utils/project-config.js", + "message": "Function 'shallowValidate' has a complexity of 11. Maximum allowed is 10.", + "ruleId": "complexity", + "line": 26, + "column": 1 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/utils/project-config.js", + "message": "Function 'deepMerge' has a complexity of 11. Maximum allowed is 10.", + "ruleId": "complexity", + "line": 35, + "column": 1 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/utils/project-config.js", + "message": "Function 'readProjectConfig' has a complexity of 40. Maximum allowed is 10.", + "ruleId": "complexity", + "line": 76, + "column": 1 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/utils/project-detection.js", + "message": "Function 'detectProjectContext' has a complexity of 18. Maximum allowed is 10.", + "ruleId": "complexity", + "line": 6, + "column": 1 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/utils/requirements.js", + "message": "Function 'checkRequirements' has a complexity of 13. Maximum allowed is 10.", + "ruleId": "complexity", + "line": 5, + "column": 1 + } + ], + "domainTerms": [], + "architecture": [ + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/bin/cli.js", + "message": "Function 'main' has too many statements (39). Maximum allowed is 30.", + "ruleId": "max-statements", + "line": 20, + "column": 1 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/commands/analyze/reporter.js", + "message": "Function 'writeAnalysisReport' has too many lines (119). Maximum allowed is 50.", + "ruleId": "max-lines-per-function", + "line": 6, + "column": 1 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/commands/analyze/reporter.js", + "message": "Function 'writeAnalysisReport' has too many statements (65). Maximum allowed is 30.", + "ruleId": "max-statements", + "line": 6, + "column": 1 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/commands/create-issues/markdown.js", + "message": "Function 'buildMarkdownSections' has too many lines (259). Maximum allowed is 50.", + "ruleId": "max-lines-per-function", + "line": 70, + "column": 1 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/commands/create-issues/markdown.js", + "message": "Function 'buildMarkdownSections' has too many statements (179). Maximum allowed is 30.", + "ruleId": "max-statements", + "line": 70, + "column": 1 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/commands/create-issues/markdown.js", + "message": "File has too many lines (394). Maximum allowed is 150.", + "ruleId": "max-lines", + "line": 169, + "column": 1 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/commands/init/index.js", + "message": "Function 'initCommand' has too many lines (96). Maximum allowed is 50.", + "ruleId": "max-lines-per-function", + "line": 34, + "column": 1 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/commands/init/index.js", + "message": "Function 'initCommand' has too many statements (50). Maximum allowed is 30.", + "ruleId": "max-statements", + "line": 34, + "column": 1 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/commands/init/index.js", + "message": "Async function 'initInteractiveCommand' has too many lines (111). Maximum allowed is 50.", + "ruleId": "max-lines-per-function", + "line": 151, + "column": 1 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/commands/init/index.js", + "message": "Async function 'initInteractiveCommand' has too many statements (79). Maximum allowed is 30.", + "ruleId": "max-statements", + "line": 151, + "column": 1 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/commands/init/index.js", + "message": "File has too many lines (234). Maximum allowed is 150.", + "ruleId": "max-lines", + "line": 183, + "column": 1 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/commands/init/index.js", + "message": "Blocks are nested too deeply (5). Maximum allowed is 4.", + "ruleId": "max-depth", + "line": 198, + "column": 11 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/commands/init/index.js", + "message": "Blocks are nested too deeply (6). Maximum allowed is 4.", + "ruleId": "max-depth", + "line": 200, + "column": 13 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/commands/init/index.js", + "message": "Blocks are nested too deeply (5). Maximum allowed is 4.", + "ruleId": "max-depth", + "line": 210, + "column": 13 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/commands/init/index.js", + "message": "Blocks are nested too deeply (6). Maximum allowed is 4.", + "ruleId": "max-depth", + "line": 212, + "column": 15 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/commands/learn/index.js", + "message": "Async function 'learnInteractiveCommand' has too many lines (152). Maximum allowed is 50.", + "ruleId": "max-lines-per-function", + "line": 12, + "column": 1 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/commands/learn/index.js", + "message": "Async function 'learnInteractiveCommand' has too many parameters (5). Maximum allowed is 4.", + "ruleId": "max-params", + "line": 12, + "column": 1 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/commands/learn/index.js", + "message": "Async function 'learnInteractiveCommand' has too many statements (116). Maximum allowed is 30.", + "ruleId": "max-statements", + "line": 12, + "column": 1 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/commands/learn/index.js", + "message": "Blocks are nested too deeply (5). Maximum allowed is 4.", + "ruleId": "max-depth", + "line": 96, + "column": 11 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/commands/learn/index.js", + "message": "Blocks are nested too deeply (6). Maximum allowed is 4.", + "ruleId": "max-depth", + "line": 99, + "column": 13 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/commands/learn/index.js", + "message": "Blocks are nested too deeply (5). Maximum allowed is 4.", + "ruleId": "max-depth", + "line": 105, + "column": 11 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/commands/learn/index.js", + "message": "Blocks are nested too deeply (5). Maximum allowed is 4.", + "ruleId": "max-depth", + "line": 110, + "column": 11 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/commands/learn/index.js", + "message": "Blocks are nested too deeply (5). Maximum allowed is 4.", + "ruleId": "max-depth", + "line": 116, + "column": 11 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/commands/learn/index.js", + "message": "File has too many lines (209). Maximum allowed is 150.", + "ruleId": "max-lines", + "line": 174, + "column": 1 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/commands/learn/index.js", + "message": "Function 'learnCommand' has too many statements (37). Maximum allowed is 30.", + "ruleId": "max-statements", + "line": 188, + "column": 1 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/commands/plan/roadmap.js", + "message": "Function 'writeRoadmap' has too many lines (69). Maximum allowed is 50.", + "ruleId": "max-lines-per-function", + "line": 23, + "column": 1 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/commands/plan/roadmap.js", + "message": "Arrow function has too many lines (57). Maximum allowed is 50.", + "ruleId": "max-lines-per-function", + "line": 35, + "column": 18 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/commands/plan/roadmap.js", + "message": "Arrow function has too many statements (48). Maximum allowed is 30.", + "ruleId": "max-statements", + "line": 35, + "column": 18 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/generators/agents-md.js", + "message": "Function 'writeAgentsMd' has too many lines (75). Maximum allowed is 50.", + "ruleId": "max-lines-per-function", + "line": 32, + "column": 1 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/generators/agents-md.js", + "message": "Function 'writeAgentsMd' has too many statements (62). Maximum allowed is 30.", + "ruleId": "max-statements", + "line": 32, + "column": 1 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/generators/eslint-arch-config.js", + "message": "Function 'generateArchitectureRules' has too many lines (68). Maximum allowed is 50.", + "ruleId": "max-lines-per-function", + "line": 10, + "column": 1 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/rules/enforce-domain-terms.js", + "message": "Method 'create' has too many lines (58). Maximum allowed is 50.", + "ruleId": "max-lines-per-function", + "line": 103, + "column": 3 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/rules/enforce-naming-conventions.js", + "message": "Method 'create' has too many lines (78). Maximum allowed is 50.", + "ruleId": "max-lines-per-function", + "line": 114, + "column": 3 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/rules/no-redundant-calculations.js", + "message": "Method 'create' has too many lines (210). Maximum allowed is 50.", + "ruleId": "max-lines-per-function", + "line": 179, + "column": 3 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/rules/no-redundant-calculations.js", + "message": "File has too many lines (322). Maximum allowed is 250.", + "ruleId": "max-lines", + "line": 385, + "column": 1 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/rules/no-redundant-conditionals.js", + "message": "Method 'create' has too many lines (451). Maximum allowed is 50.", + "ruleId": "max-lines-per-function", + "line": 31, + "column": 3 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/rules/no-redundant-conditionals.js", + "message": "Function 'simplifyLogicalExpression' has too many lines (52). Maximum allowed is 50.", + "ruleId": "max-lines-per-function", + "line": 228, + "column": 5 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/rules/no-redundant-conditionals.js", + "message": "Method 'IfStatement' has too many lines (71). Maximum allowed is 50.", + "ruleId": "max-lines-per-function", + "line": 291, + "column": 7 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/rules/no-redundant-conditionals.js", + "message": "File has too many lines (471). Maximum allowed is 250.", + "ruleId": "max-lines", + "line": 335, + "column": 1 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/rules/no-redundant-conditionals.js", + "message": "Method 'SwitchStatement' has too many lines (78). Maximum allowed is 50.", + "ruleId": "max-lines-per-function", + "line": 450, + "column": 7 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/rules/no-redundant-conditionals.js", + "message": "Method 'SwitchStatement' has too many statements (33). Maximum allowed is 30.", + "ruleId": "max-statements", + "line": 450, + "column": 22 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/rules/no-unnecessary-abstraction.js", + "message": "Method 'create' has too many lines (157). Maximum allowed is 50.", + "ruleId": "max-lines-per-function", + "line": 28, + "column": 3 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/rules/no-unnecessary-abstraction.js", + "message": "Method 'Program:exit' has too many lines (61). Maximum allowed is 50.", + "ruleId": "max-lines-per-function", + "line": 185, + "column": 7 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/rules/prefer-simpler-logic.js", + "message": "Method 'create' has too many lines (121). Maximum allowed is 50.", + "ruleId": "max-lines-per-function", + "line": 44, + "column": 3 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/rules/prefer-simpler-logic.js", + "message": "Method 'BinaryExpression' has too many lines (74). Maximum allowed is 50.", + "ruleId": "max-lines-per-function", + "line": 48, + "column": 7 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/utils/discover-constants.js", + "message": "Function 'discoverNpmPackages' has too many statements (31). Maximum allowed is 30.", + "ruleId": "max-statements", + "line": 31, + "column": 1 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/utils/project-config.js", + "message": "Function 'readProjectConfig' has too many lines (61). Maximum allowed is 50.", + "ruleId": "max-lines-per-function", + "line": 76, + "column": 1 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/utils/project-config.js", + "message": "Function 'readProjectConfig' has too many statements (49). Maximum allowed is 30.", + "ruleId": "max-statements", + "line": 76, + "column": 1 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/utils/project-config.js", + "message": "Blocks are nested too deeply (5). Maximum allowed is 4.", + "ruleId": "max-depth", + "line": 92, + "column": 11 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/utils/project-config.js", + "message": "Blocks are nested too deeply (5). Maximum allowed is 4.", + "ruleId": "max-depth", + "line": 93, + "column": 11 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/utils/project-config.js", + "message": "Blocks are nested too deeply (6). Maximum allowed is 4.", + "ruleId": "max-depth", + "line": 97, + "column": 13 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/utils/project-config.js", + "message": "Blocks are nested too deeply (7). Maximum allowed is 4.", + "ruleId": "max-depth", + "line": 100, + "column": 15 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/utils/project-detection.js", + "message": "Function 'detectProjectContext' has too many lines (84). Maximum allowed is 50.", + "ruleId": "max-lines-per-function", + "line": 6, + "column": 1 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/lib/utils/project-detection.js", + "message": "Function 'detectProjectContext' has too many statements (31). Maximum allowed is 30.", + "ruleId": "max-statements", + "line": 6, + "column": 1 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/tests/lib/rules/no-redundant-calculations.js", + "message": "File has too many lines (384). Maximum allowed is 250.", + "ruleId": "max-lines", + "line": 338, + "column": 1 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/tests/lib/rules/no-redundant-conditionals.js", + "message": "File has too many lines (593). Maximum allowed is 250.", + "ruleId": "max-lines", + "line": 340, + "column": 1 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/tests/lib/rules/no-unnecessary-abstraction.js", + "message": "File has too many lines (669). Maximum allowed is 250.", + "ruleId": "max-lines", + "line": 327, + "column": 1 + }, + { + "filePath": "/Users/dougfennell/vscode/projects/eslint-plugin-ai-code-snifftest/tests/lib/rules/prefer-simpler-logic.js", + "message": "File has too many lines (295). Maximum allowed is 250.", + "ruleId": "max-lines", + "line": 349, + "column": 1 + } + ], + "counts": { + "errors": 0, + "warnings": 126, + "autoFixable": 0 + }, + "domainSummary": [ + { + "domain": "dev-tools", + "count": 0, + "rank": 0 + }, + { + "domain": "cli", + "count": 0, + "rank": 1 + }, + { + "domain": "linting", + "count": 0, + "rank": 2 + } + ] + }, + "effort": { + "hours": 100, + "days": 12.5, + "weeks": 2.5, + "byCategory": { + "magicNumbers": 0, + "domainTerms": 0, + "architecture": 11.5, + "complexity": 88.5 + } + } +} diff --git a/docs/CONTEXT-AWARE-LINTING.md b/docs/CONTEXT-AWARE-LINTING.md new file mode 100644 index 0000000..5391101 --- /dev/null +++ b/docs/CONTEXT-AWARE-LINTING.md @@ -0,0 +1,37 @@ +# Context-Aware Linting and Telemetry + +This codebase supports a context-aware ratchet that understands refactoring intent and reports a composite Code Health score (telemetry). The traditional ratchet remains the merge gate. + +What you get +- Intent signals: refactoring / ai-generation-suspect / neutral (with confidence) +- Structural proxies: per-rule breakdowns for complexity and architecture +- Code Health score (0–100): overall + components (structural, semantic, maintainability, style) +- Non-blocking telemetry by default (recommended burn‑in period) + +How to run locally +- Traditional ratchet (merge gate): + npm run ratchet +- Context-aware telemetry (one of these will work depending on script availability): + npm run ratchet:context + # or fallback if the script isn’t present yet + node scripts/ratchet.js --mode=context --baseline=analysis-baseline.json --current=analysis-current.json + +Configuration (optional) +- Weights and categories in .ai-coding-guide.json (used in telemetry): + { + "ratchet": { + "weights": { "complexity": 10, "architecture": 8, "domainTerms": 2, "magicNumbers": 1 }, + "critical": ["complexity", "architecture"], + "minor": ["domainTerms", "magicNumbers"], + "allowMinorIncreaseDuringRefactor": true + } + } + +Promoting telemetry to a required PR check +1) Burn‑in: keep telemetry job non‑blocking for a few PRs; review reported signals and health deltas in CI. +2) Branch protection: mark the telemetry job (ratchet-context) as “required” in repository settings; optionally remove continue-on-error. +3) (Optional) Health gating: after calibration, enable policy to block when overall health decreases. This can be added as a future enhancement (tracked in issue) and controlled via config (e.g., ratchet.health.gate with minDelta). + +Notes +- Support scripts under scripts/**/*.js are excluded from complexity/architecture and plugin-complexity rules, keeping product metrics accurate. +- Traditional ratchet continues to gate merges; context-aware telemetry complements it by providing richer signals. From 68f8ed3df66109a2eb3d9b6f07be5e1eed9677fc Mon Sep 17 00:00:00 2001 From: Doug Fennell Date: Mon, 10 Nov 2025 08:49:22 -0600 Subject: [PATCH 2/5] ci(context): enable context telemetry and workflow improvements --- .github/workflows/ci-ratchet.yml | 4 ++ package.json | 1 + scripts/ratchet.js | 117 +++++++++++++++++++++++++------ 3 files changed, 102 insertions(+), 20 deletions(-) diff --git a/.github/workflows/ci-ratchet.yml b/.github/workflows/ci-ratchet.yml index 261b885..d3355d8 100644 --- a/.github/workflows/ci-ratchet.yml +++ b/.github/workflows/ci-ratchet.yml @@ -4,12 +4,15 @@ on: pull_request: push: branches: [main] + workflow_dispatch: jobs: ratchet-and-tests: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + fetch-depth: 0 - uses: actions/setup-node@v4 with: @@ -42,6 +45,7 @@ jobs: run: npm test - name: Upload artifacts + if: always() uses: actions/upload-artifact@v4 with: name: analysis-and-lint-${{ github.sha }} diff --git a/package.json b/package.json index 6a1d6ef..28cf350 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "analyze:current": "node bin/cli.js analyze --input=lint-results.json --format=json --output=analysis-current.json", "analyze:baseline": "node bin/cli.js analyze --input=lint-results.json --format=json --output=analysis-baseline.json", "ratchet": "node scripts/ratchet.js --baseline=analysis-baseline.json --current=analysis-current.json", + "ratchet:context": "node scripts/ratchet.js --mode=context --baseline=analysis-baseline.json --current=analysis-current.json", "ci:ratchet": "npm run lint:json && npm run analyze:current && npm run ratchet", "test": "mocha tests --recursive", "test:coverage": "c8 --reporter=text --reporter=lcov npm test", diff --git a/scripts/ratchet.js b/scripts/ratchet.js index 60bd832..7c2966e 100644 --- a/scripts/ratchet.js +++ b/scripts/ratchet.js @@ -32,7 +32,6 @@ function len(x) { return Array.isArray(x) ? x.length : 0; } function num(x) { return typeof x === 'number' && Number.isFinite(x) ? x : 0; } function summarize(payload) { - // Expected shape: { categories: { magicNumbers, complexity, domainTerms, architecture, counts? }, effort: { byCategory? } } const cat = payload && payload.categories ? payload.categories : {}; const effort = payload && payload.effort ? payload.effort : {}; const byCat = (effort && effort.byCategory) ? effort.byCategory : {}; @@ -41,7 +40,6 @@ function summarize(payload) { complexity: len(cat.complexity), domainTerms: len(cat.domainTerms), architecture: len(cat.architecture), - // Optional counts (informational) counts: { errors: num(cat.counts && cat.counts.errors), warnings: num(cat.counts && cat.counts.warnings), @@ -64,7 +62,6 @@ function compare(base, curr) { const c = num(curr[f]); if (c > b) deltas.push({ key: f, base: b, current: c, type: 'count' }); } - // Effort ratchet (optional, do not fail but report increases) const effortInc = []; for (const f of fields) { const b = num(base.effortByCategory && base.effortByCategory[f]); @@ -74,8 +71,100 @@ function compare(base, curr) { return { deltas, effortInc }; } +function detectIntent(base, curr) { + const totalDelta = (curr.magicNumbers - base.magicNumbers) + + (curr.complexity - base.complexity) + + (curr.domainTerms - base.domainTerms) + + (curr.architecture - base.architecture); + + let intent = 'neutral'; + let confidence = 0.5; + const signals = []; + + if (totalDelta < -10) { + intent = 'cleanup'; + confidence = 0.7; + signals.push('Violations decreased significantly'); + } else if (totalDelta > 20 && curr.domainTerms > base.domainTerms * 1.3) { + intent = 'ai-generation-suspect'; + confidence = 0.6; + signals.push('Large increase in domain term violations'); + signals.push('Rapid violation growth pattern'); + } else if (curr.complexity < base.complexity * 0.8 && curr.architecture < base.architecture * 0.8) { + intent = 'refactoring'; + confidence = 0.65; + signals.push('Complexity decreased'); + signals.push('Architecture violations decreased'); + } + + return { intent, confidence, signals }; +} + +function runContextMode(base, curr) { + console.log('\n📊 Context-Aware Telemetry'); + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'); + console.log('Mode: Non-blocking (burn-in period)\n'); + + console.log('Category Counts:'); + const categories = [ + { key: 'magicNumbers', label: 'Magic Numbers' }, + { key: 'complexity', label: 'Complexity' }, + { key: 'domainTerms', label: 'Domain Terms' }, + { key: 'architecture', label: 'Architecture' } + ]; + + for (const cat of categories) { + const baseVal = base[cat.key]; + const currVal = curr[cat.key]; + const delta = currVal - baseVal; + const emoji = delta > 0 ? '⚠️' : (delta < 0 ? '✅' : '➖'); + const sign = delta > 0 ? '+' : ''; + console.log(` ${cat.label.padEnd(15)} ${baseVal} → ${currVal} (${sign}${delta}) ${emoji}`); + } + + console.log(); + + const { intent, confidence, signals } = detectIntent(base, curr); + + console.log('Intent Detection:'); + console.log(` Detected: ${intent}`); + console.log(` Confidence: ${(confidence * 100).toFixed(0)}%`); + if (signals.length > 0) { + console.log(' Signals:'); + signals.forEach(s => console.log(` • ${s}`)); + } + + console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); + console.log('\n✅ Telemetry complete (non-blocking)\n'); +} + +function runTraditionalMode(base, curr) { + const { deltas, effortInc } = compare(base, curr); + + if (deltas.length === 0) { + console.log('[ratchet] OK: no increases in analyzer categories'); + if (effortInc.length) { + const lines = effortInc.map(d => ` effort.${d.key}: ${d.base}h -> ${d.current}h`); + console.log('[ratchet] Note: effort increased (informational):\n' + lines.join('\n')); + } + return 0; + } + + console.error('[ratchet] FAIL: new violations introduced'); + for (const d of deltas) { + console.error(` ${d.key}: ${d.base} -> ${d.current} (+${d.current - d.base})`); + } + console.error('\nTo inspect:'); + console.error(' - open analysis-current.json'); + console.error(' - compare with analysis-baseline.json'); + console.error('\nIf intentional reductions were made and counts decreased overall, refresh baseline:'); + console.error(' npm run analyze:baseline'); + return 1; +} + function main() { const args = parseArgs(process.argv); + const mode = args.mode || 'traditional'; const baselinePath = args.baseline || args._[0] || 'analysis-baseline.json'; const currentPath = args.current || args._[1] || 'analysis-current.json'; @@ -87,6 +176,7 @@ function main() { process.exit(0); return; } + const curr = readJson(currentPath); if (!curr) { console.error(`[ratchet] Current analysis not found at ${currentPath}. Run: npm run lint:json && npm run analyze:current`); @@ -96,28 +186,15 @@ function main() { const b = summarize(base); const c = summarize(curr); - const { deltas, effortInc } = compare(b, c); - if (deltas.length === 0) { - console.log('[ratchet] OK: no increases in analyzer categories'); - if (effortInc.length) { - const lines = effortInc.map(d => ` effort.${d.key}: ${d.base}h -> ${d.current}h`); - console.log('[ratchet] Note: effort increased (informational):\n' + lines.join('\n')); - } + if (mode === 'context') { + runContextMode(b, c); process.exit(0); return; } - console.error('[ratchet] FAIL: new violations introduced'); - for (const d of deltas) { - console.error(` ${d.key}: ${d.base} -> ${d.current} (+${d.current - d.base})`); - } - console.error('\nTo inspect:'); - console.error(' - open analysis-current.json'); - console.error(' - compare with analysis-baseline.json'); - console.error('\nIf intentional reductions were made and counts decreased overall, refresh baseline:'); - console.error(' npm run analyze:baseline'); - process.exit(1); + const exitCode = runTraditionalMode(b, c); + process.exit(exitCode); } main(); From 6ab858ec734bd3d5f8d0c0e9facc9cff49187019 Mon Sep 17 00:00:00 2001 From: Doug Fennell Date: Mon, 10 Nov 2025 09:06:16 -0600 Subject: [PATCH 3/5] ci(context): exclude scripts/**/*.js from architecture/complexity metrics to avoid ratchet noise --- eslint.config.mjs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/eslint.config.mjs b/eslint.config.mjs index 9064992..e8ad5a8 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -72,6 +72,17 @@ export default [ 'max-lines': ["warn",{"max":300,"skipBlankLines":true,"skipComments":true}] } }, + { + files: ["scripts/**/*.js"], + rules: { + // Exclude support scripts from architecture/complexity metrics to keep product telemetry stable + 'complexity': 'off', + 'max-statements': 'off', + 'max-lines-per-function': 'off', + 'max-depth': 'off', + 'max-lines': 'off' + } + }, { files: ["lib/rules/**/*.js"], rules: { From 64b4133fecc88ca9b7a881651ea123a5003edec0 Mon Sep 17 00:00:00 2001 From: Doug Fennell Date: Mon, 10 Nov 2025 09:09:41 -0600 Subject: [PATCH 4/5] ci(ratchet): add tolerance band (total=5) via config to reduce false fails --- .ai-coding-guide.json | 4 ++++ scripts/ratchet.js | 24 +++++++++++++++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/.ai-coding-guide.json b/.ai-coding-guide.json index 5ba9aac..a0730cb 100644 --- a/.ai-coding-guide.json +++ b/.ai-coding-guide.json @@ -84,5 +84,9 @@ "errorHandling": "explicit", "asyncStyle": "async-await" } + }, + "ratchet": { + "tolerance": { "total": 5 }, + "health": { "enabled": false } } } diff --git a/scripts/ratchet.js b/scripts/ratchet.js index 076395e..e6e9454 100644 --- a/scripts/ratchet.js +++ b/scripts/ratchet.js @@ -214,7 +214,7 @@ function runTraditionalMode(base, curr, args) { // Health telemetry + optional gating const cfg = loadProjectConfig(); const healthCfg = (cfg && cfg.ratchet && cfg.ratchet.health) || { enabled: false }; -const scores = computeHealth(curr); + const scores = computeHealth(curr); const gateOn = String(healthCfg.gateOn || 'overall').toLowerCase(); const minOverall = Number(healthCfg.minOverall || 70); const intent = detectIntent(args); @@ -225,6 +225,10 @@ const scores = computeHealth(curr); const gateActive = !!healthCfg.enabled && !shouldBypass(healthCfg); const gateFail = gateActive && currentScore < threshold; + // Tolerance band (configurable) to avoid blocking normal fluctuation + const tolCfg = (cfg && cfg.ratchet && cfg.ratchet.tolerance) || {}; + const totalTolerance = Number(tolCfg.total || 0); + if (deltas.length === 0) { console.log('[ratchet] OK: no increases in analyzer categories'); if (effortInc.length) { @@ -241,6 +245,24 @@ if (deltas.length === 0) { return 0; } + // If within tolerance, allow pass with notice + const totalIncrease = deltas.reduce((sum, d) => sum + (d.current - d.base), 0); + if (totalIncrease <= totalTolerance) { + console.log(`[ratchet] OK: within tolerance (+${totalIncrease} violations, tolerance: ${totalTolerance})`); + for (const d of deltas) { + console.log(` ${d.key}: ${d.base} → ${d.current} (+${d.current - d.base})`); + } + console.log('\nSmall increases accepted during active development.'); + // Health telemetry (informational) and optional gate still apply + console.log(`[ratchet] Health (informational): overall=${scores.overall} structural=${scores.structural} semantic=${scores.semantic}`); + if (gateFail) { + console.error(`[ratchet] HEALTH-GATE FAIL: ${failureMessage}`); + console.error(` gateOn=${gateOn} threshold=${threshold} actual=${currentScore} intent=${intent}`); + return 1; + } + return 0; + } + console.error('[ratchet] FAIL: new violations introduced'); for (const d of deltas) { console.error(` ${d.key}: ${d.base} -> ${d.current} (+${d.current - d.base})`); From a6ca714c5e71b2738a8e0e35419e33c1b3fe512f Mon Sep 17 00:00:00 2001 From: Doug Fennell Date: Mon, 10 Nov 2025 09:13:11 -0600 Subject: [PATCH 5/5] ci(ratchet): implement hybrid density strategy (critical vs minor, per 1K LOC) with configurable tolerances --- .ai-coding-guide.json | 6 ++++ scripts/ratchet.js | 66 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 71 insertions(+), 1 deletion(-) diff --git a/.ai-coding-guide.json b/.ai-coding-guide.json index a0730cb..6e4d6b0 100644 --- a/.ai-coding-guide.json +++ b/.ai-coding-guide.json @@ -86,7 +86,13 @@ } }, "ratchet": { + "strategy": "hybrid", "tolerance": { "total": 5 }, + "hybrid": { + "critical": ["complexity", "architecture"], + "minor": ["domainTerms", "magicNumbers"], + "tolerance": { "criticalDensity": 1.05, "minorDensity": 1.2 } + }, "health": { "enabled": false } } } diff --git a/scripts/ratchet.js b/scripts/ratchet.js index e6e9454..7e8b8b2 100644 --- a/scripts/ratchet.js +++ b/scripts/ratchet.js @@ -225,11 +225,75 @@ function runTraditionalMode(base, curr, args) { const gateActive = !!healthCfg.enabled && !shouldBypass(healthCfg); const gateFail = gateActive && currentScore < threshold; + const strategy = (cfg && cfg.ratchet && cfg.ratchet.strategy) || 'classic'; + + // Hybrid strategy: density-based for critical vs minor categories (per 1K LOC) + if (strategy === 'hybrid') { + const hybridCfg = (cfg && cfg.ratchet && cfg.ratchet.hybrid) || {}; + const critical = Array.isArray(hybridCfg.critical) && hybridCfg.critical.length ? hybridCfg.critical : ['complexity', 'architecture']; + const minor = Array.isArray(hybridCfg.minor) && hybridCfg.minor.length ? hybridCfg.minor : ['domainTerms', 'magicNumbers']; + const tol = (hybridCfg.tolerance) || {}; + const critTol = Number(tol.criticalDensity || 1.05); // allow 5% increase + const minorTol = Number(tol.minorDensity || 1.20); // allow 20% increase + + const baseLoc = Math.max(1, Number(base.lines && base.lines.executable) || Number(base.lines && base.lines.physical) || 1); + const currLoc = Math.max(1, Number(curr.lines && curr.lines.executable) || Number(curr.lines && curr.lines.physical) || 1); + const perK = (count, loc) => (count / (loc / 1000)); + + let failed = false; + + console.log('\n[ratchet] Hybrid density check (per 1K LOC)'); + console.log(`LOC: ${baseLoc} → ${currLoc} (${currLoc - baseLoc >= 0 ? '+' : ''}${currLoc - baseLoc})`); + + console.log('\nCritical (strict):'); + for (const cat of critical) { + const b = Number(base[cat]) || 0; + const c = Number(curr[cat]) || 0; + const bd = perK(b, baseLoc); + const cd = perK(c, currLoc); + const ratio = bd === 0 ? (cd === 0 ? 1 : Infinity) : (cd / bd); + const status = ratio > critTol ? '❌' : '✅'; + console.log(` ${String(cat).padEnd(15)} ${b} → ${c} (density: ${bd.toFixed(2)} → ${cd.toFixed(2)}) ${status}`); + if (ratio > critTol) failed = true; + } + + console.log('\nMinor (relaxed):'); + for (const cat of minor) { + const b = Number(base[cat]) || 0; + const c = Number(curr[cat]) || 0; + const bd = perK(b, baseLoc); + const cd = perK(c, currLoc); + const ratio = bd === 0 ? (cd === 0 ? 1 : Infinity) : (cd / bd); + const status = ratio > minorTol ? '⚠️' : '✅'; + console.log(` ${String(cat).padEnd(15)} ${b} → ${c} (density: ${bd.toFixed(2)} → ${cd.toFixed(2)}) ${status}`); + } + + // Health telemetry (informational) + console.log(`\n[ratchet] Health (informational): overall=${scores.overall} structural=${scores.structural} semantic=${scores.semantic}`); + + if (gateFail) { + console.error(`[ratchet] HEALTH-GATE FAIL: ${failureMessage}`); + console.error(` gateOn=${gateOn} threshold=${threshold} actual=${currentScore} intent=${intent}`); + return 1; + } + + if (failed) { + console.error('\n❌ [ratchet] FAIL: Critical violation density increased'); + console.error('Complexity and architecture are strictly controlled.'); + console.error('Refactor to reduce complexity or update baseline if intentional.'); + return 1; + } + + console.log('\n✅ [ratchet] PASS: Quality maintained or improved'); + return 0; + } + + // Classic strategy with tolerance band // Tolerance band (configurable) to avoid blocking normal fluctuation const tolCfg = (cfg && cfg.ratchet && cfg.ratchet.tolerance) || {}; const totalTolerance = Number(tolCfg.total || 0); -if (deltas.length === 0) { + if (deltas.length === 0) { console.log('[ratchet] OK: no increases in analyzer categories'); if (effortInc.length) { const lines = effortInc.map(d => ` effort.${d.key}: ${d.base}h -> ${d.current}h`);