Skip to content

Commit f5dfe39

Browse files
authored
feat: CI-safe init scripts + ratchet DX (examples, unlock hint) (#212)
feat: CI-safe init scripts and ratchet DX improvements - Add lint:ci and ratchet scripts to init-generated package.json - README: CI snippet to run lint:ci then ci:ratchet - Ratchet: include examples of new violations and unlock hint on failure Refs: #210
1 parent 2306e34 commit f5dfe39

File tree

4 files changed

+122
-3
lines changed

4 files changed

+122
-3
lines changed

README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,31 @@ Running `init` creates:
3131

3232
## Installation
3333

34+
### CI-safe setup (recommended)
35+
36+
Add scripts to your package.json:
37+
38+
```json
39+
{
40+
"scripts": {
41+
"lint": "eslint .",
42+
"lint:ci": "eslint . || true",
43+
"lint:json": "eslint . -f json -o lint-results.json",
44+
"analyze:current": "eslint-plugin-ai-code-snifftest analyze --input=lint-results.json --format=json --output=analysis-current.json",
45+
"ratchet": "eslint-plugin-ai-code-snifftest ratchet"
46+
}
47+
}
48+
```
49+
50+
GitHub Actions example:
51+
52+
```yaml
53+
- name: Lint (non-blocking)
54+
run: npm run lint:ci
55+
- name: Ratchet (blocks on regression)
56+
run: npm run ci:ratchet
57+
```
58+
3459
### Requirements
3560
- Node.js 18+
3661
- ESLint 9+

lib/commands/init/index.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ const { writeConfig } = require(path.join(__dirname, '..', '..', 'generators', '
2222
const { writeAgentsMd } = require(path.join(__dirname, '..', '..', 'generators', 'agents-md'));
2323
const { writeCursorRules } = require(path.join(__dirname, '..', '..', 'generators', 'cursorrules'));
2424
const { writeEslintConfig } = require(path.join(__dirname, '..', '..', 'generators', 'eslint-config'));
25+
const { ensurePackageScripts } = require(path.join(__dirname, '..', '..', 'generators', 'package-scripts'));
2526

2627
// Utilities
2728
const { applyFingerprintToConfig } = require(path.join(__dirname, '..', '..', 'utils', 'fingerprint'));
@@ -105,7 +106,9 @@ const external = shouldEnableExternalConstants(args);
105106
console.log('Found WARP.md — preserving it; generated AGENTS.md alongside.');
106107
}
107108
}
108-
if (shouldWriteEslint) writeEslintConfig(cwd, cfg);
109+
if (shouldWriteEslint) writeEslintConfig(cwd, cfg);
110+
// Add CI-safe scripts (non-destructive; only adds missing)
111+
try { ensurePackageScripts(cwd); } catch { /* ignore */ }
109112

110113
// Post-init guidance
111114
console.log('\nProject initialized.');

lib/generators/package-scripts.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
'use strict';
2+
3+
const fs = require('fs');
4+
const path = require('path');
5+
6+
function ensurePackageScripts(cwd) {
7+
const pkgPath = path.join(cwd, 'package.json');
8+
if (!fs.existsSync(pkgPath)) return false;
9+
let changed = false;
10+
try {
11+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
12+
pkg.scripts = pkg.scripts || {};
13+
14+
const want = {
15+
'lint': 'eslint .',
16+
'lint:ci': 'eslint . || true',
17+
'lint:json': 'eslint . -f json -o lint-results.json',
18+
'analyze:current': 'eslint-plugin-ai-code-snifftest analyze --input=lint-results.json --format=json --output=analysis-current.json',
19+
'ratchet': 'eslint-plugin-ai-code-snifftest ratchet',
20+
'ratchet:context': 'eslint-plugin-ai-code-snifftest ratchet --mode=context',
21+
'ci:ratchet': 'npm run lint:json && npm run analyze:current && npm run ratchet'
22+
};
23+
24+
for (const [k, v] of Object.entries(want)) {
25+
if (!pkg.scripts[k]) {
26+
pkg.scripts[k] = v;
27+
changed = true;
28+
}
29+
}
30+
31+
if (changed) {
32+
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
33+
console.log('Updated package.json with CI-safe lint and ratchet scripts.');
34+
}
35+
return changed;
36+
} catch (e) {
37+
console.warn(`Warning: could not update package.json scripts: ${e && e.message}`);
38+
return false;
39+
}
40+
}
41+
42+
module.exports = { ensurePackageScripts };

scripts/ratchet.js

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,19 @@ function detectIntentFromMetrics(base, curr) {
170170
return { intent, confidence, signals };
171171
}
172172

173+
function formatExamples(messages, ruleId, max = 3) {
174+
const out = [];
175+
for (const m of messages || []) {
176+
if (ruleId && m.ruleId !== ruleId) continue;
177+
if (!m.ruleId) continue;
178+
const hasLine = (m.line !== null && m.line !== undefined);
179+
const loc = hasLine ? `${m.line}:${m.column || 1}` : '';
180+
out.push(` • ${m.filePath || m.filename || 'file'}:${loc}${m.message}`);
181+
if (out.length >= max) break;
182+
}
183+
return out.join('\n');
184+
}
185+
173186
function runContextMode(base, curr) {
174187
console.log('\n📊 Context-Aware Telemetry');
175188
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
@@ -209,6 +222,21 @@ const { intent, confidence, signals } = detectIntentFromMetrics(base, curr);
209222
}
210223

211224
function runTraditionalMode(base, curr, args) {
225+
// Optional lint file for better failure messaging
226+
let lintMessages = null;
227+
try {
228+
const lintPath = args.lint || args.lintCurrent || process.env.AI_SNIFFTEST_LINT_CURRENT;
229+
if (lintPath && fs.existsSync(lintPath)) {
230+
const raw = readJson(lintPath);
231+
if (Array.isArray(raw)) {
232+
lintMessages = [];
233+
for (const f of raw) {
234+
const filePath = f.filePath || f.filename;
235+
for (const m of (f.messages || [])) lintMessages.push({ ...m, filePath });
236+
}
237+
}
238+
}
239+
} catch (_) { /* ignore */ }
212240
const { deltas, effortInc } = compare(base, curr);
213241

214242
// Health telemetry + optional gating
@@ -327,10 +355,31 @@ function runTraditionalMode(base, curr, args) {
327355
return 0;
328356
}
329357

330-
console.error('[ratchet] FAIL: new violations introduced');
331-
for (const d of deltas) {
358+
console.error('[ratchet] FAIL: new violations introduced');
359+
const ruleMap = {
360+
magicNumbers: 'ai-code-snifftest/no-redundant-calculations',
361+
complexity: null,
362+
domainTerms: null,
363+
architecture: null,
364+
eqeqeq: 'eqeqeq',
365+
camelcase: 'camelcase',
366+
empty: 'no-empty'
367+
};
368+
for (const d of deltas) {
332369
console.error(` ${d.key}: ${d.base} -> ${d.current} (+${d.current - d.base})`);
370+
try {
371+
const ruleId = ruleMap[d.key];
372+
if (lintMessages && ruleId) {
373+
const ex = formatExamples(lintMessages, ruleId, 3);
374+
if (ex) {
375+
console.error(' New occurrences:');
376+
console.error(ex);
377+
}
378+
}
379+
} catch (_) { /* ignore */ }
333380
}
381+
console.error('\n💡 To fix: address new violations, or if intentional, unlock this check:');
382+
console.error(' npm run ratchet -- --unlock=<rule>');
334383
// Health telemetry (informational) and optional gate
335384
console.error(`[ratchet] Health (informational): overall=${scores.overall} structural=${scores.structural} semantic=${scores.semantic}`);
336385
if (gateFail) {

0 commit comments

Comments
 (0)