Skip to content

Commit 762b67c

Browse files
authored
ci(context): enable context telemetry and workflow improvements (#209)
* docs(context): add telemetry guide; ci: add non-blocking context-aware telemetry step (burn-in) * ci(context): enable context telemetry and workflow improvements * ci(context): exclude scripts/**/*.js from architecture/complexity metrics to avoid ratchet noise * ci(ratchet): add tolerance band (total=5) via config to reduce false fails * ci(ratchet): implement hybrid density strategy (critical vs minor, per 1K LOC) with configurable tolerances
1 parent 3c29b4d commit 762b67c

File tree

5 files changed

+216
-27
lines changed

5 files changed

+216
-27
lines changed

.ai-coding-guide.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,5 +84,15 @@
8484
"errorHandling": "explicit",
8585
"asyncStyle": "async-await"
8686
}
87+
},
88+
"ratchet": {
89+
"strategy": "hybrid",
90+
"tolerance": { "total": 5 },
91+
"hybrid": {
92+
"critical": ["complexity", "architecture"],
93+
"minor": ["domainTerms", "magicNumbers"],
94+
"tolerance": { "criticalDensity": 1.05, "minorDensity": 1.2 }
95+
},
96+
"health": { "enabled": false }
8797
}
8898
}

.github/workflows/ci-ratchet.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,15 @@ on:
44
pull_request:
55
push:
66
branches: [main]
7+
workflow_dispatch:
78

89
jobs:
910
ratchet-and-tests:
1011
runs-on: ubuntu-latest
1112
steps:
1213
- uses: actions/checkout@v4
14+
with:
15+
fetch-depth: 0
1316

1417
- uses: actions/setup-node@v4
1518
with:
@@ -43,6 +46,7 @@ jobs:
4346
run: npm test
4447

4548
- name: Upload artifacts
49+
if: always()
4650
uses: actions/upload-artifact@v4
4751
with:
4852
name: analysis-and-lint-${{ github.sha }}

eslint.config.mjs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,17 @@ export default [
7272
'max-lines': ["warn",{"max":300,"skipBlankLines":true,"skipComments":true}]
7373
}
7474
},
75+
{
76+
files: ["scripts/**/*.js"],
77+
rules: {
78+
// Exclude support scripts from architecture/complexity metrics to keep product telemetry stable
79+
'complexity': 'off',
80+
'max-statements': 'off',
81+
'max-lines-per-function': 'off',
82+
'max-depth': 'off',
83+
'max-lines': 'off'
84+
}
85+
},
7586
{
7687
files: ["lib/rules/**/*.js"],
7788
rules: {

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"analyze:current": "node bin/cli.js analyze --input=lint-results.json --format=json --output=analysis-current.json",
2828
"analyze:baseline": "node bin/cli.js analyze --input=lint-results.json --format=json --output=analysis-baseline.json",
2929
"ratchet": "node scripts/ratchet.js --baseline=analysis-baseline.json --current=analysis-current.json",
30+
"ratchet:context": "node scripts/ratchet.js --mode=context --baseline=analysis-baseline.json --current=analysis-current.json",
3031
"ci:ratchet": "npm run lint:json && npm run analyze:current && npm run ratchet",
3132
"test": "mocha tests --recursive",
3233
"test:coverage": "c8 --reporter=text --reporter=lcov npm test",

scripts/ratchet.js

Lines changed: 190 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,6 @@ function summarize(payload) {
6565
complexity: len(cat.complexity),
6666
domainTerms: len(cat.domainTerms),
6767
architecture: len(cat.architecture),
68-
// Optional counts (informational)
6968
counts: {
7069
errors: num(cat.counts && cat.counts.errors),
7170
warnings: num(cat.counts && cat.counts.warnings),
@@ -133,7 +132,6 @@ function compare(base, curr) {
133132
const c = num(curr[f]);
134133
if (c > b) deltas.push({ key: f, base: b, current: c, type: 'count' });
135134
}
136-
// Effort ratchet (optional, do not fail but report increases)
137135
const effortInc = [];
138136
for (const f of fields) {
139137
const b = num(base.effortByCategory && base.effortByCategory[f]);
@@ -143,34 +141,80 @@ function compare(base, curr) {
143141
return { deltas, effortInc };
144142
}
145143

146-
function main() {
147-
const args = parseArgs(process.argv);
148-
const baselinePath = args.baseline || args._[0] || 'analysis-baseline.json';
149-
const currentPath = args.current || args._[1] || 'analysis-current.json';
144+
function detectIntentFromMetrics(base, curr) {
145+
const totalDelta = (curr.magicNumbers - base.magicNumbers) +
146+
(curr.complexity - base.complexity) +
147+
(curr.domainTerms - base.domainTerms) +
148+
(curr.architecture - base.architecture);
150149

151-
const base = readJson(baselinePath);
152-
if (!base) {
153-
console.log(`[ratchet] Baseline not found at ${baselinePath}.\n` +
154-
'Run: npm run lint:json && npm run analyze:baseline\n' +
155-
'Skipping ratchet (non-blocking)');
156-
process.exit(0);
157-
return;
150+
let intent = 'neutral';
151+
let confidence = 0.5;
152+
const signals = [];
153+
154+
if (totalDelta < -10) {
155+
intent = 'cleanup';
156+
confidence = 0.7;
157+
signals.push('Violations decreased significantly');
158+
} else if (totalDelta > 20 && curr.domainTerms > base.domainTerms * 1.3) {
159+
intent = 'ai-generation-suspect';
160+
confidence = 0.6;
161+
signals.push('Large increase in domain term violations');
162+
signals.push('Rapid violation growth pattern');
163+
} else if (curr.complexity < base.complexity * 0.8 && curr.architecture < base.architecture * 0.8) {
164+
intent = 'refactoring';
165+
confidence = 0.65;
166+
signals.push('Complexity decreased');
167+
signals.push('Architecture violations decreased');
158168
}
159-
const curr = readJson(currentPath);
160-
if (!curr) {
161-
console.error(`[ratchet] Current analysis not found at ${currentPath}. Run: npm run lint:json && npm run analyze:current`);
162-
process.exit(1);
163-
return;
169+
170+
return { intent, confidence, signals };
171+
}
172+
173+
function runContextMode(base, curr) {
174+
console.log('\n📊 Context-Aware Telemetry');
175+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
176+
console.log('Mode: Non-blocking (burn-in period)\n');
177+
178+
console.log('Category Counts:');
179+
const categories = [
180+
{ key: 'magicNumbers', label: 'Magic Numbers' },
181+
{ key: 'complexity', label: 'Complexity' },
182+
{ key: 'domainTerms', label: 'Domain Terms' },
183+
{ key: 'architecture', label: 'Architecture' }
184+
];
185+
186+
for (const cat of categories) {
187+
const baseVal = base[cat.key];
188+
const currVal = curr[cat.key];
189+
const delta = currVal - baseVal;
190+
const emoji = delta > 0 ? '⚠️' : (delta < 0 ? '✅' : '➖');
191+
const sign = delta > 0 ? '+' : '';
192+
console.log(` ${cat.label.padEnd(15)} ${baseVal}${currVal} (${sign}${delta}) ${emoji}`);
164193
}
165194

166-
const b = summarize(base);
167-
const c = summarize(curr);
168-
const { deltas, effortInc } = compare(b, c);
195+
console.log();
196+
197+
const { intent, confidence, signals } = detectIntentFromMetrics(base, curr);
198+
199+
console.log('Intent Detection:');
200+
console.log(` Detected: ${intent}`);
201+
console.log(` Confidence: ${(confidence * 100).toFixed(0)}%`);
202+
if (signals.length > 0) {
203+
console.log(' Signals:');
204+
signals.forEach(s => console.log(` • ${s}`));
205+
}
206+
207+
console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
208+
console.log('\n✅ Telemetry complete (non-blocking)\n');
209+
}
210+
211+
function runTraditionalMode(base, curr, args) {
212+
const { deltas, effortInc } = compare(base, curr);
169213

170214
// Health telemetry + optional gating
171215
const cfg = loadProjectConfig();
172216
const healthCfg = (cfg && cfg.ratchet && cfg.ratchet.health) || { enabled: false };
173-
const scores = computeHealth(c);
217+
const scores = computeHealth(curr);
174218
const gateOn = String(healthCfg.gateOn || 'overall').toLowerCase();
175219
const minOverall = Number(healthCfg.minOverall || 70);
176220
const intent = detectIntent(args);
@@ -181,6 +225,74 @@ function main() {
181225
const gateActive = !!healthCfg.enabled && !shouldBypass(healthCfg);
182226
const gateFail = gateActive && currentScore < threshold;
183227

228+
const strategy = (cfg && cfg.ratchet && cfg.ratchet.strategy) || 'classic';
229+
230+
// Hybrid strategy: density-based for critical vs minor categories (per 1K LOC)
231+
if (strategy === 'hybrid') {
232+
const hybridCfg = (cfg && cfg.ratchet && cfg.ratchet.hybrid) || {};
233+
const critical = Array.isArray(hybridCfg.critical) && hybridCfg.critical.length ? hybridCfg.critical : ['complexity', 'architecture'];
234+
const minor = Array.isArray(hybridCfg.minor) && hybridCfg.minor.length ? hybridCfg.minor : ['domainTerms', 'magicNumbers'];
235+
const tol = (hybridCfg.tolerance) || {};
236+
const critTol = Number(tol.criticalDensity || 1.05); // allow 5% increase
237+
const minorTol = Number(tol.minorDensity || 1.20); // allow 20% increase
238+
239+
const baseLoc = Math.max(1, Number(base.lines && base.lines.executable) || Number(base.lines && base.lines.physical) || 1);
240+
const currLoc = Math.max(1, Number(curr.lines && curr.lines.executable) || Number(curr.lines && curr.lines.physical) || 1);
241+
const perK = (count, loc) => (count / (loc / 1000));
242+
243+
let failed = false;
244+
245+
console.log('\n[ratchet] Hybrid density check (per 1K LOC)');
246+
console.log(`LOC: ${baseLoc}${currLoc} (${currLoc - baseLoc >= 0 ? '+' : ''}${currLoc - baseLoc})`);
247+
248+
console.log('\nCritical (strict):');
249+
for (const cat of critical) {
250+
const b = Number(base[cat]) || 0;
251+
const c = Number(curr[cat]) || 0;
252+
const bd = perK(b, baseLoc);
253+
const cd = perK(c, currLoc);
254+
const ratio = bd === 0 ? (cd === 0 ? 1 : Infinity) : (cd / bd);
255+
const status = ratio > critTol ? '❌' : '✅';
256+
console.log(` ${String(cat).padEnd(15)} ${b}${c} (density: ${bd.toFixed(2)}${cd.toFixed(2)}) ${status}`);
257+
if (ratio > critTol) failed = true;
258+
}
259+
260+
console.log('\nMinor (relaxed):');
261+
for (const cat of minor) {
262+
const b = Number(base[cat]) || 0;
263+
const c = Number(curr[cat]) || 0;
264+
const bd = perK(b, baseLoc);
265+
const cd = perK(c, currLoc);
266+
const ratio = bd === 0 ? (cd === 0 ? 1 : Infinity) : (cd / bd);
267+
const status = ratio > minorTol ? '⚠️' : '✅';
268+
console.log(` ${String(cat).padEnd(15)} ${b}${c} (density: ${bd.toFixed(2)}${cd.toFixed(2)}) ${status}`);
269+
}
270+
271+
// Health telemetry (informational)
272+
console.log(`\n[ratchet] Health (informational): overall=${scores.overall} structural=${scores.structural} semantic=${scores.semantic}`);
273+
274+
if (gateFail) {
275+
console.error(`[ratchet] HEALTH-GATE FAIL: ${failureMessage}`);
276+
console.error(` gateOn=${gateOn} threshold=${threshold} actual=${currentScore} intent=${intent}`);
277+
return 1;
278+
}
279+
280+
if (failed) {
281+
console.error('\n❌ [ratchet] FAIL: Critical violation density increased');
282+
console.error('Complexity and architecture are strictly controlled.');
283+
console.error('Refactor to reduce complexity or update baseline if intentional.');
284+
return 1;
285+
}
286+
287+
console.log('\n✅ [ratchet] PASS: Quality maintained or improved');
288+
return 0;
289+
}
290+
291+
// Classic strategy with tolerance band
292+
// Tolerance band (configurable) to avoid blocking normal fluctuation
293+
const tolCfg = (cfg && cfg.ratchet && cfg.ratchet.tolerance) || {};
294+
const totalTolerance = Number(tolCfg.total || 0);
295+
184296
if (deltas.length === 0) {
185297
console.log('[ratchet] OK: no increases in analyzer categories');
186298
if (effortInc.length) {
@@ -192,11 +304,27 @@ function main() {
192304
if (gateFail) {
193305
console.error(`[ratchet] HEALTH-GATE FAIL: ${failureMessage}`);
194306
console.error(` gateOn=${gateOn} threshold=${threshold} actual=${currentScore} intent=${intent}`);
195-
process.exit(1);
196-
return;
307+
return 1;
197308
}
198-
process.exit(0);
199-
return;
309+
return 0;
310+
}
311+
312+
// If within tolerance, allow pass with notice
313+
const totalIncrease = deltas.reduce((sum, d) => sum + (d.current - d.base), 0);
314+
if (totalIncrease <= totalTolerance) {
315+
console.log(`[ratchet] OK: within tolerance (+${totalIncrease} violations, tolerance: ${totalTolerance})`);
316+
for (const d of deltas) {
317+
console.log(` ${d.key}: ${d.base}${d.current} (+${d.current - d.base})`);
318+
}
319+
console.log('\nSmall increases accepted during active development.');
320+
// Health telemetry (informational) and optional gate still apply
321+
console.log(`[ratchet] Health (informational): overall=${scores.overall} structural=${scores.structural} semantic=${scores.semantic}`);
322+
if (gateFail) {
323+
console.error(`[ratchet] HEALTH-GATE FAIL: ${failureMessage}`);
324+
console.error(` gateOn=${gateOn} threshold=${threshold} actual=${currentScore} intent=${intent}`);
325+
return 1;
326+
}
327+
return 0;
200328
}
201329

202330
console.error('[ratchet] FAIL: new violations introduced');
@@ -214,7 +342,42 @@ function main() {
214342
console.error(' - compare with analysis-baseline.json');
215343
console.error('\nIf intentional reductions were made and counts decreased overall, refresh baseline:');
216344
console.error(' npm run analyze:baseline');
217-
process.exit(1);
345+
return 1;
346+
}
347+
348+
function main() {
349+
const args = parseArgs(process.argv);
350+
const mode = args.mode || 'traditional';
351+
const baselinePath = args.baseline || args._[0] || 'analysis-baseline.json';
352+
const currentPath = args.current || args._[1] || 'analysis-current.json';
353+
354+
const base = readJson(baselinePath);
355+
if (!base) {
356+
console.log(`[ratchet] Baseline not found at ${baselinePath}.\n` +
357+
'Run: npm run lint:json && npm run analyze:baseline\n' +
358+
'Skipping ratchet (non-blocking)');
359+
process.exit(0);
360+
return;
361+
}
362+
363+
const curr = readJson(currentPath);
364+
if (!curr) {
365+
console.error(`[ratchet] Current analysis not found at ${currentPath}. Run: npm run lint:json && npm run analyze:current`);
366+
process.exit(1);
367+
return;
368+
}
369+
370+
const b = summarize(base);
371+
const c = summarize(curr);
372+
373+
if (mode === 'context') {
374+
runContextMode(b, c);
375+
process.exit(0);
376+
return;
377+
}
378+
379+
const exitCode = runTraditionalMode(b, c, args);
380+
process.exit(exitCode);
218381
}
219382

220383
main();

0 commit comments

Comments
 (0)