Files
msd-core/docs/adr/457-generated-cjs-single-source.md
Tom Boucher ed1c20061b docs(#456): add testing standards, ADRs 452/456/457, and CONTEXT.md entries (#458)
- docs/adr/452-eslint-lint-harness.md (Accepted): adopt ESLint flat config
  with typescript-eslint, eslint-plugin-n, eslint-plugin-no-only-tests, and
  local AST-rule plugin; retire homegrown scripts/lint-*.cjs regex checkers;
  three test-rigor rules ship at warn, promoted to error after #453 cleanup
- docs/adr/456-test-rigor-architecture.md (Accepted): deterministic-over-racing
  via injectable clock seam + node:test mock.timers; antagonistic tier with
  fast-check + Stryker at 80% threshold; typed-surface mandate; delete-bad-tests
  policy with no-permanent-quarantine
- docs/adr/457-generated-cjs-single-source.md (Proposed): future direction to
  collapse ~59 hand-written bin/lib/*.cjs to TS-generated single source;
  eliminates tsconfig.lint.json stopgap; marked Proposed / not yet executed
- TESTING-STANDARDS.md: orients to existing docs; codifies six test-rigor
  contracts; adds new policies (no-timing-assertion, clock-seam, property-based,
  mutation-score, delete-bad-tests); pairs each with exact ESLint rule names;
  markdownlint-clean (MD040 fences, MD056 table columns)
- CONTEXT.md: adds six RULESET.TESTS.* predicates (no-timing-assertion,
  clock-seam, property-based-testing, mutation-score, delete-bad-tests,
  eslint-harness) and five glossary terms (clock seam, deterministic scheduler,
  property-based test, mutation testing/score, ESLint harness)
- docs/adr/README.md: adds index rows for ADRs 452, 456, 457

Co-authored-by: CI Rebase Check <ci@gsd-redux>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 10:33:36 -04:00

7.1 KiB

ADR 457: Collapse hand-written CJS to generated single-source [Proposed]

  • Status: Proposed
  • Date: 2026-05-28

Not yet executed. This ADR records the agreed direction and rationale. No code has been changed under this decision. Implementation is tracked separately and requires the ESLint harness (ADR 452) to be in place first.

This ADR proposes collapsing the ~59 hand-written get-shit-done/bin/lib/*.cjs files into TypeScript sources compiled (generated) into .cjs output, eliminating the hand-written/generated split, enabling type-aware linting and CJS/TS parity as first-class CI signals, and removing tsconfig.lint.json as a stopgap.

Context

Today's split

The get-shit-done/bin/lib/ directory currently holds two kinds of files:

  • Hand-written .cjs (~59 files) — authored directly as CommonJS. These are the runtime entry points for CLI commands, library seams, and utilities. Type checking relies on tsconfig.lint.json and @ts-check comments; coverage is uneven.
  • Generated .cjs (~13 files) — compiled from .ts sources in sdk/src/ or get-shit-done/src/ via tsc. These files carry a // @generated header; they must never be hand-edited. The CJS/TS parity tests (tests/cjs-ts-parity.test.cjs) assert that the generated surface matches the TS declarations.

This split creates three structural problems:

  1. Type-aware linting gap. tsconfig.lint.json is not wired into an ESLint project reference. typescript-eslint type-aware rules (@typescript-eslint/no-floating-promises, @typescript-eslint/strict-boolean-expressions, etc.) do not run on hand-written .cjs files. ADR 452 introduces the ESLint harness as a prerequisite but cannot close the type-aware gap until the sources are TypeScript.

  2. CJS/TS parity is partial. The parity tests only cover the generated surface (~13 files). The hand-written surface (~59 files) has no equivalent parity signal. Regressions on the hand-written surface are caught only by behavioral tests, not by type or surface comparison.

  3. tsconfig.lint.json as a permanent stopgap. The file was introduced with an explicit // stopgap annotation in its header comment. It adds a non-standard compilation path that must be kept in sync with tsconfig.json and tsconfig.build.json. Every time a new .cjs file is added, tsconfig.lint.json must be manually updated.

Relationship to the retired SDK boundary

ADR 0174 (0174-retire-gsd-sdk-package-boundary.md) retired the @opengsd/gsd-sdk package boundary and collapsed the runtime onto a single src/ TS surface. The generated .cjs files are the downstream artifact of that collapse. This ADR extends the same direction to the hand-written files.

Decision [Proposed]

  1. Single TS source. Each hand-written get-shit-done/bin/lib/*.cjs module is rewritten as get-shit-done/src/<module>.ts (or the equivalent path inside the unified src/ tree established by ADR 0174). The TS source is the canonical artifact; .cjs output is generated by tsc and checked in as part of the build step.

  2. No hand-written runtime .cjs. After the collapse, the only .cjs files in get-shit-done/bin/lib/ are generated. Hand-authoring a .cjs file in that directory is a policy violation caught by a new scripts/lint-no-hand-written-cjs.cjs check (or equivalent ESLint rule).

  3. tsconfig.lint.json deleted. Once all sources are TypeScript, tsconfig.lint.json is no longer needed. The ESLint project reference in eslint.config.mjs (ADR 452) points directly to tsconfig.json. @ts-check comments in .cjs files are removed as part of the migration.

  4. CJS/TS parity becomes total. The parity tests (tests/cjs-ts-parity.test.cjs) expand to cover the full bin/lib/ surface. A generated .cjs file that drifts from its TS source fails CI.

  5. Migration is incremental. Files are migrated module by module in separate PRs. Each PR: (a) rewrites one hand-written .cjs as TS, (b) adds it to the tsc build, (c) updates parity tests, (d) removes tsconfig.lint.json includes for the migrated file. The final PR removes tsconfig.lint.json entirely.

  6. Prerequisites. This ADR is not implemented until:

    • ADR 452 (ESLint harness) is merged and CI-green.
    • A migration tracking issue is opened with the full list of ~59 files and a per-module plan.
    • At least one pilot migration (chosen for low coupling) is completed and reviewed.

Consequences

Positive

  • Type-aware typescript-eslint rules apply to the full runtime surface, not just the generated subset.
  • CJS/TS parity is a total, automated signal rather than a partial one.
  • tsconfig.lint.json stopgap is removed; one fewer compilation path to maintain.
  • Contributors write TypeScript for all new runtime code; no new hand-authored .cjs files.

Negative

  • Migration cost. ~59 files must be migrated. Each migration may surface latent type errors that require fixes before the module can compile as TypeScript.
  • Build step added. Currently, hand-written .cjs files are runtime-ready without compilation. After migration, every change to a source .ts file requires a tsc step before the .cjs output is updated. CI must gate on build output being up to date.
  • Generated file commits. If .cjs outputs are checked in (as is current practice for the generated subset), contributors must remember to commit both the .ts source and the generated .cjs. A pre-commit hook or CI check enforces this.

For testing

  • Tests that currently import hand-written .cjs files directly (require('../bin/lib/foo.cjs')) continue to work unchanged; only the source behind the .cjs changes.
  • No test file changes are required as part of the migration itself, though new type-aware lint rules may surface existing test-code issues.

Rejected Alternatives

(a) Keep the hand-written/generated split indefinitely. Rejected. The split creates a permanent type-aware linting gap and requires tsconfig.lint.json as an indefinite stopgap. The direction established by ADR 0174 is single-runtime collapse; this ADR extends that to the remaining hand-written files.

(b) Full ESM rewrite. Rewrite all .cjs files as .mjs ES modules instead of TypeScript-compiled CJS. Rejected. The package currently ships CommonJS to support require() consumers. An ESM rewrite would be a breaking change requiring a semver-major bump and coordination with downstream consumers. The TS-compiled CJS approach achieves type safety without the compatibility break.

(c) Keep tsconfig.lint.json and wire it into ESLint. Extend the ADR 452 ESLint harness to use tsconfig.lint.json as the project reference for hand-written files. Rejected as a permanent solution. tsconfig.lint.json is explicitly a stopgap; wiring it into ESLint makes the stopgap permanent. The correct solution is to make the files TypeScript so the standard tsconfig.json is sufficient.

References

  • Tracking issue: #457
  • ESLint harness prerequisite: 452-eslint-lint-harness.md
  • Single-runtime collapse: 0174-retire-gsd-sdk-package-boundary.md
  • CJS/TS parity tests: tests/cjs-ts-parity.test.cjs
  • Stopgap: tsconfig.lint.json