diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1f3c26cb2..95b585e94 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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 `.cjs` ↔ `.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. diff --git a/docs/prd/3524-cjs-sdk-hard-seam.md b/docs/prd/3524-cjs-sdk-hard-seam.md index 43e482192..c7f23179a 100644 --- a/docs/prd/3524-cjs-sdk-hard-seam.md +++ b/docs/prd/3524-cjs-sdk-hard-seam.md @@ -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) diff --git a/docs/prd/README.md b/docs/prd/README.md index 6e027bc42..e377ebedb 100644 --- a/docs/prd/README.md +++ b/docs/prd/README.md @@ -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 | diff --git a/scripts/lint-shared-module-handsync.cjs b/scripts/lint-shared-module-handsync.cjs deleted file mode 100644 index f1f2d4273..000000000 --- a/scripts/lint-shared-module-handsync.cjs +++ /dev/null @@ -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/.ts, sdk/src/query/.ts, or - * sdk/src//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 /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} pair identities in cooperatingSiblings - */ -const cooperatingPairs = new Set( - (allowlist.cooperatingSiblings || []).map((e) => `${e.cjs}::${e.ts}`) -); - -/** @type {Map} pair identity -> entry for migrateMeBacklog */ -const migrateMap = new Map( - (allowlist.migrateMeBacklog || []).map((e) => [`${e.cjs}::${e.ts}`, e]) -); - -/** @type {Map>} 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/.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//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/.ts (exactly: query/.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 or --cjs-dir 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 or --sdk-src 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//index.ts as the\n' + - ' source-of-truth, write a generator script (sdk/scripts/gen-.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(); diff --git a/scripts/shared-module-handsync-allowlist.json b/scripts/shared-module-handsync-allowlist.json deleted file mode 100644 index 76f1bee76..000000000 --- a/scripts/shared-module-handsync-allowlist.json +++ /dev/null @@ -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" - } - ] -} diff --git a/tests/lint-shared-module-handsync.test.cjs b/tests/lint-shared-module-handsync.test.cjs deleted file mode 100644 index 846f7a933..000000000 --- a/tests/lint-shared-module-handsync.test.cjs +++ /dev/null @@ -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: -// / -// get-shit-done/bin/lib/.cjs -// sdk/src/query/.ts (if tsInQuery === true) -// sdk/src/.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/.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/.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/.ts pair. The sdk/src/query/.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/.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); - } - }); -}); diff --git a/tests/no-cjs-sdk-handsync-tooling.test.cjs b/tests/no-cjs-sdk-handsync-tooling.test.cjs new file mode 100644 index 000000000..55adf3bc4 --- /dev/null +++ b/tests/no-cjs-sdk-handsync-tooling.test.cjs @@ -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(', ')}`, + ); + }); +});