fix(#551): lint hand-written bin/lib/*.cjs mislabeled as generated (#555)

* fix(#551): lint hand-written bin/lib/*.cjs mislabeled as generated

GENERATED_CJS_IGNORES excluded 12 hand-written runtime modules from the
ADR-452 ESLint harness. None carry an @generated header and no generator
emits them — they are hand-written, not generated. Remove the mislabeled
list so they lint like the rest of get-shit-done/bin/**/*.cjs. The genuinely
tsc-generated semver-compare.cjs keeps its own separate ADR-457 ignore.

Also drop the stale "Type-aware via parserOptions.project=tsconfig.lint.json"
comment on the .cjs block: that block sets no parser/project and enables no
@typescript-eslint rules; type-aware linting lives in the src/**/*.cts block.

Lint stays green (0 errors); 2 pre-existing warnings surface (already warn
severity) per the file's warn-first convention, tracked as follow-up cleanup.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(#551): guard ESLint coverage of hand-written bin/lib/*.cjs

Asserts via ESLint's isPathIgnored API that every get-shit-done/bin/lib/*.cjs
without an @generated header or a src/<name>.cts|.ts source is linted (not
ignored), and that the genuinely tsc-generated semver-compare.cjs stays
ignored (ADR-457). Fails 13/14 against the pre-fix config; passes on the fix.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-05-31 20:04:03 -04:00
committed by GitHub
parent de2f73d21a
commit 01f894078d
2 changed files with 94 additions and 18 deletions

View File

@@ -22,22 +22,6 @@ const localPlugin = {
},
};
// Generated bin/lib files — never lint
const GENERATED_CJS_IGNORES = [
'get-shit-done/bin/lib/command-aliases.cjs',
'get-shit-done/bin/lib/configuration.cjs',
'get-shit-done/bin/lib/decisions.cjs',
'get-shit-done/bin/lib/phase-lifecycle.cjs',
'get-shit-done/bin/lib/plan-scan.cjs',
'get-shit-done/bin/lib/project-root.cjs',
'get-shit-done/bin/lib/schema-detect.cjs',
'get-shit-done/bin/lib/secrets.cjs',
'get-shit-done/bin/lib/state-document.cjs',
'get-shit-done/bin/lib/validate.cjs',
'get-shit-done/bin/lib/workstream-inventory-builder.cjs',
'get-shit-done/bin/lib/workstream-name-policy.cjs',
];
const sdkSrcExists = existsSync(join(__dirname, 'sdk', 'src'));
export default tseslint.config(
@@ -53,7 +37,6 @@ export default tseslint.config(
'**/*.generated.cjs',
// ADR-457: tsc-generated runtime artifact — lint the src/*.cts source, not the emitted .cjs.
'get-shit-done/bin/lib/semver-compare.cjs',
...GENERATED_CJS_IGNORES,
],
},
@@ -77,7 +60,6 @@ export default tseslint.config(
// ── get-shit-done/bin/**/*.cjs + scripts/**/*.cjs ───────────────────────────
// CommonJS Node files: js.recommended + eslint-plugin-n + local plugin rules
// Type-aware via parserOptions.project=tsconfig.lint.json where applicable
{
files: ['get-shit-done/bin/**/*.cjs', 'scripts/**/*.cjs'],
plugins: {

View File

@@ -0,0 +1,94 @@
'use strict';
/**
* Regression test for #551 — the ESLint harness (ADR-452) silently excluded 12
* hand-written `get-shit-done/bin/lib/*.cjs` modules via a `GENERATED_CJS_IGNORES`
* list mislabeled "Generated bin/lib files — never lint". The files are hand-written
* (no `@generated` header, no generator emits them), so they belong in the harness.
*
* Invariant under test (not just today's file list): a `bin/lib/*.cjs` module must
* be linted UNLESS it is genuinely generated — i.e. it carries an `@generated`
* header or has a `src/<name>.cts|.ts` source it is compiled from (ADR-457). This
* catches the next mislabeled file, not only the original 12.
*
* Behavior is checked through ESLint's own `isPathIgnored` API so the test reflects
* real resolved flat-config precedence, not a textual scan of eslint.config.mjs.
*/
const { describe, test, before } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const { ESLint } = require('eslint');
const ROOT = path.resolve(__dirname, '..');
const LIB_DIR = path.join(ROOT, 'get-shit-done', 'bin', 'lib');
// The 12 modules that #551 restored to lint coverage.
const HAND_WRITTEN = [
'command-aliases',
'configuration',
'decisions',
'phase-lifecycle',
'plan-scan',
'project-root',
'schema-detect',
'secrets',
'state-document',
'validate',
'workstream-inventory-builder',
'workstream-name-policy',
].map((name) => path.join(LIB_DIR, `${name}.cjs`));
function isGenerated(absPath) {
if (!fs.existsSync(absPath)) return false;
const head = fs.readFileSync(absPath, 'utf8').slice(0, 500);
if (/@generated/.test(head)) return true;
const base = path.basename(absPath, '.cjs');
return (
fs.existsSync(path.join(ROOT, 'src', `${base}.cts`)) ||
fs.existsSync(path.join(ROOT, 'src', `${base}.ts`))
);
}
let eslint;
before(() => {
eslint = new ESLint({ cwd: ROOT });
});
describe('#551: ESLint covers hand-written bin/lib/*.cjs', () => {
for (const file of HAND_WRITTEN) {
test(`lints ${path.basename(file)} (not ignored)`, async () => {
assert.equal(
await eslint.isPathIgnored(file),
false,
`${path.relative(ROOT, file)} is hand-written and must be linted, not ignored`,
);
});
}
test('no hand-written bin/lib/*.cjs is silently ignored', async () => {
const offenders = [];
for (const entry of fs.readdirSync(LIB_DIR)) {
if (!entry.endsWith('.cjs')) continue;
const abs = path.join(LIB_DIR, entry);
if (isGenerated(abs)) continue; // legitimately excluded from the harness
if (await eslint.isPathIgnored(abs)) offenders.push(entry);
}
assert.deepEqual(
offenders,
[],
`Hand-written modules silently excluded from ESLint: ${offenders.join(', ')}`,
);
});
test('genuinely tsc-generated semver-compare.cjs stays ignored (ADR-457)', async () => {
// Publish-time artifact compiled from src/semver-compare.cts; must not be linted.
const f = path.join(LIB_DIR, 'semver-compare.cjs');
assert.equal(
await eslint.isPathIgnored(f),
true,
'semver-compare.cjs is a tsc-generated publish-time artifact and must stay ignored',
);
});
});