Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions lib/utils/health.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
'use strict';

function num(x) { return typeof x === 'number' && Number.isFinite(x) ? x : 0; }

/**
* Compute health scores based on violation density per 1K executable LOC.
* Expects a summary with fields: lines.executable (or physical) and counts per category.
* Returns integers 0..100 for overall, structural, semantic.
*/
function computeHealth(summary) {
const exec = Math.max(1, num(summary && summary.lines && summary.lines.executable) || num(summary && summary.lines && summary.lines.physical) || 1000);
const total = num(summary && summary.magicNumbers) + num(summary && summary.complexity) + num(summary && summary.domainTerms) + num(summary && summary.architecture);
const structuralTotal = num(summary && summary.complexity) + num(summary && summary.architecture);
const semanticTotal = num(summary && summary.domainTerms) + num(summary && summary.magicNumbers);
const perK = total / (exec / 1000);
const perKStructural = structuralTotal / (exec / 1000);
const perKSemantic = semanticTotal / (exec / 1000);
const toScore = (d) => Math.max(0, Math.min(100, 100 - d * 10));
return {
overall: Math.round(toScore(perK)),
structural: Math.round(toScore(perKStructural)),
semantic: Math.round(toScore(perKSemantic))
};
}

module.exports = { computeHealth };
18 changes: 2 additions & 16 deletions scripts/ratchet.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,22 +85,8 @@ function summarize(payload) {
};
}

// Rough health scoring using violation density (per K executable LOC if available)
function computeHealth(summary) {
const exec = Math.max(1, num(summary.lines && summary.lines.executable) || num(summary.lines && summary.lines.physical) || 1000);
const total = num(summary.magicNumbers) + num(summary.complexity) + num(summary.domainTerms) + num(summary.architecture);
const structuralTotal = num(summary.complexity) + num(summary.architecture);
const semanticTotal = num(summary.domainTerms) + num(summary.magicNumbers);
const perK = total / (exec / 1000);
const perKStructural = structuralTotal / (exec / 1000);
const perKSemantic = semanticTotal / (exec / 1000);
const toScore = (d) => Math.max(0, Math.min(100, 100 - d * 10));
return {
overall: Math.round(toScore(perK)),
structural: Math.round(toScore(perKStructural)),
semantic: Math.round(toScore(perKSemantic))
};
}
// Health scoring moved to utility for testability
const { computeHealth } = require(path.join(__dirname, '..', 'lib', 'utils', 'health.js'));

function detectIntent(args) {
if (args && (args.refactoring || args.intent === 'refactoring')) return 'refactoring';
Expand Down
66 changes: 66 additions & 0 deletions tests/integration/health-gating.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,56 @@ describe('health gating', function () {
assert.ok(res.out.includes('HEALTH-GATE FAIL'));
});

it('passes gate when score equals threshold (>= semantics)', function () {
const cwd = mkTmp();
// exec=1000, structural=3 → overall=70
const cats = { magicNumbers: [], complexity: Array.from({length:3},()=>({})), domainTerms: [], architecture: [], counts: {} };
const base = { categories: cats, effort: { byCategory: {} }, lines: { physical: 1000, executable: 1000 }, meta: {} };
const curr = { categories: cats, effort: { byCategory: {} }, lines: { physical: 1000, executable: 1000 }, meta: {} };
const cfg = { ratchet: { health: { enabled: true, gateOn: 'overall', minOverall: 70 } } };
const res = runRatchet(cwd, base, curr, { AI_SNIFFTEST_CONFIG_JSON: JSON.stringify(cfg) });
assert.strictEqual(res.code, 0);
});

it('respects gateOn: passes with structural gate, fails with semantic gate', function () {
const cwd = mkTmp();
// structural=1 (score=90), semantic=5 (score=50), overall=6 (score=40)
const cats = { magicNumbers: Array.from({length:5},()=>({})), complexity: Array.from({length:1},()=>({})), domainTerms: [], architecture: [], counts: {} };
const base = { categories: cats, effort: { byCategory: {} }, lines: { physical: 1000, executable: 1000 }, meta: {} };
const curr = { categories: cats, effort: { byCategory: {} }, lines: { physical: 1000, executable: 1000 }, meta: {} };
const cfgStruct = { ratchet: { health: { enabled: true, gateOn: 'structural', minOverall: 90 } } };
const cfgSem = { ratchet: { health: { enabled: true, gateOn: 'semantic', minOverall: 90 } } };
const resStruct = runRatchet(cwd, base, curr, { AI_SNIFFTEST_CONFIG_JSON: JSON.stringify(cfgStruct) });
const resSem = runRatchet(cwd, base, curr, { AI_SNIFFTEST_CONFIG_JSON: JSON.stringify(cfgSem) });
assert.strictEqual(resStruct.code, 0);
assert.ok(resSem.out.includes('HEALTH-GATE FAIL'));
});

it('applies intentOverrides when intent=refactoring', function () {
const cwd = mkTmp();
const cats = { magicNumbers: [], complexity: Array.from({length:3},()=>({})), domainTerms: [], architecture: [], counts: {} }; // overall=70
const base = { categories: cats, effort: { byCategory: {} }, lines: { physical: 1000, executable: 1000 }, meta: {} };
const curr = { categories: cats, effort: { byCategory: {} }, lines: { physical: 1000, executable: 1000 }, meta: {} };
const cfg = { ratchet: { health: { enabled: true, gateOn: 'overall', minOverall: 80, intentOverrides: { refactoring: { minOverall: 60 } } } } };
// Without intent override → fail
const res1 = runRatchet(cwd, base, curr, { AI_SNIFFTEST_CONFIG_JSON: JSON.stringify(cfg) });
assert.ok(res1.out.includes('HEALTH-GATE FAIL'));
// With intent=refactoring → pass
const env2 = { AI_SNIFFTEST_CONFIG_JSON: JSON.stringify(cfg) };
// pass extra arg via environment by setting AI_SNIFFTEST_ARGS? Not supported; append to argv instead
// We call node directly including --intent=refactoring by modifying runRatchet here
const b = path.join(cwd, 'baseline.json');
const c = path.join(cwd, 'current.json');
writeJson(b, base);
writeJson(c, curr);
try {
const out = cp.execFileSync(process.execPath, [path.join(process.cwd(), 'scripts/ratchet.js'), `--baseline=${b}`, `--current=${c}`, '--intent=refactoring'], { cwd, env: { ...process.env, ...env2 }, stdio: 'pipe' });
assert.strictEqual(String(out).includes('HEALTH-GATE FAIL'), false);
} catch (e) {
assert.fail('expected pass with intent override');
}
});

it('bypasses gate when HEALTH_BYPASS=true', function () {
const cwd = mkTmp();
// Make baseline and current have same category counts so deltas=0 (avoid traditional ratchet failure)
Expand All @@ -60,4 +110,20 @@ describe('health gating', function () {
// Bypass active: gating not enforced; exit should be success since there are no deltas
assert.strictEqual(res.code, 0);
});

it('bypasses gate via commit token in latest commit message', function () {
const cwd = mkTmp();
// Keep deltas=0
const cats = { magicNumbers: Array.from({length:20},()=>({})), complexity: [], domainTerms: [], architecture: [], counts: {} }; // semantic heavy → low score
const base = { categories: cats, effort: { byCategory: {} }, lines: { physical: 1000, executable: 1000 }, meta: {} };
const curr = { categories: cats, effort: { byCategory: {} }, lines: { physical: 1000, executable: 1000 }, meta: {} };
// Initialize a git repo and commit with bypass token
cp.execSync('git init', { cwd });
fs.writeFileSync(path.join(cwd, 'tmp.txt'), 'x');
cp.execSync('git add .', { cwd });
cp.execSync("git -c user.email=test@example.com -c user.name='Test' commit -m '[health-bypass] demo'", { cwd });
const cfg = { ratchet: { health: { enabled: true, gateOn: 'semantic', minOverall: 90, bypass: { commitToken: '[health-bypass]' } } } };
const res = runRatchet(cwd, base, curr, { AI_SNIFFTEST_CONFIG_JSON: JSON.stringify(cfg) });
assert.strictEqual(res.code, 0);
});
});
43 changes: 43 additions & 0 deletions tests/lib/utils/health.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/* eslint-env mocha */
/* global describe, it */
'use strict';

const assert = require('assert');
const { computeHealth } = require('../../../lib/utils/health');

function summary({ exec = 1000, magicNumbers = 0, complexity = 0, domainTerms = 0, architecture = 0 } = {}) {
return {
magicNumbers,
complexity,
domainTerms,
architecture,
lines: { executable: exec }
};
}

describe('utils/health.computeHealth', function () {
it('returns 100s when there are no violations', function () {
const s = summary({ exec: 1000 });
const h = computeHealth(s);
assert.deepStrictEqual(h, { overall: 100, structural: 100, semantic: 100 });
});

it('drops to 0 when perK density is 10 (1000 exec, 10 total)', function () {
const s = summary({ exec: 1000, complexity: 5, architecture: 5 });
const h = computeHealth(s);
assert.strictEqual(h.overall, 0);
assert.strictEqual(h.structural, 0);
assert.strictEqual(h.semantic, 100);
});

it('computes expected rounding at partial densities', function () {
// 2000 exec LOC, total=5 → perK=2.5 → score=100 - 25 = 75
const s = summary({ exec: 2000, magicNumbers: 2, complexity: 3 });
const h = computeHealth(s);
assert.strictEqual(h.overall, 75);
// structural=3, perK=1.5 → 85
assert.strictEqual(h.structural, 85);
// semantic=2, perK=1.0 → 90
assert.strictEqual(h.semantic, 90);
});
});
Loading