@theglitchking/gimme-the-lint 2.2.0 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +34 -0
- package/bin/gimme-the-lint.js +82 -2
- package/lib/adapters/adapter.js +7 -1
- package/lib/adapters/biome.js +13 -3
- package/lib/adapters/eslint.js +15 -3
- package/lib/adapters/ruff.js +2 -1
- package/lib/baseline-store.js +15 -1
- package/lib/baseline.js +77 -6
- package/lib/check.js +19 -1
- package/lib/config-manager.js +40 -0
- package/lib/drift.js +4 -1
- package/lib/migrate.js +14 -0
- package/lib/project-model.js +58 -19
- package/lib/report.js +37 -3
- package/lib/toolchain.js +4 -1
- package/package.json +1 -1
|
@@ -6,13 +6,13 @@
|
|
|
6
6
|
},
|
|
7
7
|
"metadata": {
|
|
8
8
|
"description": "Official marketplace for gimme-the-lint - polyglot progressive linting with per-app baselines and drift detection",
|
|
9
|
-
"version": "2.
|
|
9
|
+
"version": "2.3.0"
|
|
10
10
|
},
|
|
11
11
|
"plugins": [
|
|
12
12
|
{
|
|
13
13
|
"name": "gimme-the-lint",
|
|
14
14
|
"description": "Polyglot progressive linting for monorepos — adapter-driven baselines, per-app drift detection, and idempotent skips across JavaScript/TypeScript, Python, Go, Rust, Terraform, and Ansible.",
|
|
15
|
-
"version": "2.
|
|
15
|
+
"version": "2.3.0",
|
|
16
16
|
"author": {
|
|
17
17
|
"name": "TheGlitchKing"
|
|
18
18
|
},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gimme-the-lint",
|
|
3
3
|
"description": "Polyglot progressive linting for monorepos — adapter-driven baselines, per-app drift detection, and idempotent skips across JavaScript/TypeScript, Python, Go, Rust, Terraform, and Ansible.",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.3.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "TheGlitchKing",
|
|
7
7
|
"email": "theglitchking@users.noreply.github.com"
|
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,40 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [2.3.0] - 2026-05-17
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- **Bug A — monorepo linter binary resolution.** The ESLint and Biome adapters
|
|
12
|
+
resolved `node_modules/.bin/<linter>` only at the repo root, so in a monorepo
|
|
13
|
+
(where each JS app carries its own `node_modules`) `available()` returned
|
|
14
|
+
false and the linter was silently skipped — capturing an empty baseline.
|
|
15
|
+
Adapters now resolve the binary app-dir-first (app → repo root → PATH) and
|
|
16
|
+
run inside the app directory so the app's own flat config is discovered. The
|
|
17
|
+
Ruff adapter resolves its `.venv` the same way.
|
|
18
|
+
- **Bug B — discovery bound the wrong directories.** Manifest discovery treated
|
|
19
|
+
a bare `requirements.txt` as a ruff app marker (binding nested load-test and
|
|
20
|
+
utility dirs) and discarded a repo-root config whenever any nested manifest
|
|
21
|
+
existed (leaving the real app unbound). `requirements.txt` is no longer a
|
|
22
|
+
discovery marker — `pyproject.toml`, `ruff.toml`, `.ruff.toml` and `setup.py`
|
|
23
|
+
are — and workspace-root detection is now **per linter**, so a repo-root
|
|
24
|
+
`[tool.ruff]` config binds the root even when nested `package.json` apps sit
|
|
25
|
+
below it.
|
|
26
|
+
- **Bug C — "couldn't run" collapsed into "skipped".** An unavailable or
|
|
27
|
+
errored linter was recorded with the status used for "no code here", making
|
|
28
|
+
an incomplete baseline indistinguishable from a clean one — every
|
|
29
|
+
pre-existing violation later counted as new and blocked the commit. New
|
|
30
|
+
baseline statuses `unavailable` and `error` keep incomplete baselines
|
|
31
|
+
distinct; `migrate` and `baseline` print a prominent summary and exit
|
|
32
|
+
non-zero (override with `--allow-incomplete`); `hooks` refuses to install
|
|
33
|
+
against an incomplete baseline (override with `--force`); `check` reports
|
|
34
|
+
`needs-baseline` instead of flooding new violations.
|
|
35
|
+
|
|
36
|
+
### Added
|
|
37
|
+
- `migrate` now writes the discovered app/linter layout into
|
|
38
|
+
`gimme-the-lint.config.js` as an explicit `apps` map, so the guess is visible
|
|
39
|
+
and editable instead of silently re-derived on every run. It also emits a
|
|
40
|
+
warning when the layout is ambiguous (a repo-root config plus nested apps).
|
|
41
|
+
|
|
8
42
|
## [2.2.0] - 2026-05-17
|
|
9
43
|
|
|
10
44
|
### Added
|
package/bin/gimme-the-lint.js
CHANGED
|
@@ -154,6 +154,7 @@ program
|
|
|
154
154
|
.description('Create or refresh progressive-lint baselines')
|
|
155
155
|
.option('--strict', 'Fail when a linter is missing for code that is present')
|
|
156
156
|
.option('--empty', 'Write empty baselines (greenfield: treat every violation as new)')
|
|
157
|
+
.option('--allow-incomplete', 'Exit 0 even if some linters could not be baselined')
|
|
157
158
|
.action(async (target, opts) => {
|
|
158
159
|
const chalk = require('chalk');
|
|
159
160
|
const { runBaseline } = require('../lib/baseline');
|
|
@@ -166,6 +167,17 @@ program
|
|
|
166
167
|
});
|
|
167
168
|
console.log(formatBaselineReport(report, process.cwd()));
|
|
168
169
|
console.log('');
|
|
170
|
+
// A baseline that could not capture a linter is incomplete — fail loudly
|
|
171
|
+
// rather than reporting success, unless the user opts in.
|
|
172
|
+
if (report.incomplete && report.incomplete.length && !opts.allowIncomplete) {
|
|
173
|
+
console.error(
|
|
174
|
+
chalk.red(
|
|
175
|
+
`✗ Baseline INCOMPLETE — ${report.incomplete.length} linter(s) not captured. ` +
|
|
176
|
+
'Install them and re-run, or pass --allow-incomplete.\n'
|
|
177
|
+
)
|
|
178
|
+
);
|
|
179
|
+
process.exit(1);
|
|
180
|
+
}
|
|
169
181
|
} catch (err) {
|
|
170
182
|
console.error(chalk.red(`\n✗ ${err.message}\n`));
|
|
171
183
|
process.exit(1);
|
|
@@ -176,6 +188,7 @@ program
|
|
|
176
188
|
.command('migrate')
|
|
177
189
|
.description('Migrate a v1 (.lttf) project to the v2 .gtl/ layout')
|
|
178
190
|
.option('--strict', 'Fail when a linter is missing for code that is present')
|
|
191
|
+
.option('--allow-incomplete', 'Exit 0 even if some linters could not be baselined')
|
|
179
192
|
.action(async (opts) => {
|
|
180
193
|
const chalk = require('chalk');
|
|
181
194
|
const { migrate } = require('../lib/migrate');
|
|
@@ -188,9 +201,46 @@ program
|
|
|
188
201
|
console.log(chalk.green('\n✓ Migrated to the v2 .gtl/ layout\n'));
|
|
189
202
|
console.log(` Legacy baselines backed up: ${result.backedUp.join(', ')}`);
|
|
190
203
|
console.log(chalk.dim(` → ${result.backupPath}`));
|
|
204
|
+
if (result.appsConfig && result.appsConfig.created) {
|
|
205
|
+
console.log(
|
|
206
|
+
` Wrote explicit app map → ${path.basename(result.appsConfig.path)}`
|
|
207
|
+
);
|
|
208
|
+
}
|
|
191
209
|
console.log(` Re-baselined ${result.baseline.unitCount} app(s) into .gtl/`);
|
|
210
|
+
for (const w of result.discoveryWarnings || []) {
|
|
211
|
+
console.log(chalk.yellow(` ⚠ ${w}`));
|
|
212
|
+
}
|
|
192
213
|
console.log('');
|
|
193
|
-
|
|
214
|
+
|
|
215
|
+
// An incomplete baseline must never be reported as a clean migration.
|
|
216
|
+
if (result.incomplete && result.incomplete.length) {
|
|
217
|
+
console.error(
|
|
218
|
+
chalk.red(`✗ ${result.incomplete.length} linter(s) were NOT baselined:`)
|
|
219
|
+
);
|
|
220
|
+
for (const i of result.incomplete) {
|
|
221
|
+
console.error(chalk.red(` ${i.app} · ${i.linter} (${i.status})`));
|
|
222
|
+
}
|
|
223
|
+
console.error(
|
|
224
|
+
chalk.yellow(
|
|
225
|
+
' The baseline is INCOMPLETE — install the missing linters and run\n' +
|
|
226
|
+
' `gimme-the-lint baseline`, then commit .gtl/.'
|
|
227
|
+
)
|
|
228
|
+
);
|
|
229
|
+
if (!opts.allowIncomplete) {
|
|
230
|
+
console.error(
|
|
231
|
+
chalk.red(
|
|
232
|
+
'\n✗ Migration finished with an incomplete baseline ' +
|
|
233
|
+
'(pass --allow-incomplete to accept).\n'
|
|
234
|
+
)
|
|
235
|
+
);
|
|
236
|
+
process.exit(1);
|
|
237
|
+
}
|
|
238
|
+
console.log('');
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
console.log(
|
|
242
|
+
'Next: review the new .gtl/ directory and gimme-the-lint.config.js, then commit them.'
|
|
243
|
+
);
|
|
194
244
|
console.log('');
|
|
195
245
|
} catch (err) {
|
|
196
246
|
console.error(chalk.red(`\n✗ Migration failed: ${err.message}\n`));
|
|
@@ -210,13 +260,43 @@ program
|
|
|
210
260
|
program
|
|
211
261
|
.command('hooks')
|
|
212
262
|
.description('Install git hooks for pre-commit linting')
|
|
213
|
-
.
|
|
263
|
+
.option('--force', 'Install even when the baseline is incomplete')
|
|
264
|
+
.action(async (opts) => {
|
|
214
265
|
const chalk = require('chalk');
|
|
215
266
|
const gitHooksManager = require('../lib/git-hooks-manager');
|
|
267
|
+
const { findIncompleteBaselines } = require('../lib/baseline');
|
|
216
268
|
|
|
217
269
|
try {
|
|
270
|
+
// Refuse to gate commits against an incomplete baseline: hooks would
|
|
271
|
+
// diff against linter sections that were never captured, so every
|
|
272
|
+
// pre-existing violation would count as new and block the commit.
|
|
273
|
+
const incomplete = await findIncompleteBaselines(process.cwd());
|
|
274
|
+
if (incomplete.length && !opts.force) {
|
|
275
|
+
console.error(
|
|
276
|
+
chalk.red('\n✗ Refusing to install hooks — the baseline is incomplete:\n')
|
|
277
|
+
);
|
|
278
|
+
for (const i of incomplete) {
|
|
279
|
+
console.error(chalk.red(` ${i.app} · ${i.linter} (${i.status})`));
|
|
280
|
+
}
|
|
281
|
+
console.error(
|
|
282
|
+
chalk.yellow(
|
|
283
|
+
'\n These linters were never baselined, so a pre-commit hook would\n' +
|
|
284
|
+
' flag every pre-existing violation as new. Install the missing\n' +
|
|
285
|
+
' linters and run `gimme-the-lint baseline`, or pass --force.\n'
|
|
286
|
+
)
|
|
287
|
+
);
|
|
288
|
+
process.exit(1);
|
|
289
|
+
}
|
|
290
|
+
|
|
218
291
|
const installed = await gitHooksManager.installHooks(process.cwd());
|
|
219
292
|
console.log(chalk.green(`\n✓ Installed git hooks: ${installed.join(', ')}\n`));
|
|
293
|
+
if (incomplete.length && opts.force) {
|
|
294
|
+
console.log(
|
|
295
|
+
chalk.yellow(
|
|
296
|
+
'⚠ Installed against an INCOMPLETE baseline (--force) — re-baseline soon.\n'
|
|
297
|
+
)
|
|
298
|
+
);
|
|
299
|
+
}
|
|
220
300
|
} catch (e) {
|
|
221
301
|
console.error(chalk.red(`\n✗ ${e.message}\n`));
|
|
222
302
|
process.exit(1);
|
package/lib/adapters/adapter.js
CHANGED
|
@@ -26,8 +26,14 @@ const DEFAULT_IGNORE_DIRS = new Set([
|
|
|
26
26
|
const SCAN_DEPTH = 5;
|
|
27
27
|
|
|
28
28
|
class LinterAdapter {
|
|
29
|
-
constructor({ projectRoot } = {}) {
|
|
29
|
+
constructor({ projectRoot, appRoot } = {}) {
|
|
30
30
|
this.projectRoot = projectRoot || process.cwd();
|
|
31
|
+
// appRoot is the directory of the specific app/unit being linted. In a
|
|
32
|
+
// monorepo each app carries its own node_modules / .venv, so project-local
|
|
33
|
+
// tool resolution and config discovery must start here — not at the repo
|
|
34
|
+
// root, which often has neither. Defaults to projectRoot for single-package
|
|
35
|
+
// projects and for callers that do not supply it.
|
|
36
|
+
this.appRoot = appRoot || this.projectRoot;
|
|
31
37
|
}
|
|
32
38
|
|
|
33
39
|
// --- identity (override in subclasses) ---
|
package/lib/adapters/biome.js
CHANGED
|
@@ -38,9 +38,14 @@ class BiomeAdapter extends LinterAdapter {
|
|
|
38
38
|
return ['.js', '.jsx', '.mjs', '.cjs', '.ts', '.tsx'];
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
/**
|
|
41
|
+
/**
|
|
42
|
+
* Prefer the app-local Biome, then the repo-root Biome, then PATH. Each JS
|
|
43
|
+
* app in a monorepo has its own node_modules; resolving only at projectRoot
|
|
44
|
+
* makes available() falsely return false for app-installed Biome.
|
|
45
|
+
*/
|
|
42
46
|
get binary() {
|
|
43
47
|
return this.resolveBinary([
|
|
48
|
+
path.join(this.appRoot, 'node_modules', '.bin', 'biome'),
|
|
44
49
|
path.join(this.projectRoot, 'node_modules', '.bin', 'biome'),
|
|
45
50
|
'biome',
|
|
46
51
|
]);
|
|
@@ -52,9 +57,14 @@ class BiomeAdapter extends LinterAdapter {
|
|
|
52
57
|
|
|
53
58
|
buildCommand(targets, opts = {}) {
|
|
54
59
|
const args = ['lint', '--reporter=json'];
|
|
55
|
-
|
|
60
|
+
// Run inside the app directory so Biome resolves the app's own biome.json.
|
|
61
|
+
const cwd = opts.cwd || this.appRoot;
|
|
62
|
+
const list = (targets && targets.length ? targets : ['.']).map((t) => {
|
|
63
|
+
const rel = path.relative(cwd, path.resolve(this.projectRoot, t));
|
|
64
|
+
return rel === '' ? '.' : rel;
|
|
65
|
+
});
|
|
56
66
|
args.push(...list);
|
|
57
|
-
return { cmd: this.binary, args, cwd
|
|
67
|
+
return { cmd: this.binary, args, cwd };
|
|
58
68
|
}
|
|
59
69
|
|
|
60
70
|
parse(stdout) {
|
package/lib/adapters/eslint.js
CHANGED
|
@@ -40,9 +40,15 @@ class EslintAdapter extends LinterAdapter {
|
|
|
40
40
|
return ['.js', '.jsx', '.mjs', '.cjs', '.ts', '.tsx'];
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
/**
|
|
43
|
+
/**
|
|
44
|
+
* Prefer the app-local ESLint, then the repo-root ESLint, then PATH. In a
|
|
45
|
+
* monorepo each JS app has its own node_modules; the repo root often has
|
|
46
|
+
* none, so resolving only at projectRoot makes available() falsely return
|
|
47
|
+
* false and the linter gets silently skipped.
|
|
48
|
+
*/
|
|
44
49
|
get binary() {
|
|
45
50
|
return this.resolveBinary([
|
|
51
|
+
path.join(this.appRoot, 'node_modules', '.bin', 'eslint'),
|
|
46
52
|
path.join(this.projectRoot, 'node_modules', '.bin', 'eslint'),
|
|
47
53
|
'eslint',
|
|
48
54
|
]);
|
|
@@ -55,9 +61,15 @@ class EslintAdapter extends LinterAdapter {
|
|
|
55
61
|
buildCommand(targets, opts = {}) {
|
|
56
62
|
const args = ['--format=json'];
|
|
57
63
|
if (opts.fix) args.push('--fix');
|
|
58
|
-
|
|
64
|
+
// Run inside the app directory so ESLint resolves the app's own flat
|
|
65
|
+
// config (eslint.config.js) — in a monorepo the repo root often has none.
|
|
66
|
+
const cwd = opts.cwd || this.appRoot;
|
|
67
|
+
const list = (targets && targets.length ? targets : ['.']).map((t) => {
|
|
68
|
+
const rel = path.relative(cwd, path.resolve(this.projectRoot, t));
|
|
69
|
+
return rel === '' ? '.' : rel;
|
|
70
|
+
});
|
|
59
71
|
args.push(...list);
|
|
60
|
-
return { cmd: this.binary, args, cwd
|
|
72
|
+
return { cmd: this.binary, args, cwd };
|
|
61
73
|
}
|
|
62
74
|
|
|
63
75
|
parse(stdout) {
|
package/lib/adapters/ruff.js
CHANGED
|
@@ -31,9 +31,10 @@ class RuffAdapter extends LinterAdapter {
|
|
|
31
31
|
return ['.py', '.pyi'];
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
/** Prefer the
|
|
34
|
+
/** Prefer the app venv's Ruff, then the repo-root venv, then PATH. */
|
|
35
35
|
get binary() {
|
|
36
36
|
return this.resolveBinary([
|
|
37
|
+
path.join(this.appRoot, '.venv', 'bin', 'ruff'),
|
|
37
38
|
path.join(this.projectRoot, '.venv', 'bin', 'ruff'),
|
|
38
39
|
'ruff',
|
|
39
40
|
]);
|
package/lib/baseline-store.js
CHANGED
|
@@ -29,12 +29,25 @@ const { fingerprint } = require('./fingerprint');
|
|
|
29
29
|
const SCHEMA_VERSION = 2;
|
|
30
30
|
|
|
31
31
|
// Per-linter status values recorded in each section / the manifest.
|
|
32
|
+
//
|
|
33
|
+
// UNAVAILABLE and ERROR mean the baseline for that linter was NOT captured —
|
|
34
|
+
// they must stay distinct from CLEAN (linter ran, nothing found) and from a
|
|
35
|
+
// genuine empty baseline. A section in either state is an INCOMPLETE baseline:
|
|
36
|
+
// treating it as clean would make every pre-existing violation count as new
|
|
37
|
+
// the first time the linter actually runs.
|
|
32
38
|
const STATUS = Object.freeze({
|
|
33
39
|
BASELINED: 'baselined', // violations captured into the baseline
|
|
34
40
|
CLEAN: 'clean', // linter ran, found nothing to baseline
|
|
35
|
-
SKIPPED: 'skipped', //
|
|
41
|
+
SKIPPED: 'skipped', // intentionally not baselined (kept for back-compat)
|
|
42
|
+
UNAVAILABLE: 'unavailable', // code present but the linter binary is missing
|
|
43
|
+
ERROR: 'error', // the linter applies here but failed to run
|
|
36
44
|
});
|
|
37
45
|
|
|
46
|
+
/** True when a section's status means the baseline was not actually captured. */
|
|
47
|
+
function isIncompleteStatus(status) {
|
|
48
|
+
return status === STATUS.UNAVAILABLE || status === STATUS.ERROR;
|
|
49
|
+
}
|
|
50
|
+
|
|
38
51
|
/** Build a {fingerprint: count} map from a list of NormalizedViolations. */
|
|
39
52
|
function buildFingerprintMap(violations) {
|
|
40
53
|
const map = {};
|
|
@@ -109,6 +122,7 @@ async function writeBaseline(baselinePath, baseline) {
|
|
|
109
122
|
module.exports = {
|
|
110
123
|
SCHEMA_VERSION,
|
|
111
124
|
STATUS,
|
|
125
|
+
isIncompleteStatus,
|
|
112
126
|
buildFingerprintMap,
|
|
113
127
|
createLinterSection,
|
|
114
128
|
emptyBaseline,
|
package/lib/baseline.js
CHANGED
|
@@ -11,6 +11,12 @@ const gtlManifest = require('./gtl-manifest');
|
|
|
11
11
|
// .gtl/apps/<app>/baseline.json. After this, only NEW violations block.
|
|
12
12
|
// With opts.noBaseline it writes an EMPTY baseline instead — the greenfield
|
|
13
13
|
// "strict from day one" stance (Phase 5).
|
|
14
|
+
//
|
|
15
|
+
// A linter that is unavailable or errors out is recorded with an INCOMPLETE
|
|
16
|
+
// status (unavailable / error), never as a clean baseline — see baseline-store
|
|
17
|
+
// STATUS. runBaseline() surfaces these in `incomplete` so callers (migrate,
|
|
18
|
+
// the baseline CLI, the hooks installer) can refuse to gate commits against a
|
|
19
|
+
// baseline that never actually captured a linter.
|
|
14
20
|
|
|
15
21
|
/** Hash the first config file found for an adapter (unit root, then project). */
|
|
16
22
|
async function configHashFor(projectRoot, unitRoot, adapter) {
|
|
@@ -33,7 +39,10 @@ async function baselineUnit(projectRoot, unit, opts = {}) {
|
|
|
33
39
|
for (const linterId of unit.linters) {
|
|
34
40
|
let adapter;
|
|
35
41
|
try {
|
|
36
|
-
adapter = adapters.getAdapter(linterId, {
|
|
42
|
+
adapter = adapters.getAdapter(linterId, {
|
|
43
|
+
projectRoot,
|
|
44
|
+
appRoot: unit.root,
|
|
45
|
+
});
|
|
37
46
|
} catch (err) {
|
|
38
47
|
sections.push({ linter: linterId, status: 'error', reason: err.message });
|
|
39
48
|
continue;
|
|
@@ -44,6 +53,9 @@ async function baselineUnit(projectRoot, unit, opts = {}) {
|
|
|
44
53
|
continue;
|
|
45
54
|
}
|
|
46
55
|
|
|
56
|
+
// Code is present but the linter binary is missing. Under strict/offline
|
|
57
|
+
// this is a hard failure; otherwise record an UNAVAILABLE section — NOT a
|
|
58
|
+
// clean one — so downstream knows this baseline was never captured.
|
|
47
59
|
if (!adapter.available()) {
|
|
48
60
|
if (opts.strict) {
|
|
49
61
|
const err = new Error(
|
|
@@ -56,13 +68,13 @@ async function baselineUnit(projectRoot, unit, opts = {}) {
|
|
|
56
68
|
baseline,
|
|
57
69
|
linterId,
|
|
58
70
|
baselineStore.createLinterSection([], {
|
|
59
|
-
status: baselineStore.STATUS.
|
|
71
|
+
status: baselineStore.STATUS.UNAVAILABLE,
|
|
60
72
|
})
|
|
61
73
|
);
|
|
62
74
|
sections.push({
|
|
63
75
|
linter: linterId,
|
|
64
|
-
status:
|
|
65
|
-
reason: `${adapter.id} not installed`,
|
|
76
|
+
status: baselineStore.STATUS.UNAVAILABLE,
|
|
77
|
+
reason: `${adapter.id} is not installed`,
|
|
66
78
|
});
|
|
67
79
|
continue;
|
|
68
80
|
}
|
|
@@ -72,7 +84,20 @@ async function baselineUnit(projectRoot, unit, opts = {}) {
|
|
|
72
84
|
try {
|
|
73
85
|
violations = adapter.lint([unit.appPath], {});
|
|
74
86
|
} catch (err) {
|
|
75
|
-
|
|
87
|
+
// The linter applies here but could not run — record an ERROR section
|
|
88
|
+
// rather than leaving the baseline silently without this linter.
|
|
89
|
+
baselineStore.setLinterSection(
|
|
90
|
+
baseline,
|
|
91
|
+
linterId,
|
|
92
|
+
baselineStore.createLinterSection([], {
|
|
93
|
+
status: baselineStore.STATUS.ERROR,
|
|
94
|
+
})
|
|
95
|
+
);
|
|
96
|
+
sections.push({
|
|
97
|
+
linter: linterId,
|
|
98
|
+
status: baselineStore.STATUS.ERROR,
|
|
99
|
+
reason: err.message,
|
|
100
|
+
});
|
|
76
101
|
continue;
|
|
77
102
|
}
|
|
78
103
|
}
|
|
@@ -101,10 +126,29 @@ async function baselineUnit(projectRoot, unit, opts = {}) {
|
|
|
101
126
|
};
|
|
102
127
|
}
|
|
103
128
|
|
|
129
|
+
/** Collect every incomplete (unavailable/errored) section from unit results. */
|
|
130
|
+
function collectIncomplete(unitResults) {
|
|
131
|
+
const incomplete = [];
|
|
132
|
+
for (const u of unitResults || []) {
|
|
133
|
+
for (const s of u.sections || []) {
|
|
134
|
+
if (baselineStore.isIncompleteStatus(s.status)) {
|
|
135
|
+
incomplete.push({
|
|
136
|
+
app: u.appPath,
|
|
137
|
+
linter: s.linter,
|
|
138
|
+
status: s.status,
|
|
139
|
+
reason: s.reason,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return incomplete;
|
|
145
|
+
}
|
|
146
|
+
|
|
104
147
|
/**
|
|
105
148
|
* Create/refresh baselines for the whole project.
|
|
106
149
|
* @param {string} projectRoot
|
|
107
150
|
* @param {object} opts { noBaseline, strict }
|
|
151
|
+
* @returns {{unitCount, units, incomplete}}
|
|
108
152
|
*/
|
|
109
153
|
async function runBaseline(projectRoot, opts = {}) {
|
|
110
154
|
const root = projectRoot || process.cwd();
|
|
@@ -115,11 +159,38 @@ async function runBaseline(projectRoot, opts = {}) {
|
|
|
115
159
|
}
|
|
116
160
|
// Refresh the global manifest so drift detection has a current snapshot.
|
|
117
161
|
await gtlManifest.writeManifest(root, gtlManifest.buildManifest(results));
|
|
118
|
-
return {
|
|
162
|
+
return {
|
|
163
|
+
unitCount: units.length,
|
|
164
|
+
units: results,
|
|
165
|
+
incomplete: collectIncomplete(results),
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Scan committed baselines for incomplete (unavailable/errored) linter
|
|
171
|
+
* sections. Used by the hooks installer to refuse to gate commits against a
|
|
172
|
+
* baseline that never captured a linter.
|
|
173
|
+
* @returns {Promise<{app, linter, status}[]>}
|
|
174
|
+
*/
|
|
175
|
+
async function findIncompleteBaselines(projectRoot) {
|
|
176
|
+
const root = projectRoot || process.cwd();
|
|
177
|
+
const out = [];
|
|
178
|
+
for (const unit of resolveUnits(root)) {
|
|
179
|
+
const baseline = await baselineStore.readBaseline(unit.baselinePath);
|
|
180
|
+
if (!baseline || !baseline.linters) continue;
|
|
181
|
+
for (const [linterId, section] of Object.entries(baseline.linters)) {
|
|
182
|
+
if (baselineStore.isIncompleteStatus(section && section.status)) {
|
|
183
|
+
out.push({ app: unit.appPath, linter: linterId, status: section.status });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return out;
|
|
119
188
|
}
|
|
120
189
|
|
|
121
190
|
module.exports = {
|
|
122
191
|
runBaseline,
|
|
123
192
|
baselineUnit,
|
|
124
193
|
configHashFor,
|
|
194
|
+
collectIncomplete,
|
|
195
|
+
findIncompleteBaselines,
|
|
125
196
|
};
|
package/lib/check.js
CHANGED
|
@@ -74,6 +74,21 @@ async function checkUnit(projectRoot, unit, adapter, opts) {
|
|
|
74
74
|
|
|
75
75
|
const baseline = await baselineStore.readBaseline(unit.baselinePath);
|
|
76
76
|
const section = baselineStore.getLinterSection(baseline, adapter.id);
|
|
77
|
+
|
|
78
|
+
// An incomplete baseline section (the linter was unavailable or errored when
|
|
79
|
+
// the baseline was captured) is not a real baseline — diffing against its
|
|
80
|
+
// empty fingerprint map would flag every pre-existing violation as new and
|
|
81
|
+
// block the commit. Surface it as a warning to re-baseline instead.
|
|
82
|
+
if (section && baselineStore.isIncompleteStatus(section.status)) {
|
|
83
|
+
return {
|
|
84
|
+
...base,
|
|
85
|
+
status: 'needs-baseline',
|
|
86
|
+
reason:
|
|
87
|
+
`baseline for ${adapter.id} is incomplete (was "${section.status}" ` +
|
|
88
|
+
'when captured) — run: gimme-the-lint baseline',
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
77
92
|
const result = diffEngine.diff(violations, section);
|
|
78
93
|
|
|
79
94
|
return {
|
|
@@ -99,7 +114,10 @@ async function runCheck(projectRoot, opts = {}) {
|
|
|
99
114
|
for (const linterId of unit.linters) {
|
|
100
115
|
let adapter;
|
|
101
116
|
try {
|
|
102
|
-
adapter = adapters.getAdapter(linterId, {
|
|
117
|
+
adapter = adapters.getAdapter(linterId, {
|
|
118
|
+
projectRoot: root,
|
|
119
|
+
appRoot: unit.root,
|
|
120
|
+
});
|
|
103
121
|
} catch (err) {
|
|
104
122
|
results.push({
|
|
105
123
|
unit: unit.id,
|
package/lib/config-manager.js
CHANGED
|
@@ -104,10 +104,50 @@ module.exports = ${JSON.stringify(config, null, 2)};
|
|
|
104
104
|
return { created: true, path: configPath, projectType };
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Write an explicit `apps` map into gimme-the-lint.config.js so the discovered
|
|
109
|
+
* app/linter layout is visible and editable instead of silently re-guessed on
|
|
110
|
+
* every run. Never overwrites an existing config.
|
|
111
|
+
* @param {string} projectRoot
|
|
112
|
+
* @param {{appPath: string, linters: string[]}[]} apps
|
|
113
|
+
*/
|
|
114
|
+
async function writeAppsConfig(projectRoot, apps) {
|
|
115
|
+
const cjsPath = path.join(projectRoot, 'gimme-the-lint.config.cjs');
|
|
116
|
+
const jsPath = path.join(projectRoot, 'gimme-the-lint.config.js');
|
|
117
|
+
if ((await fs.pathExists(cjsPath)) || (await fs.pathExists(jsPath))) {
|
|
118
|
+
return {
|
|
119
|
+
created: false,
|
|
120
|
+
path: (await fs.pathExists(cjsPath)) ? cjsPath : jsPath,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
let isESM = false;
|
|
125
|
+
const pkgPath = path.join(projectRoot, 'package.json');
|
|
126
|
+
if (await fs.pathExists(pkgPath)) {
|
|
127
|
+
try {
|
|
128
|
+
isESM = JSON.parse(await fs.readFile(pkgPath, 'utf8')).type === 'module';
|
|
129
|
+
} catch {}
|
|
130
|
+
}
|
|
131
|
+
const configPath = isESM ? cjsPath : jsPath;
|
|
132
|
+
|
|
133
|
+
const appsObj = {};
|
|
134
|
+
for (const app of apps || []) {
|
|
135
|
+
appsObj[app.appPath] = { linters: app.linters };
|
|
136
|
+
}
|
|
137
|
+
const content = `// gimme-the-lint configuration
|
|
138
|
+
// Generated by gimme-the-lint migrate — explicit app/linter bindings.
|
|
139
|
+
// Edit this map to correct any mis-discovered apps or linters.
|
|
140
|
+
module.exports = ${JSON.stringify({ apps: appsObj }, null, 2)};
|
|
141
|
+
`;
|
|
142
|
+
await fs.writeFile(configPath, content, 'utf8');
|
|
143
|
+
return { created: true, path: configPath, apps: appsObj };
|
|
144
|
+
}
|
|
145
|
+
|
|
107
146
|
module.exports = {
|
|
108
147
|
copyTemplate,
|
|
109
148
|
detectProjectType,
|
|
110
149
|
getConfig,
|
|
111
150
|
initConfig,
|
|
151
|
+
writeAppsConfig,
|
|
112
152
|
TEMPLATES_DIR,
|
|
113
153
|
};
|
package/lib/drift.js
CHANGED
|
@@ -47,7 +47,10 @@ async function detectDrift(projectRoot) {
|
|
|
47
47
|
for (const linterId of app.linters) {
|
|
48
48
|
let adapter;
|
|
49
49
|
try {
|
|
50
|
-
adapter = adapters.getAdapter(linterId, {
|
|
50
|
+
adapter = adapters.getAdapter(linterId, {
|
|
51
|
+
projectRoot: root,
|
|
52
|
+
appRoot: path.resolve(root, app.appPath),
|
|
53
|
+
});
|
|
51
54
|
} catch {
|
|
52
55
|
continue;
|
|
53
56
|
}
|
package/lib/migrate.js
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
const fs = require('fs-extra');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const { runBaseline } = require('./baseline');
|
|
6
|
+
const projectModel = require('./project-model');
|
|
7
|
+
const configManager = require('./config-manager');
|
|
6
8
|
|
|
7
9
|
// Migration from the v1 layout (.lttf/ + .lttf-ruff/ per-directory baselines)
|
|
8
10
|
// to the v2 .gtl/ layout. v1 baseline files hold per-directory violation data
|
|
@@ -89,6 +91,12 @@ async function migrate(projectRoot, opts = {}) {
|
|
|
89
91
|
backedUp.push(rel);
|
|
90
92
|
}
|
|
91
93
|
|
|
94
|
+
// Pin the discovered app/linter layout into gimme-the-lint.config.js so the
|
|
95
|
+
// guess is visible and editable, not silently re-derived on every run.
|
|
96
|
+
const discoveredApps = projectModel.discoverApps(root);
|
|
97
|
+
const appsConfig = await configManager.writeAppsConfig(root, discoveredApps);
|
|
98
|
+
const discoveryWarnings = projectModel.discoveryWarnings(root);
|
|
99
|
+
|
|
92
100
|
// Re-baseline from the current code into the v2 .gtl/ layout.
|
|
93
101
|
const baseline = await runBaseline(root, { strict: opts.strict });
|
|
94
102
|
|
|
@@ -96,7 +104,13 @@ async function migrate(projectRoot, opts = {}) {
|
|
|
96
104
|
migrated: true,
|
|
97
105
|
backedUp,
|
|
98
106
|
backupPath: path.relative(root, backupRoot),
|
|
107
|
+
discoveredApps,
|
|
108
|
+
appsConfig,
|
|
109
|
+
discoveryWarnings,
|
|
99
110
|
baseline,
|
|
111
|
+
// Incomplete linter sections (unavailable / errored) — a baseline in this
|
|
112
|
+
// state never captured those linters and must not silently gate commits.
|
|
113
|
+
incomplete: baseline.incomplete || [],
|
|
100
114
|
};
|
|
101
115
|
}
|
|
102
116
|
|
package/lib/project-model.js
CHANGED
|
@@ -11,14 +11,19 @@ const configManager = require('./config-manager');
|
|
|
11
11
|
// monorepo — apps/orders-api (TS), apps/orders-worker (Py), apps/billing
|
|
12
12
|
// (Go), apps/audit (Rust) — discover cleanly with zero config.
|
|
13
13
|
|
|
14
|
-
// Manifest filename → linter id.
|
|
14
|
+
// Manifest filename → linter id. These must be STRONG markers — a file whose
|
|
15
|
+
// presence reliably means "a real, separately-configured app lives here".
|
|
16
|
+
// `requirements.txt` is deliberately excluded: it is too weak (it shows up in
|
|
17
|
+
// load-test dirs, docs tooling, sub-utilities) and binding ruff to every dir
|
|
18
|
+
// that has one mis-binds nested noise and hides the real app.
|
|
15
19
|
const MANIFEST_LINTERS = {
|
|
16
20
|
'package.json': 'eslint',
|
|
17
21
|
'biome.json': 'biome',
|
|
18
22
|
'biome.jsonc': 'biome',
|
|
19
23
|
'pyproject.toml': 'ruff',
|
|
24
|
+
'ruff.toml': 'ruff',
|
|
25
|
+
'.ruff.toml': 'ruff',
|
|
20
26
|
'setup.py': 'ruff',
|
|
21
|
-
'requirements.txt': 'ruff',
|
|
22
27
|
'go.mod': 'golangci-lint',
|
|
23
28
|
'Cargo.toml': 'clippy',
|
|
24
29
|
'ansible.cfg': 'ansible-lint',
|
|
@@ -104,12 +109,15 @@ function findManifestDirs(projectRoot) {
|
|
|
104
109
|
}
|
|
105
110
|
|
|
106
111
|
/**
|
|
107
|
-
* Discover the apps in a project.
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
112
|
+
* Discover the apps in a project, binding each to its linters.
|
|
113
|
+
*
|
|
114
|
+
* Workspace-root detection is PER LINTER: a manifest directory is an app for
|
|
115
|
+
* linter L unless another manifest directory NESTED below it also carries L
|
|
116
|
+
* (then the outer dir is a workspace root for L, and the nested dirs are the
|
|
117
|
+
* apps). This is what lets a repo-root `pyproject.toml` (ruff) be a real
|
|
118
|
+
* Python app even when nested `package.json` dirs (eslint) sit below it — the
|
|
119
|
+
* old "any nested manifest ⇒ skip the whole dir" rule discarded the root
|
|
120
|
+
* config and bound nothing for Python.
|
|
113
121
|
* @returns {{appPath: string, linters: string[]}[]}
|
|
114
122
|
*/
|
|
115
123
|
function discoverApps(projectRoot, opts = {}) {
|
|
@@ -121,35 +129,66 @@ function discoverApps(projectRoot, opts = {}) {
|
|
|
121
129
|
];
|
|
122
130
|
|
|
123
131
|
const manifestDirs = findManifestDirs(root);
|
|
124
|
-
const allDirs = manifestDirs.map((m) => m.dir);
|
|
125
132
|
|
|
126
|
-
|
|
133
|
+
// dir → Set<linter>, accumulated via the per-linter workspace-root rule.
|
|
134
|
+
const appLinters = new Map();
|
|
127
135
|
for (const entry of manifestDirs) {
|
|
128
|
-
const childPrefix = entry.dir === '.' ? '' : `${entry.dir}/`;
|
|
129
|
-
const isWorkspaceRoot = allDirs.some(
|
|
130
|
-
(other) => other !== entry.dir && other.startsWith(childPrefix)
|
|
131
|
-
);
|
|
132
|
-
if (isWorkspaceRoot) continue;
|
|
133
|
-
|
|
134
136
|
// Skip template/scaffold directories (match any path segment).
|
|
135
137
|
const segments = entry.dir === '.' ? [] : entry.dir.split('/');
|
|
136
138
|
if (segments.some((segment) => matchesAny(segment, skipPatterns))) continue;
|
|
137
139
|
|
|
140
|
+
const childPrefix = entry.dir === '.' ? '' : `${entry.dir}/`;
|
|
141
|
+
for (const linter of entry.linters) {
|
|
142
|
+
const isWorkspaceRootForLinter = manifestDirs.some(
|
|
143
|
+
(other) =>
|
|
144
|
+
other.dir !== entry.dir &&
|
|
145
|
+
other.dir.startsWith(childPrefix) &&
|
|
146
|
+
other.linters.has(linter)
|
|
147
|
+
);
|
|
148
|
+
if (isWorkspaceRootForLinter) continue; // nested dirs are the apps for L
|
|
149
|
+
if (!appLinters.has(entry.dir)) appLinters.set(entry.dir, new Set());
|
|
150
|
+
appLinters.get(entry.dir).add(linter);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const apps = [];
|
|
155
|
+
for (const [dir, linterSet] of appLinters) {
|
|
138
156
|
// A biome.json is an explicit choice of Biome over ESLint for JS/TS —
|
|
139
157
|
// honor it by dropping the default ESLint binding for that app.
|
|
140
|
-
let linters = [...
|
|
158
|
+
let linters = [...linterSet];
|
|
141
159
|
if (linters.includes('biome') && linters.includes('eslint')) {
|
|
142
160
|
linters = linters.filter((l) => l !== 'eslint');
|
|
143
161
|
}
|
|
144
|
-
|
|
145
|
-
apps.push({ appPath: entry.dir, linters: linters.sort() });
|
|
162
|
+
if (linters.length) apps.push({ appPath: dir, linters: linters.sort() });
|
|
146
163
|
}
|
|
147
164
|
|
|
148
165
|
return apps.sort((a, b) => a.appPath.localeCompare(b.appPath));
|
|
149
166
|
}
|
|
150
167
|
|
|
168
|
+
/**
|
|
169
|
+
* Discovery warnings — surfaced by `migrate` / `baseline` so a guessed app
|
|
170
|
+
* layout is visible rather than silent. The ambiguous case is a repo-root
|
|
171
|
+
* linter config AND nested apps: discovery has to guess the boundary, so an
|
|
172
|
+
* explicit `apps` map should pin it.
|
|
173
|
+
* @returns {string[]}
|
|
174
|
+
*/
|
|
175
|
+
function discoveryWarnings(projectRoot, opts = {}) {
|
|
176
|
+
const apps = discoverApps(projectRoot, opts);
|
|
177
|
+
const warnings = [];
|
|
178
|
+
const hasRootApp = apps.some((a) => a.appPath === '.');
|
|
179
|
+
if (hasRootApp && apps.length > 1) {
|
|
180
|
+
warnings.push(
|
|
181
|
+
'Discovery found a repo-root linter config AND nested app(s); the app ' +
|
|
182
|
+
'boundaries are a guess. Pin them with an explicit `apps` map in ' +
|
|
183
|
+
'gimme-the-lint.config.js (migrate writes one for you).'
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
return warnings;
|
|
187
|
+
}
|
|
188
|
+
|
|
151
189
|
module.exports = {
|
|
152
190
|
discoverApps,
|
|
191
|
+
discoveryWarnings,
|
|
153
192
|
findManifestDirs,
|
|
154
193
|
globToRegExp,
|
|
155
194
|
MANIFEST_LINTERS,
|
package/lib/report.js
CHANGED
|
@@ -52,6 +52,10 @@ function formatCheckReport(report) {
|
|
|
52
52
|
lines.push(chalk.dim(`· ${label} — no staged changes`));
|
|
53
53
|
} else if (u.status === 'error') {
|
|
54
54
|
lines.push(`${chalk.red('✗')} ${label} ${chalk.red(`— ${u.reason}`)}`);
|
|
55
|
+
} else if (u.status === 'needs-baseline') {
|
|
56
|
+
lines.push(
|
|
57
|
+
`${chalk.yellow('⚠ NEEDS BASELINE')} ${label} ${chalk.yellow(`— ${u.reason}`)}`
|
|
58
|
+
);
|
|
55
59
|
}
|
|
56
60
|
// 'no-code' is intentionally silent.
|
|
57
61
|
if ((u.status === 'pass' || u.status === 'fail') && u.hasBaseline === false) {
|
|
@@ -91,10 +95,18 @@ function formatBaselineReport(report, projectRoot) {
|
|
|
91
95
|
lines.push(chalk.cyan(` ${u.appPath}`));
|
|
92
96
|
for (const s of u.sections) {
|
|
93
97
|
if (s.status === 'no-code') continue;
|
|
94
|
-
if (s.status === '
|
|
95
|
-
lines.push(
|
|
98
|
+
if (s.status === 'unavailable') {
|
|
99
|
+
lines.push(
|
|
100
|
+
` ${chalk.red('⚠ UNAVAILABLE')} ${s.linter} — ${s.reason} ` +
|
|
101
|
+
chalk.red('(NOT baselined)')
|
|
102
|
+
);
|
|
96
103
|
} else if (s.status === 'error') {
|
|
97
|
-
lines.push(
|
|
104
|
+
lines.push(
|
|
105
|
+
` ${chalk.red('✗ ERROR')} ${s.linter} — ${s.reason} ` +
|
|
106
|
+
chalk.red('(NOT baselined)')
|
|
107
|
+
);
|
|
108
|
+
} else if (s.status === 'skipped') {
|
|
109
|
+
lines.push(` ${chalk.yellow('⚠ SKIPPED')} ${s.linter} — ${s.reason}`);
|
|
98
110
|
} else {
|
|
99
111
|
lines.push(
|
|
100
112
|
` ${chalk.green('✓')} ${s.linter} — ${s.total} violation(s) ` +
|
|
@@ -104,6 +116,28 @@ function formatBaselineReport(report, projectRoot) {
|
|
|
104
116
|
}
|
|
105
117
|
lines.push(chalk.dim(` → ${path.relative(root, u.baselinePath)}`));
|
|
106
118
|
}
|
|
119
|
+
|
|
120
|
+
// Prominent incomplete summary — a baseline missing linters must never look
|
|
121
|
+
// like a success.
|
|
122
|
+
if (report.incomplete && report.incomplete.length) {
|
|
123
|
+
lines.push('');
|
|
124
|
+
lines.push(
|
|
125
|
+
chalk.red(`✗ ${report.incomplete.length} linter(s) were NOT baselined:`)
|
|
126
|
+
);
|
|
127
|
+
for (const i of report.incomplete) {
|
|
128
|
+
lines.push(chalk.red(` ${i.app} · ${i.linter} (${i.status})`));
|
|
129
|
+
}
|
|
130
|
+
lines.push(
|
|
131
|
+
chalk.yellow(
|
|
132
|
+
' This baseline is INCOMPLETE. Install the missing linters and re-run'
|
|
133
|
+
)
|
|
134
|
+
);
|
|
135
|
+
lines.push(
|
|
136
|
+
chalk.yellow(
|
|
137
|
+
' `gimme-the-lint baseline` before relying on it to gate commits.'
|
|
138
|
+
)
|
|
139
|
+
);
|
|
140
|
+
}
|
|
107
141
|
return lines.join('\n');
|
|
108
142
|
}
|
|
109
143
|
|
package/lib/toolchain.js
CHANGED
|
@@ -51,7 +51,10 @@ function findGaps(projectRoot) {
|
|
|
51
51
|
for (const linterId of unit.linters) {
|
|
52
52
|
let adapter;
|
|
53
53
|
try {
|
|
54
|
-
adapter = adapters.getAdapter(linterId, {
|
|
54
|
+
adapter = adapters.getAdapter(linterId, {
|
|
55
|
+
projectRoot: root,
|
|
56
|
+
appRoot: unit.root,
|
|
57
|
+
});
|
|
55
58
|
} catch {
|
|
56
59
|
continue;
|
|
57
60
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@theglitchking/gimme-the-lint",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"description": "Polyglot progressive linting for monorepos — adapter-driven baselines, per-app drift detection, and idempotent skips across JavaScript/TypeScript, Python, Go, Rust, Terraform, and Ansible.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"linting",
|