chore(#556): retire orphaned CJS↔SDK hand-sync tooling (#559)

The @opengsd/gsd-sdk package boundary was retired (ADR-0174, #191/#192) and
the sdk/ tree is no longer tracked. ADR-0174's Supersedes table explicitly
records that the generator-based Shared-Module hand-sync lint is deleted as
part of that collapse. This removes the now-orphaned machinery it left behind:

- scripts/lint-shared-module-handsync.cjs — paired bin/lib/*.cjs files with
  sdk/src/**/*.ts sources that no longer exist; wired into no CI workflow or
  npm script (dead).
- scripts/shared-module-handsync-allowlist.json — the lint's allowlist; every
  entry pointed at a non-existent sdk/src source / generated artifact /
  freshness check.
- tests/lint-shared-module-handsync.test.cjs — tested the deleted lint.

Docs corrected to match:
- CONTRIBUTING.md — removed the "CJS↔SDK seam" instruction (it linked the
  already-deleted docs/agents/cjs-sdk-seam.md and told contributors to
  maintain the allowlist under a retired generator pattern).
- docs/prd/3524-cjs-sdk-hard-seam.md + docs/prd/README.md — marked the PRD
  Superseded by ADR-0174, matching the already-superseded ADR-3524.

Added tests/no-cjs-sdk-handsync-tooling.test.cjs as a regression guard so the
retired tooling stays removed and is not silently re-wired into package.json.

ADR-3524 is left in place (already Superseded by ADR-0174); runtime modules and
regression tests that cite it in comments keep resolving. No user-facing change.

Closes #556

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-05-31 20:28:57 -04:00
committed by GitHub
parent 84e9888d25
commit 9b5ee37364
7 changed files with 55 additions and 992 deletions

View File

@@ -1,417 +0,0 @@
'use strict';
/**
* Tests for scripts/lint-shared-module-handsync.cjs — Phase 6 of #3524 (#3575).
*
* Three cases:
* 1. No new drift pair: lint exits 0 on the current repo tree (all cooperating
* siblings on the allowlist; migrateMeBacklog pairs do not fail).
* 2. Intentional new drift: synthesize a fixture tree with an unlisted
* foo-test.cjs / foo-test.ts pair, assert exit 1 + typed error JSON.
* 3. Allowlist entry honored: same pair as case 2, but with a cooperatingSiblings
* allowlist entry present, assert exit 0.
*
* Assertions use the lint's --json mode: the production code emits a typed IR
* (ok / reason / errors / warnings / counts), and tests parse and assert on
* structured fields rather than substring-matching stderr/stdout (per
* CONTRIBUTING.md "Prohibited: Raw Text Matching on Test Outputs").
*/
const { test, describe } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const os = require('node:os');
const { spawnSync } = require('node:child_process');
const LINT_SCRIPT = path.join(__dirname, '..', 'scripts', 'lint-shared-module-handsync.cjs');
const ALLOWLIST_PATH = path.join(__dirname, '..', 'scripts', 'shared-module-handsync-allowlist.json');
const REPO_ROOT = path.join(__dirname, '..');
// ---------------------------------------------------------------------------
// Helper: run the lint script in --json mode and parse the result.
// Returns { status, payload } where payload is the parsed JSON IR (or null
// if the lint emitted no JSON, which would be a test-infrastructure bug).
// ---------------------------------------------------------------------------
function runLintJson(extraArgs = []) {
const result = spawnSync(process.execPath, [LINT_SCRIPT, '--json', ...extraArgs], {
encoding: 'utf8',
cwd: REPO_ROOT,
});
let payload = null;
try {
payload = JSON.parse(result.stdout.trim());
} catch {
// Leave payload as null; tests assert on payload presence.
}
return { status: result.status, payload };
}
// ---------------------------------------------------------------------------
// Helper: create an isolated fixture tree for testing
//
// Layout:
// <tmpDir>/
// get-shit-done/bin/lib/<cjsName>.cjs
// sdk/src/query/<tsName>.ts (if tsInQuery === true)
// sdk/src/<tsName>.ts (if tsInQuery === false)
// scripts/shared-module-handsync-allowlist.json (custom allowlist)
// ---------------------------------------------------------------------------
function createFixture({ cjsName, tsName, tsInQuery = true, allowlistExtra = {} }) {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-lint-handsync-'));
const cjsDir = path.join(tmpDir, 'get-shit-done', 'bin', 'lib');
fs.mkdirSync(cjsDir, { recursive: true });
const tsDir = tsInQuery
? path.join(tmpDir, 'sdk', 'src', 'query')
: path.join(tmpDir, 'sdk', 'src');
fs.mkdirSync(tsDir, { recursive: true });
const scriptsDir = path.join(tmpDir, 'scripts');
fs.mkdirSync(scriptsDir, { recursive: true });
fs.writeFileSync(path.join(cjsDir, `${cjsName}.cjs`), `'use strict';\n// fixture cjs\n`);
fs.writeFileSync(path.join(tsDir, `${tsName}.ts`), `// fixture ts\nexport {};\n`);
const realAllowlist = JSON.parse(fs.readFileSync(ALLOWLIST_PATH, 'utf8'));
const fixtureAllowlist = {
cooperatingSiblings: [
...(realAllowlist.cooperatingSiblings || []),
...(allowlistExtra.cooperatingSiblings || []),
],
migrateMeBacklog: [
...(realAllowlist.migrateMeBacklog || []),
...(allowlistExtra.migrateMeBacklog || []),
],
};
fs.writeFileSync(
path.join(scriptsDir, 'shared-module-handsync-allowlist.json'),
JSON.stringify(fixtureAllowlist, null, 2)
);
return tmpDir;
}
function cleanupFixture(dir) {
fs.rmSync(dir, { recursive: true, force: true });
}
// ---------------------------------------------------------------------------
// Case 1: No new drift pair — exits 0 on current repo tree
// ---------------------------------------------------------------------------
describe('lint-shared-module-handsync: current repo tree', () => {
test('exits 0 with the real allowlist and current repo tree', () => {
const { status, payload } = runLintJson();
assert.strictEqual(status, 0);
assert.ok(payload, 'expected JSON payload on stdout');
assert.strictEqual(payload.ok, true);
assert.ok(
payload.reason === undefined || payload.reason === 'sdk_retired',
`unexpected success reason: ${payload.reason}`
);
});
test('reports numeric counts when lint runs or short-circuits on retired SDK tree', () => {
const { payload } = runLintJson();
assert.ok(payload);
assert.strictEqual(typeof payload.cooperatingCount, 'number');
assert.strictEqual(typeof payload.backlogCount, 'number');
if (payload.reason !== 'sdk_retired') {
assert.ok(payload.cooperatingCount > 0, 'expected at least one cooperating sibling');
}
// No errors field on success — only warnings (backlog) may be present
assert.strictEqual(payload.ok, true);
});
test('script has no syntax errors', () => {
const result = spawnSync(process.execPath, ['--check', LINT_SCRIPT], { encoding: 'utf8' });
assert.strictEqual(result.status, 0);
});
});
// ---------------------------------------------------------------------------
// Case 2: Intentional new drift — exits 1 with informative typed error
// ---------------------------------------------------------------------------
describe('lint-shared-module-handsync: intentional new drift pair', () => {
test('exits 1 when an unlisted cjs/ts pair exists', () => {
const tmpDir = createFixture({ cjsName: 'foo-test', tsName: 'foo-test', tsInQuery: true });
try {
const { status, payload } = runLintJson(['--root', tmpDir]);
assert.strictEqual(status, 1);
assert.ok(payload);
assert.strictEqual(payload.ok, false);
assert.strictEqual(payload.reason, 'unauthorized_pairs');
} finally {
cleanupFixture(tmpDir);
}
});
test('typed error payload names the unauthorized pair', () => {
const tmpDir = createFixture({ cjsName: 'foo-test', tsName: 'foo-test', tsInQuery: true });
try {
const { payload } = runLintJson(['--root', tmpDir]);
assert.ok(payload && Array.isArray(payload.errors));
assert.strictEqual(payload.errors.length, 1);
const [entry] = payload.errors;
assert.match(entry.relCjs, /foo-test\.cjs$/);
assert.ok(Array.isArray(entry.tsPaths));
assert.ok(entry.tsPaths.some((p) => /foo-test\.ts$/.test(p)));
} finally {
cleanupFixture(tmpDir);
}
});
test('exits 1 for unlisted pair in sdk/src/<name>.ts (non-query) position', () => {
const tmpDir = createFixture({ cjsName: 'bar-test', tsName: 'bar-test', tsInQuery: false });
try {
const { status, payload } = runLintJson(['--root', tmpDir]);
assert.strictEqual(status, 1);
assert.ok(payload);
assert.strictEqual(payload.ok, false);
assert.strictEqual(payload.reason, 'unauthorized_pairs');
} finally {
cleanupFixture(tmpDir);
}
});
});
// ---------------------------------------------------------------------------
// Case 3: Allowlist entry honored — exits 0 when pair IS on cooperatingSiblings
// ---------------------------------------------------------------------------
describe('lint-shared-module-handsync: allowlist entry honored', () => {
test('exits 0 when pair is in cooperatingSiblings allowlist', () => {
const cjsName = 'baz-cooperating';
const tsName = 'baz-cooperating';
const tmpDir = createFixture({
cjsName,
tsName,
tsInQuery: true,
allowlistExtra: {
cooperatingSiblings: [
{
cjs: `get-shit-done/bin/lib/${cjsName}.cjs`,
ts: `sdk/src/query/${tsName}.ts`,
classification: 'cooperating-sibling',
justification: 'Test fixture: synthetic cooperating sibling for lint test.',
},
],
},
});
try {
const { status, payload } = runLintJson(['--root', tmpDir]);
assert.strictEqual(status, 0);
assert.ok(payload);
assert.strictEqual(payload.ok, true);
} finally {
cleanupFixture(tmpDir);
}
});
// Regression guard: the lint matches on the (cjs, ts) PAIR, not on the
// cjs path alone. An allowlist entry whose ts points to a different path
// than the actual ts sibling on disk must NOT silently pass the pair.
test('rejects pair when TS path differs from allowlist entry', () => {
const cjsName = 'foo-wrong-ts';
const tsName = 'foo-wrong-ts';
const tmpDir = createFixture({
cjsName,
tsName,
tsInQuery: true, // creates sdk/src/query/foo-wrong-ts.ts on disk
allowlistExtra: {
cooperatingSiblings: [
{
cjs: `get-shit-done/bin/lib/${cjsName}.cjs`,
// Allowlist points at sdk/src/<name>.ts — different location.
// Lint must reject because the on-disk pair is unauthorized.
ts: `sdk/src/${tsName}.ts`,
classification: 'cooperating-sibling',
justification: 'Test: validates pair-aware matching enforces ts path.',
},
],
},
});
try {
const { status, payload } = runLintJson(['--root', tmpDir]);
assert.strictEqual(status, 1, 'must fail when ts path mismatches');
assert.ok(payload);
assert.strictEqual(payload.ok, false);
assert.strictEqual(payload.reason, 'unauthorized_pairs');
} finally {
cleanupFixture(tmpDir);
}
});
test('exits 0 (no error) when pair is in migrateMeBacklog allowlist', () => {
const cjsName = 'qux-backlog';
const tsName = 'qux-backlog';
const tmpDir = createFixture({
cjsName,
tsName,
tsInQuery: true,
allowlistExtra: {
migrateMeBacklog: [
{
cjs: `get-shit-done/bin/lib/${cjsName}.cjs`,
ts: `sdk/src/query/${tsName}.ts`,
classification: 'drift-anti-pattern',
justification: 'Test fixture: synthetic backlog pair for lint test.',
trackedIn: 'test only',
},
],
},
});
try {
const { status, payload } = runLintJson(['--root', tmpDir]);
assert.strictEqual(status, 0);
assert.ok(payload);
assert.strictEqual(payload.ok, true);
// The backlog pair should be reported in warnings (not errors)
assert.ok(Array.isArray(payload.warnings));
assert.ok(
payload.warnings.some((w) => /qux-backlog\.cjs$/.test(w.relCjs)),
'expected qux-backlog in warnings'
);
} finally {
cleanupFixture(tmpDir);
}
});
// Regression guard for #3632: when a cjs has TWO ts siblings sharing the
// same basename (e.g. sdk/src/foo.ts AND sdk/src/query/foo.ts) and only
// ONE pair is allowlisted, the unallowlisted sibling must still be reported.
// Prior bug: .some() at the cjs level returned true on the allowlisted
// pair, short-circuiting and silently dropping the unallowlisted sibling.
test('reports unallowlisted ts sibling when another ts sibling for the same cjs IS allowlisted (#3632)', () => {
const cjsName = 'multi-sibling';
const tsName = 'multi-sibling';
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-lint-multi-'));
try {
const cjsDir = path.join(tmpDir, 'get-shit-done', 'bin', 'lib');
fs.mkdirSync(cjsDir, { recursive: true });
const tsDirRoot = path.join(tmpDir, 'sdk', 'src');
const tsDirQuery = path.join(tmpDir, 'sdk', 'src', 'query');
fs.mkdirSync(tsDirQuery, { recursive: true });
const scriptsDir = path.join(tmpDir, 'scripts');
fs.mkdirSync(scriptsDir, { recursive: true });
// One cjs, two ts siblings on disk (same basename, different paths).
fs.writeFileSync(path.join(cjsDir, `${cjsName}.cjs`), `'use strict';\n`);
fs.writeFileSync(path.join(tsDirRoot, `${tsName}.ts`), `export {};\n`);
fs.writeFileSync(path.join(tsDirQuery, `${tsName}.ts`), `export {};\n`);
// Allowlist ONLY the sdk/src/<name>.ts pair. The sdk/src/query/<name>.ts
// sibling is intentionally NOT allowlisted and must be reported.
fs.writeFileSync(
path.join(scriptsDir, 'shared-module-handsync-allowlist.json'),
JSON.stringify(
{
cooperatingSiblings: [
{
cjs: `get-shit-done/bin/lib/${cjsName}.cjs`,
ts: `sdk/src/${tsName}.ts`,
classification: 'cooperating-sibling',
justification: 'Test: only the non-query sibling is allowlisted.',
},
],
migrateMeBacklog: [],
},
null,
2
)
);
const { status, payload } = runLintJson(['--root', tmpDir]);
assert.strictEqual(
status,
1,
'must fail: the sdk/src/query/<name>.ts sibling is not allowlisted'
);
assert.ok(payload);
assert.strictEqual(payload.ok, false);
assert.strictEqual(payload.reason, 'unauthorized_pairs');
assert.ok(Array.isArray(payload.errors) && payload.errors.length >= 1);
const reportedTs = payload.errors.flatMap((e) => e.tsPaths);
assert.ok(
reportedTs.some((p) => /sdk\/src\/query\/multi-sibling\.ts$/.test(p)),
`expected query sibling in errors, got: ${JSON.stringify(reportedTs)}`
);
// The allowlisted sibling must NOT appear in errors.
assert.ok(
!reportedTs.some((p) => /^sdk\/src\/multi-sibling\.ts$/.test(p)),
`allowlisted sibling sdk/src/multi-sibling.ts must not be flagged, got: ${JSON.stringify(reportedTs)}`
);
} finally {
cleanupFixture(tmpDir);
}
});
test('generated .cjs files are excluded from pair detection', () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-lint-gen-'));
try {
const cjsDir = path.join(tmpDir, 'get-shit-done', 'bin', 'lib');
fs.mkdirSync(cjsDir, { recursive: true });
const tsDir = path.join(tmpDir, 'sdk', 'src', 'query');
fs.mkdirSync(tsDir, { recursive: true });
const scriptsDir = path.join(tmpDir, 'scripts');
fs.mkdirSync(scriptsDir, { recursive: true });
// A .generated.cjs file + matching TS — should NOT trigger lint error
fs.writeFileSync(path.join(cjsDir, 'my-module.generated.cjs'), `'use strict';\n`);
fs.writeFileSync(path.join(tsDir, 'my-module.ts'), `export {};\n`);
fs.writeFileSync(
path.join(scriptsDir, 'shared-module-handsync-allowlist.json'),
JSON.stringify({ cooperatingSiblings: [], migrateMeBacklog: [] }, null, 2)
);
const { status, payload } = runLintJson(['--root', tmpDir]);
assert.strictEqual(status, 0);
assert.ok(payload);
assert.strictEqual(payload.ok, true);
} finally {
cleanupFixture(tmpDir);
}
});
});
describe('lint-shared-module-handsync: cross-name pair support', () => {
test('surfaces declared cross-name migrateMeBacklog pair in warnings', () => {
const cjsName = 'verify';
const tsName = 'validate';
const tmpDir = createFixture({
cjsName,
tsName,
tsInQuery: true,
allowlistExtra: {
migrateMeBacklog: [
{
cjs: `get-shit-done/bin/lib/${cjsName}.cjs`,
ts: `sdk/src/query/${tsName}.ts`,
classification: 'drift-anti-pattern',
justification: 'Test fixture: cross-name pair should be observable by lint.',
trackedIn: 'issue-11-test',
},
],
},
});
try {
const { status, payload } = runLintJson(['--root', tmpDir]);
assert.strictEqual(status, 0);
assert.ok(payload);
assert.strictEqual(payload.ok, true);
assert.ok(Array.isArray(payload.warnings));
assert.ok(
payload.warnings.some((w) =>
/verify\.cjs$/.test(w.relCjs) &&
Array.isArray(w.tsPaths) &&
w.tsPaths.some((p) => /sdk\/src\/query\/validate\.ts$/.test(p))
),
`expected cross-name verify.cjs <-> validate.ts warning, got: ${JSON.stringify(payload.warnings)}`
);
} finally {
cleanupFixture(tmpDir);
}
});
});

View File

@@ -0,0 +1,53 @@
// allow-test-rule: architectural-invariant
// Guards a removal mandated by ADR-0174 (retire @opengsd/gsd-sdk package
// boundary), which explicitly deletes the generator-based CJS↔SDK hand-sync
// tooling. These assertions check repo structure (file absence, package.json
// wiring) — they are not source-text inspection of any .cjs module — and exist
// to prevent silent re-introduction of the retired seam tooling.
/**
* Regression guard for issue #556 — retire orphaned CJS↔SDK hand-sync tooling.
*
* The @opengsd/gsd-sdk package boundary was retired (ADR-0174, #191/#192) and
* the `sdk/` tree is no longer tracked. The generator-based hand-sync lint that
* policed CJS-vs-SDK TypeScript drift is therefore dead infrastructure: it
* referenced an `sdk/src` tree that no longer exists and was wired into no CI
* workflow or npm script. This guard asserts it stays removed.
*/
const { test, describe } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const REPO_ROOT = path.join(__dirname, '..');
describe('retired CJS↔SDK hand-sync tooling (#556 / ADR-0174)', () => {
test('the hand-sync pair lint script is absent', () => {
const lintScript = path.join(REPO_ROOT, 'scripts', 'lint-shared-module-handsync.cjs');
assert.ok(
!fs.existsSync(lintScript),
'scripts/lint-shared-module-handsync.cjs should be removed — the CJS↔SDK seam it policed was retired by ADR-0174',
);
});
test('the hand-sync allowlist is absent', () => {
const allowlist = path.join(REPO_ROOT, 'scripts', 'shared-module-handsync-allowlist.json');
assert.ok(
!fs.existsSync(allowlist),
'scripts/shared-module-handsync-allowlist.json should be removed — it paired bin/lib/*.cjs files with sdk/src sources that no longer exist',
);
});
test('no npm script re-wires the retired hand-sync lint', () => {
const pkg = JSON.parse(fs.readFileSync(path.join(REPO_ROOT, 'package.json'), 'utf8'));
const offenders = Object.entries(pkg.scripts || {})
.filter(([, cmd]) => cmd.includes('lint-shared-module-handsync'))
.map(([name]) => name);
assert.deepEqual(
offenders,
[],
`package.json scripts must not invoke the retired hand-sync lint; found: ${offenders.join(', ')}`,
);
});
});