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

@@ -179,8 +179,6 @@ Contributor requirements (summary):
- Do not rewrite maintainer intent in `CONTEXT.md`/ADRs as part of drive-by cleanup; propose focused updates tied to approved scope.
- If using an AI assistant, prompt it to read `CONTEXT.md` and the relevant ADRs before writing any code or docs, and verify it used the correct vocabulary before opening the PR.
**CJS↔SDK seam.** When working on `bin/lib/*.cjs` or `sdk/src/**`, read [`docs/agents/cjs-sdk-seam.md`](docs/agents/cjs-sdk-seam.md). It documents the canonical pattern for Shared Modules (data manifest + source-of-truth file + generator + freshness check + Adapters) and the hand-sync pair lint that blocks new drift. New `<name>.cjs` ↔ `<name>.ts` pairs require either migration to a Shared Module or an explicit allowlist entry with justification in `scripts/shared-module-handsync-allowlist.json`. Adding an allowlist entry requires maintainer review via CODEOWNERS.
**Every PR must link to an approved issue.** PRs without a linked issue are closed without review, no exceptions.
- **No draft PRs** — draft PRs are automatically closed. Only open a PR when it is complete, tested, and ready for review. If your work is not finished, keep it on your local branch until it is.

View File

@@ -1,6 +1,6 @@
# PRD: CJS↔SDK hard seam — Shared-Module migration
- **Status:** Reference
- **Status:** Superseded by [ADR-0174](../adr/0174-retire-gsd-sdk-package-boundary.md) (2026-05-23) — historical migration plan; the CJS↔SDK seam and its hand-sync tooling were retired with the `@opengsd/gsd-sdk` package boundary
- **Date:** 2026-05-14
- **Tracking issue:** [#3524](https://github.com/open-gsd/gsd-core/issues/3524)
- **Related ADR:** [`docs/adr/3524-cjs-sdk-hard-seam.md`](../adr/3524-cjs-sdk-hard-seam.md)

View File

@@ -24,4 +24,4 @@ The GitHub-assigned issue number is the prefix. Do not compute a sequential numb
| PRD | Title | Status |
|-----|-------|--------|
| [3524-cjs-sdk-hard-seam.md](3524-cjs-sdk-hard-seam.md) | CJS↔SDK hard seam — phased migration (#3524) | Reference |
| [3524-cjs-sdk-hard-seam.md](3524-cjs-sdk-hard-seam.md) | CJS↔SDK hard seam — phased migration (#3524) | Superseded by ADR-0174 |

View File

@@ -1,388 +0,0 @@
#!/usr/bin/env node
'use strict';
/**
* Shared Module hand-sync drift lint — Phase 6 of #3524 (#3575).
*
* Scans get-shit-done/bin/lib/ for .cjs files and checks whether a matching
* TypeScript file exists in sdk/src/<name>.ts, sdk/src/query/<name>.ts, or
* sdk/src/<name>/index.ts (excluding *.generated.ts and *.test.ts).
*
* Allowlist entries are keyed by the (cjs, ts) PAIR. An entry with cjs
* `bin/lib/foo.cjs` and ts `sdk/src/foo.ts` only allow-throughs that exact
* pair — a sibling at `sdk/src/query/foo.ts` is still flagged.
*
* Cross-name pairs are also supported when declared in the allowlist
* (for example `verify.cjs` <-> `validate.ts`), so cooperating siblings are
* observable even when basenames differ.
*
* If a pair is found:
* - cooperatingSiblings (matching cjs + ts): accepted silently (exit 0).
* - migrateMeBacklog (matching cjs + ts): emits a WARNING only when
* --warn-all is set; otherwise the pair passes silently. Backlog
* pairs never fail CI.
* - Unlisted pairs (cjs or ts not on either list): ERROR — exit 1.
*
* Usage:
* node scripts/lint-shared-module-handsync.cjs
* node scripts/lint-shared-module-handsync.cjs --root /path/to/repo
* node scripts/lint-shared-module-handsync.cjs --warn-all
* node scripts/lint-shared-module-handsync.cjs --cjs-dir custom/bin/lib --sdk-src custom/sdk/src
*/
const fs = require('fs');
const path = require('path');
// ---------------------------------------------------------------------------
// Argument parsing
// ---------------------------------------------------------------------------
const args = process.argv.slice(2);
let ROOT = path.resolve(__dirname, '..');
let CJS_DIR = null; // resolved below
let SDK_SRC = null; // resolved below
let SDK_SRC_EXPLICIT = false;
let ALLOWLIST_OVERRIDE = null; // resolved below
let WARN_ALL = false;
let JSON_OUTPUT = false;
for (let i = 0; i < args.length; i++) {
if (args[i] === '--root' && args[i + 1]) {
ROOT = path.resolve(args[++i]);
} else if (args[i] === '--cjs-dir' && args[i + 1]) {
CJS_DIR = path.resolve(args[++i]);
} else if (args[i] === '--sdk-src' && args[i + 1]) {
SDK_SRC = path.resolve(args[++i]);
SDK_SRC_EXPLICIT = true;
} else if (args[i] === '--allowlist' && args[i + 1]) {
ALLOWLIST_OVERRIDE = path.resolve(args[++i]);
} else if (args[i] === '--warn-all') {
WARN_ALL = true;
} else if (args[i] === '--json') {
JSON_OUTPUT = true;
}
}
if (!CJS_DIR) CJS_DIR = path.join(ROOT, 'get-shit-done', 'bin', 'lib');
if (!SDK_SRC) SDK_SRC = path.join(ROOT, 'sdk', 'src');
// ---------------------------------------------------------------------------
// Load allowlist
// When --root is given (e.g. in tests), prefer <ROOT>/scripts/allowlist.json
// so fixture trees can supply their own allowlist. Fall back to the copy
// co-located with this script (default production path).
// ---------------------------------------------------------------------------
const ALLOWLIST_PATH = ALLOWLIST_OVERRIDE
? ALLOWLIST_OVERRIDE
: fs.existsSync(path.join(ROOT, 'scripts', 'shared-module-handsync-allowlist.json'))
? path.join(ROOT, 'scripts', 'shared-module-handsync-allowlist.json')
: path.join(__dirname, 'shared-module-handsync-allowlist.json');
let allowlist;
try {
allowlist = JSON.parse(fs.readFileSync(ALLOWLIST_PATH, 'utf8'));
} catch (err) {
process.stderr.write(
`lint-shared-module-handsync: failed to read allowlist at ${ALLOWLIST_PATH}: ${err.message}\n`
);
process.exit(1);
}
/**
* Pair identity = `${cjs}::${ts}`. Keying on the pair (not just cjs)
* prevents an allowlisted entry from silently passing an unintended
* sibling at a different ts path with the same basename.
*
* @type {Set<string>} pair identities in cooperatingSiblings
*/
const cooperatingPairs = new Set(
(allowlist.cooperatingSiblings || []).map((e) => `${e.cjs}::${e.ts}`)
);
/** @type {Map<string, object>} pair identity -> entry for migrateMeBacklog */
const migrateMap = new Map(
(allowlist.migrateMeBacklog || []).map((e) => [`${e.cjs}::${e.ts}`, e])
);
/** @type {Map<string, Set<string>>} cjs rel path -> explicit declared ts rel paths */
const explicitTsByCjs = new Map();
for (const entry of [
...(allowlist.cooperatingSiblings || []),
...(allowlist.migrateMeBacklog || []),
]) {
if (!entry || typeof entry.cjs !== 'string' || typeof entry.ts !== 'string') continue;
if (!explicitTsByCjs.has(entry.cjs)) explicitTsByCjs.set(entry.cjs, new Set());
explicitTsByCjs.get(entry.cjs).add(entry.ts);
}
// ---------------------------------------------------------------------------
// Build SDK name index: name -> array of absolute TS paths
// (excludes *.generated.ts and *.test.ts)
// ---------------------------------------------------------------------------
function buildSdkIndex(sdkSrc) {
const index = new Map(); // name -> [absPath, ...]
function addEntry(name, absPath) {
if (!index.has(name)) index.set(name, []);
index.get(name).push(absPath);
}
function walk(dir) {
let entries;
try {
entries = fs.readdirSync(dir, { withFileTypes: true });
} catch (_) {
return;
}
for (const ent of entries) {
const abs = path.join(dir, ent.name);
if (ent.isDirectory()) {
walk(abs);
} else if (ent.isFile() && ent.name.endsWith('.ts') &&
!ent.name.endsWith('.generated.ts') &&
!ent.name.endsWith('.test.ts')) {
const rel = path.relative(sdkSrc, abs);
const parts = rel.split(path.sep);
// sdk/src/<name>.ts (direct child, not in a subdir)
if (parts.length === 1) {
const name = parts[0].slice(0, -3); // strip .ts
addEntry(name, abs);
}
// sdk/src/<name>/index.ts (one subdir deep, file is index.ts)
else if (parts.length === 2 && parts[1] === 'index.ts') {
const name = parts[0];
addEntry(name, abs);
}
// sdk/src/query/<name>.ts (exactly: query/<something>.ts)
else if (parts.length === 2 && parts[0] === 'query' && parts[1] !== 'index.ts') {
const name = parts[1].slice(0, -3); // strip .ts
addEntry(name, abs);
}
}
}
}
walk(sdkSrc);
return index;
}
// ---------------------------------------------------------------------------
// Scan CJS files (direct children only; exclude *.generated.cjs)
// ---------------------------------------------------------------------------
function scanCjsFiles(cjsDir) {
let entries;
try {
entries = fs.readdirSync(cjsDir, { withFileTypes: true });
} catch (err) {
process.stderr.write(
`lint-shared-module-handsync: cannot read CJS dir ${cjsDir}: ${err.message}\n`
);
process.exit(1);
}
return entries
.filter(
(e) =>
e.isFile() &&
e.name.endsWith('.cjs') &&
!e.name.endsWith('.generated.cjs')
)
.map((e) => ({
name: e.name.slice(0, -4), // strip .cjs
absPath: path.join(cjsDir, e.name),
}));
}
// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------
function emitJson(payload) {
process.stdout.write(JSON.stringify(payload) + '\n');
}
function main() {
// Check that the directories exist
if (!fs.existsSync(CJS_DIR)) {
if (JSON_OUTPUT) {
emitJson({ ok: false, reason: 'cjs_dir_missing', path: CJS_DIR });
} else {
process.stderr.write(
`lint-shared-module-handsync: CJS dir not found: ${CJS_DIR}\n` +
` Pass --root <repo-root> or --cjs-dir <path> to override.\n`
);
}
process.exit(1);
}
if (!fs.existsSync(SDK_SRC)) {
// SDK tree was retired in ADR-0174 Phase 5.2. In default repo mode this
// lint becomes a no-op success; explicit --sdk-src remains fail-loud.
if (SDK_SRC_EXPLICIT) {
if (JSON_OUTPUT) {
emitJson({ ok: false, reason: 'sdk_src_missing', path: SDK_SRC });
} else {
process.stderr.write(
`lint-shared-module-handsync: SDK src dir not found: ${SDK_SRC}\n` +
` Pass --root <repo-root> or --sdk-src <path> to override.\n`
);
}
process.exit(1);
}
if (JSON_OUTPUT) {
emitJson({
ok: true,
reason: 'sdk_retired',
cooperatingCount: 0,
backlogCount: 0,
warnings: [],
});
} else {
process.stdout.write(
'ok lint-shared-module-handsync: SDK source tree retired; no hand-sync pairs to lint.\n'
);
}
process.exit(0);
}
const sdkIndex = buildSdkIndex(SDK_SRC);
const cjsFiles = scanCjsFiles(CJS_DIR);
const errors = [];
const warnings = [];
function candidateTsPathsFor(relCjs, name) {
const byName = sdkIndex.has(name)
? sdkIndex
.get(name)
.map((p) => path.relative(ROOT, p).replace(/\\/g, '/'))
: [];
const declared = explicitTsByCjs.has(relCjs)
? Array.from(explicitTsByCjs.get(relCjs))
: [];
const declaredExisting = declared.filter((relTs) => {
const abs = path.join(ROOT, relTs);
return (
relTs.endsWith('.ts') &&
!relTs.endsWith('.generated.ts') &&
!relTs.endsWith('.test.ts') &&
fs.existsSync(abs) &&
fs.statSync(abs).isFile()
);
});
return Array.from(new Set([...byName, ...declaredExisting]));
}
for (const { name, absPath } of cjsFiles) {
// Compute relative CJS path and all declared/discovered TS candidates.
const relCjs = path.relative(ROOT, absPath).replace(/\\/g, '/');
const tsPaths = candidateTsPathsFor(relCjs, name);
if (tsPaths.length === 0) continue;
// Pair-aware matching, per ts sibling. Each ts candidate is classified
// independently against the allowlist so a partially-allowlisted set of
// siblings still surfaces the unauthorized ones. See #3632.
const unauthorizedTs = [];
const backlogTsForCjs = [];
for (const relTs of tsPaths) {
const pairKey = `${relCjs}::${relTs}`;
if (cooperatingPairs.has(pairKey)) continue;
if (migrateMap.has(pairKey)) {
backlogTsForCjs.push(relTs);
continue;
}
unauthorizedTs.push(relTs);
}
if (unauthorizedTs.length > 0) {
errors.push({ relCjs, tsPaths: unauthorizedTs });
}
if (backlogTsForCjs.length > 0) {
const entry = migrateMap.get(`${relCjs}::${backlogTsForCjs[0]}`);
warnings.push({ relCjs, tsPaths: backlogTsForCjs, entry });
}
}
// Count cjs files whose pair identity (cjs+ts) is on cooperatingSiblings.
// A file with multiple ts candidates is counted once if any pair matches.
const cooperatingCount = cjsFiles.filter((f) => {
const relCjs = path.relative(ROOT, f.absPath).replace(/\\/g, '/');
return candidateTsPathsFor(relCjs, f.name).some((relTs) =>
cooperatingPairs.has(`${relCjs}::${relTs}`)
);
}).length;
// -------------------------------------------------------------------------
// Report errors (exit 1)
// -------------------------------------------------------------------------
if (errors.length > 0) {
if (JSON_OUTPUT) {
emitJson({
ok: false,
reason: 'unauthorized_pairs',
errors,
warnings,
cooperatingCount,
});
} else {
process.stderr.write(
`\nERROR lint-shared-module-handsync: ${errors.length} unauthorized hand-sync pair(s) found.\n\n`
);
for (const { relCjs, tsPaths } of errors) {
process.stderr.write(` CJS: ${relCjs}\n`);
for (const ts of tsPaths) {
process.stderr.write(` TS: ${ts}\n`);
}
process.stderr.write('\n');
}
process.stderr.write(
'To resolve, choose one of:\n' +
' 1. Migrate to a Shared Module (preferred): create sdk/src/<name>/index.ts as the\n' +
' source-of-truth, write a generator script (sdk/scripts/gen-<name>.mjs), add a\n' +
' freshness check, and update CI. See docs/agents/cjs-sdk-seam.md for the pattern.\n' +
' 2. Add an explicit allowlist entry to scripts/shared-module-handsync-allowlist.json\n' +
' with a justification explaining why this pair is a legitimate cooperating sibling\n' +
' rather than a drift anti-pattern. Requires maintainer review via CODEOWNERS.\n\n'
);
}
process.exit(1);
}
// -------------------------------------------------------------------------
// Report warnings (no exit code change)
// -------------------------------------------------------------------------
if (warnings.length > 0 && WARN_ALL && !JSON_OUTPUT) {
process.stderr.write(
`\nWARNING lint-shared-module-handsync: ${warnings.length} known drift anti-pattern pair(s) in migrateMeBacklog.\n` +
`These are tracked for future Shared Module migration but do not block CI.\n\n`
);
for (const { relCjs, tsPaths, entry } of warnings) {
process.stderr.write(` CJS: ${relCjs}\n`);
for (const ts of tsPaths) {
process.stderr.write(` TS: ${ts}\n`);
}
process.stderr.write(` Tracked: ${entry.trackedIn}\n`);
process.stderr.write(` Hint: ${entry.justification}\n\n`);
}
}
// -------------------------------------------------------------------------
// Success
// -------------------------------------------------------------------------
if (JSON_OUTPUT) {
emitJson({
ok: true,
cooperatingCount,
backlogCount: warnings.length,
warnings,
});
} else {
process.stdout.write(
`ok lint-shared-module-handsync: no unauthorized hand-sync pairs found` +
` (${cooperatingCount} cooperating sibling(s), ${warnings.length} backlog pair(s))\n`
);
}
process.exit(0);
}
main();

View File

@@ -1,183 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"_comment": "Allowlist for scripts/lint-shared-module-handsync.cjs — Phase 6 of #3524 (#3575). Two categories: cooperatingSiblings (legitimate pairs, lint accepts silently) and migrateMeBacklog (known drift anti-patterns, lint warns but does not fail). All entries require cjs + ts path + classification + justification.",
"cooperatingSiblings": [
{
"cjs": "get-shit-done/bin/lib/active-workstream-store.cjs",
"ts": "sdk/src/query/active-workstream-store.ts",
"classification": "cooperating-sibling",
"justification": "CJS manages filesystem-backed workstream store; SDK layer wraps via Adapter for query dispatch. Different responsibilities, not drift."
},
{
"cjs": "get-shit-done/bin/lib/config-schema.cjs",
"ts": "sdk/src/query/config-schema.ts",
"classification": "cooperating-sibling",
"justification": "SDK config-schema.ts is the generated source-of-truth derived from sdk/shared/config-schema.manifest.json (Phase 2/#3540). CJS config-schema.cjs is the Adapter that reads from that manifest. Not a hand-sync pair; freshness check enforces alignment."
},
{
"cjs": "get-shit-done/bin/lib/frontmatter.cjs",
"ts": "sdk/src/query/frontmatter.ts",
"classification": "cooperating-sibling",
"justification": "CJS implements full frontmatter parsing/mutation; SDK frontmatter.ts is the native SDK query handler delegating to the CJS runtime via the seam bridge. Not duplicating logic."
},
{
"cjs": "get-shit-done/bin/lib/init.cjs",
"ts": "sdk/src/query/init.ts",
"classification": "cooperating-sibling",
"justification": "CJS init.cjs is the authoritative initializer; SDK init.ts provides the native handler layer for the SDK query seam. Phase 5.2+ will migrate remaining subcommands, but current architecture is intentional."
},
{
"cjs": "get-shit-done/bin/lib/phase.cjs",
"ts": "sdk/src/query/phase.ts",
"classification": "cooperating-sibling",
"justification": "CJS phase.cjs implements sync mutation handlers (phaseAdd, phaseInsert, phaseRemove, phaseComplete) and query handlers with sync I/O; SDK phase.ts provides the native async query handler. The async mutation handlers (phase-lifecycle.ts) and pure policy helpers (phase-lifecycle-policy.ts) are I/O-bound or pure and remain per-side (ADR-3524 §4). Issue #4 fix: phase.cjs cmdPhaseComplete now consumes pure helpers from phase-lifecycle.generated.cjs (generated from sdk/src/query/phase-lifecycle.ts) for idempotent Completed Phases counting and clamped percent. *.generated.cjs files are excluded from lint scanner; see sdk/scripts/check-phase-lifecycle-fresh.mjs for freshness enforcement."
},
{
"cjs": "get-shit-done/bin/lib/phase-lifecycle.cjs",
"ts": "sdk/src/query/phase-lifecycle.ts",
"classification": "ADAPTER-OVER-MODULE",
"justification": "CJS phase-lifecycle.cjs is the pure helper module consumed by phase.cjs; sdk/src/query/phase-lifecycle.ts remains the typed source counterpart. This pair is intentionally tracked as adapter-over-module rather than an unauthorized hand-sync pair."
},
{
"cjs": "get-shit-done/bin/lib/profile-output.cjs",
"ts": "sdk/src/query/profile-output.ts",
"classification": "cooperating-sibling",
"justification": "CJS profile-output.cjs handles profiling output rendering; SDK profile-output.ts is the corresponding SDK query handler. Separate responsibilities across the seam."
},
{
"cjs": "get-shit-done/bin/lib/roadmap.cjs",
"ts": "sdk/src/query/roadmap.ts",
"classification": "cooperating-sibling",
"justification": "CJS roadmap.cjs is the full roadmap implementation; SDK roadmap.ts provides the native handler for SDK query dispatch. Phase 5.2+ candidate."
},
{
"cjs": "get-shit-done/bin/lib/state.cjs",
"ts": "sdk/src/query/state.ts",
"classification": "cooperating-sibling",
"justification": "CJS state.cjs is the full state implementation; SDK state.ts routes known subcommands via executeForCjs (Phase 5.0/#3558, Phase 5.1/#3574). Intentional seam delegation pattern."
},
{
"cjs": "get-shit-done/bin/lib/state-document.cjs",
"ts": "sdk/src/state/index.ts",
"classification": "ADAPTER-OVER-MODULE",
"justification": "CJS state-document.cjs is a thin Adapter over get-shit-done/bin/lib/state-document.generated.cjs. That generated artifact is emitted from sdk/src/state/index.ts by sdk/scripts/gen-state-document.ts; freshness is enforced by sdk/scripts/check-state-document-fresh.mjs."
},
{
"cjs": "get-shit-done/bin/lib/template.cjs",
"ts": "sdk/src/query/template.ts",
"classification": "cooperating-sibling",
"justification": "CJS template.cjs handles template operations; SDK template.ts is the corresponding SDK native handler. Separate responsibilities across the seam."
},
{
"cjs": "get-shit-done/bin/lib/uat.cjs",
"ts": "sdk/src/query/uat.ts",
"classification": "cooperating-sibling",
"justification": "CJS uat.cjs implements UAT workflows; SDK uat.ts provides the SDK query handler layer. Separate responsibilities."
},
{
"cjs": "get-shit-done/bin/lib/verify.cjs",
"ts": "sdk/src/query/verify.ts",
"classification": "cooperating-sibling",
"justification": "CJS verify.cjs is the full verify implementation; SDK verify.ts provides the native handler. Phase 5.2+ candidate for further delegation. Check 8 (W006/W007) helpers now generated from sdk/src/query/validate.ts via sdk/scripts/gen-validate.mjs (issue #6); freshness check: sdk/scripts/check-validate-fresh.mjs."
},
{
"cjs": "get-shit-done/bin/lib/validate.cjs",
"ts": "sdk/src/query/validate.ts",
"classification": "ADAPTER-OVER-MODULE",
"justification": "CJS validate.cjs provides the pure validation helpers consumed by verify.cjs while sdk/src/query/validate.ts is the SDK handler-side counterpart. Keep this explicit adapter pair allowlisted to avoid false-positive hand-sync lint failures."
},
{
"cjs": "get-shit-done/bin/lib/workstream.cjs",
"ts": "sdk/src/query/workstream.ts",
"classification": "cooperating-sibling",
"justification": "CJS workstream.cjs handles workstream management; SDK workstream.ts provides the SDK query handler. Workstream support inside sync bridge is an open follow-up item."
},
{
"cjs": "get-shit-done/bin/lib/workstream-inventory.cjs",
"ts": "sdk/src/query/workstream-inventory.ts",
"classification": "cooperating-sibling",
"justification": "CJS workstream-inventory.cjs is the generated Adapter for the workstream-inventory Shared Module (Phase 3/#3548). SDK workstream-inventory.ts is the source-of-truth query handler. Freshness check enforces alignment."
},
{
"cjs": "get-shit-done/bin/lib/config.cjs",
"ts": "sdk/src/config.ts",
"classification": "CJS-CLI-ONLY",
"justification": "Phase 2 (#3536) already migrated CONFIG_DEFAULTS and loadConfig/mergeDefaults to the Configuration Module and sdk/src/config.ts. What remains in config.cjs is exclusively CLI command handlers (cmdConfigGet, cmdConfigSet, cmdConfigNewProject, cmdConfigEnsureSection, cmdConfigSetModelProfile, cmdConfigPath, cmdMigrateConfig, buildNewProjectConfig, setConfigValue, ensureConfigFile) that depend on CJS-only APIs (withPlanningLock, platformWriteSync/ReadSync/EnsureDir, sync fs ops, process.exit). sdk/src/config.ts provides only the async loadConfig/mergeDefaults SDK layer. The two files serve disjoint surfaces with no logical overlap — not a hand-sync drift anti-pattern."
},
{
"cjs": "get-shit-done/bin/lib/config.cjs",
"ts": "sdk/src/config/index.ts",
"classification": "ADAPTER-OVER-MODULE",
"justification": "sdk/src/config/index.ts is the Configuration Module source-of-truth; CJS config.cjs consumes its generated Adapter through core/config-schema integration. This is an intentional seam relationship, not manual hand-sync."
},
{
"cjs": "get-shit-done/bin/lib/state.cjs",
"ts": "sdk/src/state/index.ts",
"classification": "ADAPTER-OVER-MODULE",
"justification": "sdk/src/state/index.ts is the STATE.md Document Module source-of-truth that emits get-shit-done/bin/lib/state-document.generated.cjs. state.cjs consumes that generated module as an Adapter dependency; basename overlap is intentional and not a duplicated implementation."
},
{
"cjs": "get-shit-done/bin/lib/intel.cjs",
"ts": "sdk/src/query/intel.ts",
"classification": "cooperating-sibling",
"justification": "CJS intel.cjs is the synchronous runtime implementation used by gsd-tools.cjs; sdk/src/query/intel.ts is the async QueryHandler port for the SDK query seam (explicitly documented as a port in its file header). The two files intentionally diverge on INTEL_FILES naming (CJS: file-roles.json/api-map.json/dependency-graph.json/arch-decisions.json; SDK: files.json/apis.json/deps.json/arch.md) — existing CJS tests are locked to the old naming. Not a hand-sync drift pattern; separate runtime responsibilities across the seam."
},
{
"cjs": "get-shit-done/bin/lib/model-catalog.cjs",
"ts": "sdk/src/model-catalog.ts",
"classification": "ADAPTER-OVER-MODULE",
"justification": "Both files read from sdk/shared/model-catalog.json (ADR-0003 precedent) as independent consumers of the shared manifest. CJS exposes VALID_AGENT_TIERS, MODEL_ALIAS_MAP, RUNTIME_PROFILE_MAP, KNOWN_RUNTIMES, RUNTIMES_WITH_REASONING_EFFORT, nextTier, formatAgentToModelMapAsTable for core.cjs and model-profiles.cjs consumers. SDK exposes resolveRuntimeTierDefault, runtimesWithReasoningEffort for session-runner.ts and query handlers. The shared JSON is the single source-of-truth; both adapters derive their exports from it without duplicating any logic between themselves."
},
{
"cjs": "get-shit-done/bin/lib/plan-scan.cjs",
"ts": "sdk/src/query/plan-scan.ts",
"classification": "ADAPTER-OVER-MODULE",
"justification": "CJS plan-scan.cjs is the generated Adapter reading from sdk/src/query/plan-scan.ts Shared Module (Phase 6/#3575). SDK plan-scan.ts is the source-of-truth. Freshness check (check-plan-scan-fresh.mjs) enforces alignment."
},
{
"cjs": "get-shit-done/bin/lib/secrets.cjs",
"ts": "sdk/src/query/secrets.ts",
"classification": "ADAPTER-OVER-MODULE",
"justification": "CJS secrets.cjs is the generated Adapter reading from sdk/src/query/secrets.ts Shared Module (Phase 6/#3575). SDK secrets.ts is the source-of-truth. Freshness check (check-secrets-fresh.mjs) enforces alignment."
},
{
"cjs": "get-shit-done/bin/lib/schema-detect.cjs",
"ts": "sdk/src/query/schema-detect.ts",
"classification": "ADAPTER-OVER-MODULE",
"justification": "CJS schema-detect.cjs is the generated Adapter reading from sdk/src/query/schema-detect.ts Shared Module (Phase 6/#3575). SDK schema-detect.ts is the source-of-truth. Generated CJS adds detectSchemaOrm compat export (not in SDK) and exports SCHEMA_PATTERNS/ORM_INFO for backward compatibility. Freshness check (check-schema-detect-fresh.mjs) enforces alignment."
},
{
"cjs": "get-shit-done/bin/lib/decisions.cjs",
"ts": "sdk/src/query/decisions.ts",
"classification": "ADAPTER-OVER-MODULE",
"justification": "Phase 6 (#3575): CJS decisions.cjs is the generated Adapter reading from sdk/src/query/decisions.ts Shared Module. SDK source-of-truth; regex aligned to accept alphanumeric IDs (D-INFRA-01). CJS callers (gap-checker.cjs) use {id, text} subset; extra fields {category, tags, trackable} are present but ignored. Freshness check (check-decisions-fresh.mjs) enforces alignment."
},
{
"cjs": "get-shit-done/bin/lib/workstream-name-policy.cjs",
"ts": "sdk/src/workstream-name-policy.ts",
"classification": "ADAPTER-OVER-MODULE",
"justification": "Phase 6 (#3575): CJS workstream-name-policy.cjs is the generated Adapter reading from sdk/src/workstream-name-policy.ts Shared Module. SDK source-of-truth now exports all three functions used by CJS callers (toWorkstreamSlug, hasInvalidPathSegment, isValidActiveWorkstreamName) plus validateWorkstreamName alias. Freshness check (check-workstream-name-policy-fresh.mjs) enforces alignment."
},
{
"cjs": "get-shit-done/bin/lib/runtime-name-policy.cjs",
"ts": "sdk/src/runtime-name-policy.ts",
"classification": "ADAPTER-OVER-MODULE",
"justification": "Both CJS and SDK runtime-name adapters consume sdk/shared/runtime-aliases.manifest.json as the single source of truth for alias canonicalization. This pair is an intentional seam-level adapter split, not a drift-prone hand-sync anti-pattern."
},
{
"cjs": "get-shit-done/bin/lib/prompt-budget.cjs",
"ts": "sdk/src/query/prompt-budget.ts",
"classification": "cooperating-sibling",
"justification": "CJS prompt-budget.cjs provides the applyBudget implementation used by gsd-tools.cjs case 'prompt-budget'. SDK prompt-budget.ts is the native QueryHandler port for gsd-sdk query dispatch (#3081). The SDK handler ports the pure budget logic and adds CLI arg parsing / file I/O directly, satisfying the registry-integration drift-guard without duplicating shared state."
}
],
"migrateMeBacklog": [
{
"cjs": "get-shit-done/bin/lib/verify.cjs",
"ts": "sdk/src/query/validate.ts",
"classification": "drift-anti-pattern",
"justification": "Cross-name cooperating pair where validate policy changes can drift from verify checks; keep explicitly visible until Shared Module extraction removes hand-sync risk.",
"trackedIn": "#11"
}
]
}

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(', ')}`,
);
});
});