* docs(#457): rewrite ADR-457 to ground truth and accept build-at-publish The prior draft asserted a codebase state that never existed (13 tsc-generated files, src/ trees, a tests/cjs-ts-parity.test.cjs). Corrected to verified ground truth (84 bin/lib .cjs, 1 value-baked package-identity.cjs, no tsc pipeline), distinguished value-baking from transpilation so package-identity stops being miscited as precedent, made check-in-the-artifact vs build-at-publish the central decision, and flipped status to Accepted (build-at-publish). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * build(#537): pilot TS build-at-publish for bin/lib (semver-compare) First hand-written module collapsed to a TypeScript source of truth per ADR-457. src/semver-compare.cts compiles (tsc, strict, noEmitOnError) to a gitignored get-shit-done/bin/lib/semver-compare.cjs. build:lib is wired into build, pretest, pretest:coverage, and prepublishOnly so the artifact is built before test and shipped on publish. Type-aware ESLint on src/**/*.cts immediately caught the params were over-typed as `unknown` (no-base-to-string); narrowed to a honest VersionInput domain type. Behavior preserved: semver-compare.test.cjs (14) and bug-10 (4) pass against the generated output; runtime consumer changeset/cli.cjs unaffected. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#537): make build-at-publish robust across all CI paths (codex review) Adversarial review found the pilot's generated artifact would be missing on clean CI checkouts. `pretest`/`pretest:coverage` only fire for `npm test`, but CI runs `test:unit`/`test:integration`/`test:install` and `node run-tests.cjs` directly — none of which built the artifact, so any suite requiring semver-compare.cjs would hit module-not-found on a clean checkout, and install-smoke's `npm pack` could ship without it. - Add a `prepare` script (`npm run build:lib`). `npm ci` runs it automatically, so every CI test job and install-smoke's pack emit the artifact before use. This is the idiomatic npm mechanism for compiled-output-not-in-git and fixes both the test and pack paths in one place. - Add `src/` + `tsconfig.build.json` to ci-test-scope and the install-smoke / mutation path filters, so a source-only edit to a migrated module still triggers its tests and mutation coverage (prevents silent CI skips). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#537): map src/*.cts to built artifact in mutation changed-files detection Follow-up to the codex re-review. The prior commit added src/**/*.cts to the mutation workflow's path trigger but left its "compute changed core lib files" step diffing only get-shit-done/bin/lib/**/*.cjs — which are now gitignored and never appear in a diff. A source-only edit would trigger the workflow then early-exit ("no core lib files changed"), silently skipping mutation testing. Map each changed src/*.cts to its built get-shit-done/bin/lib/*.cjs path (the on-disk artifact Stryker mutates after prepare/build:lib), merge with the hand-written .cjs diff, and apply the test/excluded-module filters once. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#537): use 'src/' pathspec in mutation diff (git glob doesn't match top-level) Codex review caught that `git diff -- 'src/**/*.cts'` returns empty for a top-level file like src/semver-compare.cts — git's default pathspec glob does not match `**` across zero directories (verified on git 2.50.1). The prior commit's src-detection therefore never fired, so source-only changes still skipped mutation. Switch to the dir-scoped pathspec 'src/' + a `.cts` grep (robust for flat and nested layouts), and broaden the workflow path trigger to 'src/**' to match install-smoke. Verified end-to-end: a change to src/semver-compare.cts now resolves to get-shit-done/bin/lib/semver-compare.cjs in the --mutate list. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore(#537): add changeset fragment for build-at-publish pilot Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#537): replace prepare with prepack + build-if-missing; defer mutation wiring CI surfaced three real issues the local run and codex review missed: 1. lockfile-sync failed on every platform. Root cause: `npm ci --dry-run` (the repo's lockfile health check) RUNS the `prepare` script, but in dry-run the devDependencies aren't installed, so `tsc` is not found (exit 127) and the check reports a misleading "out of sync". `prepare` is the wrong hook for a build needing a devDep. Replace it with `prepack` (runs only on pack/publish, when node_modules exists) for the tarball path, and build the artifact inside scripts/run-tests.cjs (build-if-missing) for the test path — the universal chokepoint every CI test invocation funnels through, including the direct `node run-tests.cjs --files-from` step that bypasses npm lifecycle hooks. The guard is a no-op once built, so the run-tests harness test is unaffected. 2. The Stryker mutation gate ran only 1 test against semver-compare (~0% score, 71/71 mutants surviving) — a Stryker test-selection problem orthogonal to the build migration, and raising the score needs property tests (ADR-456). Revert the mutation.yml src wiring; mutation coverage for src-authored modules is a separate follow-up tracked in #537. (The deletion of the gitignored top-level .cjs does not match the workflow's `bin/lib/**/*.cjs` git pathspec, so the gate skips cleanly.) Verified: clean-room `npm ci --dry-run` exits 0; deleting the artifact then running a suite rebuilds it; run-tests harness 22/22 green; `npm pack` includes the built artifact via prepack. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
5
.changeset/tidy-goats-cheer.md
Normal file
5
.changeset/tidy-goats-cheer.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Changed
|
||||
pr: 541
|
||||
---
|
||||
Runtime `bin/lib` modules can now be authored as TypeScript and compiled to CommonJS via `tsc` (ADR-457 build-at-publish pilot). The first module, `semver-compare`, moves to `src/semver-compare.cts`; its `.cjs` is now a generated, gitignored build artifact emitted by `npm run build:lib` (wired into build, pretest, prepare, and prepublishOnly). Behavior is unchanged and the package still ships CommonJS.
|
||||
2
.github/workflows/install-smoke.yml
vendored
2
.github/workflows/install-smoke.yml
vendored
@@ -22,6 +22,8 @@ on:
|
||||
- 'bin/install.js'
|
||||
- 'get-shit-done/bin/gsd-tools.cjs'
|
||||
- 'get-shit-done/bin/**'
|
||||
- 'src/**'
|
||||
- 'tsconfig.build.json'
|
||||
- 'package.json'
|
||||
- 'package-lock.json'
|
||||
- 'scripts/release-tarball-smoke.cjs'
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -62,6 +62,11 @@ Thumbs.db
|
||||
.next/
|
||||
dist/
|
||||
build/
|
||||
|
||||
# ADR-457 build-at-publish: TS-generated runtime artifacts (compiled from src/*.cts
|
||||
# by `npm run build:lib`). Source of truth is src/; these are emitted, never edited.
|
||||
# Published via prepublishOnly; built before test via pretest. Grows as modules migrate.
|
||||
/get-shit-done/bin/lib/semver-compare.cjs
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.venv/
|
||||
|
||||
@@ -1,82 +1,172 @@
|
||||
# ADR 457: Collapse hand-written CJS to generated single-source [Proposed]
|
||||
# ADR 457: Generation model for `bin/lib/*.cjs` type safety [Accepted]
|
||||
|
||||
- **Status:** Proposed
|
||||
- **Date:** 2026-05-28
|
||||
- **Status:** Accepted
|
||||
- **Date:** 2026-05-28 (rewritten and accepted 2026-05-31 after correcting fabricated context)
|
||||
|
||||
> **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.
|
||||
> **Accepted direction:** build-at-publish (model 2 below). Implementation is
|
||||
> tracked in a separate migration issue and proceeds module-by-module. The sole
|
||||
> prerequisite — the ESLint harness (ADR 452) — is already **accepted/merged**.
|
||||
>
|
||||
> **Provenance note.** An earlier draft of this ADR (and issue #457) was authored
|
||||
> by an agent and asserted a codebase state that did not exist — "~13 files
|
||||
> generated from `.ts` via `tsc`", `get-shit-done/src/` / `sdk/src/` source trees,
|
||||
> and a `tests/cjs-ts-parity.test.cjs`. None of those existed. This rewrite
|
||||
> grounds the decision in verified ground truth. Do not restore the earlier
|
||||
> "natural completion of the 13 generated files" framing; it was fiction.
|
||||
|
||||
## Context
|
||||
|
||||
### Today's split
|
||||
### What actually exists today (verified 2026-05-31)
|
||||
|
||||
The `get-shit-done/bin/lib/` directory currently holds two kinds of files:
|
||||
- `get-shit-done/bin/lib/` holds **84** `.cjs` files. **Exactly one** carries a
|
||||
`// @generated` header: `package-identity.cjs`.
|
||||
- That one generated file is **not** `tsc` output. It is produced by
|
||||
`scripts/generate-package-identity.cjs` — a plain Node script that reads
|
||||
`package.json` and **bakes literal coordinate values** into a CJS module.
|
||||
- There is **no** `get-shit-done/src/` or `sdk/src/` TypeScript tree. There is
|
||||
**no** TS→CJS transpilation pipeline. There is **no**
|
||||
`tests/cjs-ts-parity.test.cjs`. The only parity test is
|
||||
`tests/issue-498-package-identity.test.cjs`, scoped to the one baked file: it
|
||||
regenerates from `package.json` and asserts the committed output is not stale.
|
||||
- `tsconfig.lint.json` exists with `allowJs` + `checkJs`, but it is **not**
|
||||
wired into `eslint.config.mjs`. The `.cjs` config block (`eslint.config.mjs`
|
||||
lines 61–92) sets only `sourceType`/`globals` — **no `parser`, no
|
||||
`parserOptions.project`, no `projectService`, and no `@typescript-eslint`
|
||||
rule** is enabled. A comment on line 60 nonetheless *claims* "Type-aware via
|
||||
parserOptions.project=tsconfig.lint.json" — so the file is linted **without**
|
||||
type information while the config advertises the opposite.
|
||||
- `eslint.config.mjs` lists 12 files in `GENERATED_CJS_IGNORES` and treats them
|
||||
as generated (never linted) — but those 12 are all **hand-written** (verified:
|
||||
none carry an `@generated` header). This is a latent inconsistency: the lint
|
||||
config already pretends a generation pipeline exists for them.
|
||||
|
||||
- **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.
|
||||
So the real situation is: **83 hand-written `.cjs`, 1 value-baked `.cjs`, and a
|
||||
lint config that already advertises — in a comment and in a 12-file ignore list
|
||||
— a type-aware generation pipeline that was never built.**
|
||||
|
||||
This split creates three structural problems:
|
||||
### Two different things are both called "generation"
|
||||
|
||||
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.
|
||||
This distinction is the crux of the decision, and the earlier draft erased it:
|
||||
|
||||
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.
|
||||
- **Value baking (exists, forced).** `package-identity.cjs` must be generated
|
||||
because the *installed* tree ships a synthetic `{"type":"commonjs"}`
|
||||
`package.json` with no `.name`, so a runtime `require('package.json').name`
|
||||
is `undefined` (bug #378). The values literally cannot be read at runtime;
|
||||
baking them at build time is the only option. **Deletion test:** remove the
|
||||
generator and the complexity reappears across every consumer. It is a deep
|
||||
seam and earns its keep.
|
||||
|
||||
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.
|
||||
- **Transpilation (proposed, optional).** Authoring `bin/lib` logic as TS and
|
||||
emitting `.cjs` via `tsc`. **Deletion test:** remove it and *nothing*
|
||||
reappears — a hand-written `.cjs` and a `tsc`-emitted `.cjs` are behaviorally
|
||||
identical at runtime. The seam buys **no runtime leverage**. Its entire value
|
||||
is **author-time and CI type checking**.
|
||||
|
||||
### Relationship to the retired SDK boundary
|
||||
`package-identity` is therefore **not** precedent for the proposed transpilation
|
||||
work. They are different techniques with different forcing functions.
|
||||
|
||||
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.
|
||||
### The problem actually worth solving
|
||||
|
||||
## Decision [Proposed]
|
||||
Type safety on the hand-written runtime surface is **second-class**: type errors
|
||||
surface (if at all) as lint findings via the un-wired `tsconfig.lint.json`, not
|
||||
as compile errors. Any contributor — human or agent — adding a `bin/lib` file
|
||||
must decide hand-write vs generate, with no enforced answer. That inconsistency
|
||||
is real and grows.
|
||||
|
||||
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.
|
||||
## The decision this ADR must make
|
||||
|
||||
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).
|
||||
Type-checking TS sources is the goal. The load-bearing question the earlier
|
||||
draft skipped is: **do we check the generated `.cjs` into git, or treat it as a
|
||||
build artifact?** Three models:
|
||||
|
||||
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.
|
||||
1. **Check in both `.ts` source and `.cjs` output.** Creates a permanent
|
||||
"two copies must match" invariant, requiring parity tests, dual commits, and
|
||||
a pre-commit/CI drift gate. This is the model the earlier draft assumed —
|
||||
inherited from value-baking, where checking in output is *forced*. For
|
||||
transpilation, nothing forces it, so this imports maximum friction for no
|
||||
runtime gain.
|
||||
|
||||
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.
|
||||
2. **Build at publish (recommended).** `bin/lib/*.cjs` becomes a gitignored
|
||||
build artifact emitted from a TS `src/` tree by `tsc`; npm publishes the
|
||||
built output. **Feasible today:** `package.json` already ships `get-shit-done`
|
||||
and `scripts` via its `files` array, and already runs a pre-publish build
|
||||
step (`"prepublishOnly": "npm run build:hooks"`) — the `.cjs` emit hooks into
|
||||
the same step, and `npm pack` includes on-disk artifacts regardless of
|
||||
`.gitignore`. No drift invariant, no parity test, no dual commits — the
|
||||
"Negative" consequences below mostly evaporate. Cost: contributors run a
|
||||
build to exercise local changes, and CI must build before test.
|
||||
|
||||
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.
|
||||
3. **Build at install.** Rejected: fragile across Node versions and platforms
|
||||
(CONTEXT.md notes Windows / Node 24 hazards) and slows every install.
|
||||
|
||||
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.
|
||||
## Decision [Accepted]
|
||||
|
||||
1. **Pursue type safety via a TS `src/` tree compiled with `tsc`**, adopting
|
||||
**model 2 (build at publish)**: TS source is canonical, `.cjs` is a
|
||||
gitignored artifact. This dissolves the drift-policing machinery rather than
|
||||
building it.
|
||||
2. **Keep value baking separate.** `package-identity.cjs` stays a checked-in
|
||||
baked artifact under its existing generator and parity test; the install-tree
|
||||
#378 constraint is unaffected by this ADR.
|
||||
3. **Migrate incrementally, lowest-coupling module first**, behind one pilot PR
|
||||
that stands up the `src/` tree + build wiring for a single module before any
|
||||
bulk move.
|
||||
4. **Reconcile the lint config to reality first.** The 12-entry
|
||||
`GENERATED_CJS_IGNORES` list currently lies about hand-written files; it must
|
||||
be corrected (those files linted as hand-written) before, not after, a
|
||||
pipeline exists — otherwise the inconsistency masks the migration's progress.
|
||||
5. **Wire type-aware linting** to the real `tsconfig.json` as modules become TS;
|
||||
retire `tsconfig.lint.json` only when the last hand-written `.cjs` is gone.
|
||||
|
||||
## 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.
|
||||
- Type-aware `typescript-eslint` rules apply to migrated runtime code.
|
||||
- No checked-in generated `.cjs`, so **no** drift invariant and **no** parity
|
||||
test to maintain for the transpiled surface (contrast: the rejected model 1).
|
||||
- One enforced answer to "hand-write or generate?" for new `bin/lib` code.
|
||||
|
||||
### 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.
|
||||
- A build step now sits between editing `src/*.ts` and running `bin/lib/*.cjs`.
|
||||
Local dev and CI must build before exercising runtime behavior.
|
||||
- Migration touches ~83 files; each may surface latent type errors to fix.
|
||||
- Tooling that today reads `bin/lib/*.cjs` from a checkout (not an install) must
|
||||
build first or read from `src/`.
|
||||
|
||||
### 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.
|
||||
- Tests importing `bin/lib/*.cjs` keep working **only if** the build has run;
|
||||
the test command must depend on the build. This is the main behavioral change
|
||||
versus today, where the `.cjs` is always present in the tree.
|
||||
|
||||
## 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.
|
||||
- **(a) Keep the split indefinitely** — leaves the type-aware gap and the
|
||||
un-wired `tsconfig.lint.json` permanently. Rejected as a final state.
|
||||
- **(b) Check in `.ts` + generated `.cjs` (model 1)** — imports a drift
|
||||
invariant, parity tests, and dual commits for zero runtime benefit. Rejected
|
||||
in favor of build-at-publish.
|
||||
- **(c) Full ESM rewrite** — breaks `require()` consumers (no `"type":"module"`
|
||||
today); semver-major. Out of scope.
|
||||
- **(d) Wire `tsconfig.lint.json` into ESLint and stop there** — keeps the
|
||||
stopgap permanent and never delivers compile-level (vs lint-level) type
|
||||
errors. Rejected as the final state, but acceptable as an interim while the
|
||||
pilot proves out.
|
||||
|
||||
**(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.
|
||||
## Open questions
|
||||
|
||||
**(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.
|
||||
- Does any consumer rely on `bin/lib/*.cjs` being present in a raw (un-built)
|
||||
checkout? If so, build-at-publish needs a `prepare`-script bridge.
|
||||
- `tsc` CJS interop details (`esModuleInterop`, `__importDefault` shims) for the
|
||||
modules that re-`require` each other.
|
||||
- Whether the pilot should be a leaf utility or one of the 12 mislabeled
|
||||
`GENERATED_CJS_IGNORES` files (which already advertise themselves as generated).
|
||||
|
||||
## References
|
||||
|
||||
- Tracking issue: [#457](https://github.com/open-gsd/get-shit-done-redux/issues/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`
|
||||
- ESLint harness prerequisite (**accepted**): `452-eslint-lint-harness.md`
|
||||
- Test-rigor policies (**accepted**): `456-test-rigor-architecture.md`
|
||||
- Single-runtime collapse (**accepted**): `0174-retire-gsd-sdk-package-boundary.md`
|
||||
- Superseded shared-module seam: `3524-cjs-sdk-hard-seam.md`
|
||||
- Tracking issue: [#457](https://github.com/open-gsd/gsd-core/issues/457)
|
||||
- Value-baking precedent (distinct technique): `scripts/generate-package-identity.cjs`,
|
||||
`tests/issue-498-package-identity.test.cjs`
|
||||
|
||||
@@ -51,10 +51,30 @@ export default tseslint.config(
|
||||
'.claude/**',
|
||||
'coverage/**',
|
||||
'**/*.generated.cjs',
|
||||
// ADR-457: tsc-generated runtime artifact — lint the src/*.cts source, not the emitted .cjs.
|
||||
'get-shit-done/bin/lib/semver-compare.cjs',
|
||||
...GENERATED_CJS_IGNORES,
|
||||
],
|
||||
},
|
||||
|
||||
// ── src/**/*.cts — TypeScript runtime sources (ADR-457 build-at-publish) ─────
|
||||
// First-class type-aware linting on the migrated source. The TS compiler
|
||||
// (`npm run build:lib`, strict + noEmitOnError) is the primary type gate;
|
||||
// these rules add lint-level coverage. warn-first per the harness convention.
|
||||
{
|
||||
files: ['src/**/*.cts'],
|
||||
extends: [tseslint.configs.recommendedTypeChecked],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: './tsconfig.build.json',
|
||||
tsconfigRootDir: __dirname,
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }],
|
||||
},
|
||||
},
|
||||
|
||||
// ── get-shit-done/bin/**/*.cjs + scripts/**/*.cjs ───────────────────────────
|
||||
// CommonJS Node files: js.recommended + eslint-plugin-n + local plugin rules
|
||||
// Type-aware via parserOptions.project=tsconfig.lint.json where applicable
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
function toNumericTuple(input) {
|
||||
const cleaned = String(input == null ? '' : input).trim().replace(/^v/, '');
|
||||
const base = cleaned.replace(/[-+].*$/, '');
|
||||
const parts = base.split('.');
|
||||
const major = Number.parseInt(parts[0], 10) || 0;
|
||||
const minor = Number.parseInt(parts[1], 10) || 0;
|
||||
const patch = Number.parseInt(parts[2], 10) || 0;
|
||||
return [major, minor, patch];
|
||||
}
|
||||
|
||||
function compareSemverCore(a, b) {
|
||||
const [a0, a1, a2] = toNumericTuple(a);
|
||||
const [b0, b1, b2] = toNumericTuple(b);
|
||||
if (a0 !== b0) return a0 > b0 ? 1 : -1;
|
||||
if (a1 !== b1) return a1 > b1 ? 1 : -1;
|
||||
if (a2 !== b2) return a2 > b2 ? 1 : -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
function isSemverNewer(a, b) {
|
||||
return compareSemverCore(a, b) > 0;
|
||||
}
|
||||
|
||||
function isStableTripletSemver(v) {
|
||||
return /^\d+\.\d+\.\d+$/.test(String(v || '').replace(/^v/, ''));
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
compareSemverCore,
|
||||
isSemverNewer,
|
||||
isStableTripletSemver,
|
||||
toNumericTuple,
|
||||
};
|
||||
10
package.json
10
package.json
@@ -70,12 +70,14 @@
|
||||
"check:alias-drift": "node scripts/check-alias-drift.cjs",
|
||||
"check:identity-drift": "node scripts/lint-package-identity-drift.cjs",
|
||||
"check:integrity": "node scripts/check-npm-integrity.cjs",
|
||||
"build": "npm run generate:identity && npm run build:hooks",
|
||||
"build": "npm run generate:identity && npm run build:lib && npm run build:hooks",
|
||||
"build:hooks": "node scripts/build-hooks.js",
|
||||
"build:lib": "tsc -p tsconfig.build.json",
|
||||
"generate:identity": "node scripts/generate-package-identity.cjs",
|
||||
"prepublishOnly": "npm run build:hooks",
|
||||
"pretest": "npm run lint:skill-deps",
|
||||
"pretest:coverage": "npm run lint:skill-deps",
|
||||
"prepack": "npm run build:lib",
|
||||
"prepublishOnly": "npm run build:lib && npm run build:hooks",
|
||||
"pretest": "npm run build:lib && npm run lint:skill-deps",
|
||||
"pretest:coverage": "npm run build:lib && npm run lint:skill-deps",
|
||||
"lint": "eslint . --cache --cache-location node_modules/.cache/eslint/",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"lint:descriptions": "node scripts/lint-descriptions.cjs",
|
||||
|
||||
@@ -42,6 +42,16 @@ const RULES = [
|
||||
'tests/bug-3588-npm-audit-clean.test.cjs',
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'TS runtime sources (ADR-457 build-at-publish)',
|
||||
// src/*.cts compiles into get-shit-done/bin/lib/*.cjs; a source-only edit must
|
||||
// still trigger the migrated module's tests (otherwise CI silently skips them).
|
||||
match: path => path.startsWith('src/') || path === 'tsconfig.build.json',
|
||||
tests: [
|
||||
'tests/semver-compare.test.cjs',
|
||||
'tests/bug-10-semver-policy-consolidation.test.cjs',
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'installer and package layout',
|
||||
match: path => path.startsWith('bin/') ||
|
||||
|
||||
@@ -20,11 +20,32 @@
|
||||
// See docs/TESTING-SUITES.md for full grouping policy.
|
||||
'use strict';
|
||||
|
||||
const { readdirSync } = require('fs');
|
||||
const { readdirSync, existsSync } = require('fs');
|
||||
const { join } = require('path');
|
||||
const { execFileSync } = require('child_process');
|
||||
|
||||
const SUITES = ['all', 'unit', 'integration', 'install', 'security', 'slow'];
|
||||
|
||||
// ADR-457 build-at-publish: get-shit-done/bin/lib/*.cjs is generated from
|
||||
// src/*.cts and gitignored, so on a clean checkout (fresh CI, before any build)
|
||||
// the artifact is absent — yet test files require it. This is the universal
|
||||
// chokepoint every test path funnels through (test:unit, --files-from, direct
|
||||
// invocation), so build the artifact here if missing. It is a no-op once built
|
||||
// (dev, pretest, a prior run in the same job), which keeps the harness test's
|
||||
// spawned invocations side-effect-free. Paths resolve from __dirname (not cwd),
|
||||
// so it works regardless of GSD_TEST_DIR / temp-dir cwd. NOTE: the sentinel is
|
||||
// the pilot module; revisit (or switch to an unconditional quiet build) as more
|
||||
// modules migrate into src/.
|
||||
function ensureBuiltArtifacts() {
|
||||
const root = join(__dirname, '..');
|
||||
const sentinel = join(root, 'get-shit-done', 'bin', 'lib', 'semver-compare.cjs');
|
||||
if (existsSync(sentinel)) return;
|
||||
const tscBin = require.resolve('typescript/bin/tsc');
|
||||
execFileSync(process.execPath, [tscBin, '-p', join(root, 'tsconfig.build.json')], {
|
||||
cwd: root,
|
||||
stdio: 'inherit',
|
||||
});
|
||||
}
|
||||
const MARKED_SUITES = ['integration', 'install', 'security', 'slow'];
|
||||
|
||||
function parseArgs(argv) {
|
||||
@@ -203,6 +224,9 @@ function main() {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Build the gitignored bin/lib artifact if absent, before any test requires it.
|
||||
ensureBuiltArtifacts();
|
||||
|
||||
// Log selected files to stderr for CI / harness-test visibility.
|
||||
// node:test default reporter doesn't echo filenames, so this gives
|
||||
// operators a single stable line they can grep.
|
||||
|
||||
51
src/semver-compare.cts
Normal file
51
src/semver-compare.cts
Normal file
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Shared semver comparison utility (ADR-457 pilot: first hand-written
|
||||
* bin/lib/*.cjs collapsed to a TypeScript source of truth).
|
||||
*
|
||||
* Logic is preserved byte-for-behaviour from the prior hand-written
|
||||
* `get-shit-done/bin/lib/semver-compare.cjs`; only types are added. The
|
||||
* normalization policy here is locked by `tests/semver-compare.test.cjs` and
|
||||
* consumed by update-check, statusline dev-install detection, and changeset
|
||||
* range compare (`scripts/changeset/cli.cjs`).
|
||||
*/
|
||||
|
||||
/** [major, minor, patch] — non-negative integers, never NaN. */
|
||||
export type SemverTuple = [number, number, number];
|
||||
|
||||
/** Comparison result: -1 (a < b), 0 (equal), 1 (a > b). */
|
||||
export type CompareResult = -1 | 0 | 1;
|
||||
|
||||
/**
|
||||
* What callers actually pass: a version string (`"1.2.3"`, `"v1.2.3-rc.1"`), a
|
||||
* bare number, or a missing value. The old hand-written `.cjs` typed these as
|
||||
* `unknown` and leaned on `String()` — which the type-aware lint flagged as an
|
||||
* `[object Object]` hazard. Narrowing to the real domain type is the fix.
|
||||
*/
|
||||
export type VersionInput = string | number | null | undefined;
|
||||
|
||||
export function toNumericTuple(input: VersionInput): SemverTuple {
|
||||
const cleaned = String(input == null ? '' : input).trim().replace(/^v/, '');
|
||||
const base = cleaned.replace(/[-+].*$/, '');
|
||||
const parts = base.split('.');
|
||||
const major = Number.parseInt(parts[0], 10) || 0;
|
||||
const minor = Number.parseInt(parts[1], 10) || 0;
|
||||
const patch = Number.parseInt(parts[2], 10) || 0;
|
||||
return [major, minor, patch];
|
||||
}
|
||||
|
||||
export function compareSemverCore(a: VersionInput, b: VersionInput): CompareResult {
|
||||
const [a0, a1, a2] = toNumericTuple(a);
|
||||
const [b0, b1, b2] = toNumericTuple(b);
|
||||
if (a0 !== b0) return a0 > b0 ? 1 : -1;
|
||||
if (a1 !== b1) return a1 > b1 ? 1 : -1;
|
||||
if (a2 !== b2) return a2 > b2 ? 1 : -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
export function isSemverNewer(a: VersionInput, b: VersionInput): boolean {
|
||||
return compareSemverCore(a, b) > 0;
|
||||
}
|
||||
|
||||
export function isStableTripletSemver(v: VersionInput): boolean {
|
||||
return /^\d+\.\d+\.\d+$/.test(String(v || '').replace(/^v/, ''));
|
||||
}
|
||||
20
tsconfig.build.json
Normal file
20
tsconfig.build.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"//": "ADR-457 build-at-publish: compile TS runtime sources in src/ to gitignored .cjs artifacts under get-shit-done/bin/lib/. Source uses the .cts extension so tsc emits .cjs natively. As modules migrate, they move from hand-written bin/lib/*.cjs into src/*.cts here.",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "get-shit-done/bin/lib",
|
||||
"module": "nodenext",
|
||||
"moduleResolution": "nodenext",
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2022"],
|
||||
"types": [],
|
||||
"strict": true,
|
||||
"declaration": false,
|
||||
"sourceMap": false,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noEmitOnError": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["src/**/*.cts"]
|
||||
}
|
||||
Reference in New Issue
Block a user