Skip to content
Merged
5 changes: 5 additions & 0 deletions .changeset/immutable-project.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@systemfsoftware/stryker-js": major
---

`Engine.Project` is now read-only. Its `files` and `filesToMutate` are `ReadonlyMap<string, ProjectFile>` instead of `MutableHashMap`, so read them with `.get`, `.keys()`, `.values()` and `.size`.
5 changes: 5 additions & 0 deletions .changeset/instrument-mutate-description-schema.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@systemfsoftware/stryker-js-instrumenter": minor
---

`Instrument.MutateDescriptionSchema` is exported, so a consumer can declare a file's `mutate` field with the same schema the instrumenter uses.
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
---
title: Decompose the Grab-Bag Modules - Plan
type: refactor
date: 2026-10-09
date: 2026-10-10
supersedes: docs/plans/2026-10-09-1840-refactor-grab-bag-decomposition-plan.md
topic: grab-bag-decomposition
artifact_contract: ce-unified-plan/v1
product_contract_source: ce-brainstorm
Expand Down Expand Up @@ -209,7 +210,7 @@ Entries 1-11 are planned in full as U1-U11 below. Entries 12-31 keep this table;
- **KTD1. A pure operation two modules share lives in the schema file of the type it operates on, and both import it.** Checker's `commandFailed` and `plansByIdOf`, for example, go into `Checker/Checker.schema.ts`, which `check.cell.ts` and `group.cell.ts` import. A shared helper whose signature names no type the series declares gets that type, declared in the schema file of its domain; it is never copied. A decision that chooses an outcome stays a workflow (Key Decision on pure operations, ADR-0002). Governs R5.
- **KTD2. A workflow may run inside a loop by being called from a cell's read or write.** A per-file or per-directory decision (tsconfig rewrite, directory classification, candidate files) is called from the cell that walks, not wrapped in a cell per item (P16).
- **KTD3. When several I/O steps are gated by flags, one `plan-*` workflow decides which steps run, and the cell that owns the steps acts on it.** `plan-sandbox-acquisition` (U3) and `plan-report-persistence` (U8) follow this. One table-driven decision keeps the gates in the mutation population without a workflow per boolean.
- **KTD4. R6 is enforced by type for direct writes.** `Project.files` and `Project.filesToMutate` become immutable `HashMap.HashMap` (`Project.schema.ts:17-18`), `TsConfigSchema` loses `S.mutableKey`/`S.mutable`, and `FilePreprocessor` returns a `Project`. A property assignment or `MutableHashMap.set` on a caller's record then fails to compile (harness rule OP7). `Object.assign` still compiles over `readonly` fields, so U2 removes the one at `Sandbox.blueprint.ts:149` and its verification greps for any other; no new lint or CI check is added (R17). The cost is a wide U2 diff over every `MutableHashMap` read of a `Project`. Governs R6.
- **KTD4. R6 is enforced by type for direct writes.** `Project.files` and `Project.filesToMutate` become `ReadonlyMap<string, ProjectFile>` (`Project.schema.ts:17-18`), `TsConfigSchema` loses `S.mutableKey`/`S.mutable`, and `FilePreprocessor` returns a `Project`. Every operation that changes a `Project` builds fresh maps; the merge of preprocessed files is `withPreprocessedFiles` in `Project.schema.ts`, a pure operation over the type in its schema (ADR-0002), so no `merge-preprocessed-files.workflow.ts` exists. A property assignment or `.set` on a caller's record then fails to compile (harness rule OP7). `ReadonlyMap` is a type-level guarantee: a holder of the concrete `Map` could still mutate it, so the `withPreprocessedFiles` law asserts the caller's maps are unchanged and the returned maps are new objects. `Object.assign` still compiles over `readonly` fields, so U2 removes the one at `Sandbox.blueprint.ts:149` and its verification greps for any other; no new lint or CI check is added (R17). Governs R6.
- **KTD5. The reporter registry is one typed list in the `Reporter` layer.** Each entry is a `BuiltinReporter` declaring `name`, `stdout` and a factory built from the reporter's own cell (today's `builtin` record at `reporter.service.ts:51-58`). Stdout membership reaches `planReporters` as command data, not a schema literal. The two protocol roles stay schema literals: `progress-stream` (the machine stream) and `clear-text` (its human substitute). Each reporter cell builds its own `ReporterFailed` mapper from its name, so `reporter-failures.ts` goes. Governs R11, R12.
- **KTD6. `html` stays host-injected** through `RunEnvironment.builtinReporters` (`bin/main.ts:221`, `Serve/Serve.cell.ts:116`, `run/run-stages.ts:24`) and is not a stdout reporter. The registry admits host entries alongside its own. Governs R13.
- **KTD7. Public API breaks are taken with major changesets** (BREAK-1): `Checker.checkGroupedPlans` becomes `Checker.checkGroupedCell` (U1), and `ImportClosure.analyzeImportClosure` becomes the closure cell (U5). Each regenerates `packages/stryker-js/etc/stryker-js.api.md`.
Expand Down Expand Up @@ -321,28 +322,30 @@ Paths are under `packages/stryker-js/` unless stated. Every unit runs `pnpm lint

**Files:**

- Modify `src/Project.schema.ts` (`files` and `filesToMutate` become `HashMap.HashMap<string, ProjectFile>`; add a `ProjectFile` schema for workflow commands), `src/read-project.cell.ts:428-449`, `src/run/instrument.ts:59-67,155-158,184-207`, `src/Sandbox.blueprint.ts:40-318`, `src/Sandbox.schema.ts:14-35`, the `Project` readers in P15 (`src/mutation-reporting.service.ts:271-280`, `src/run/dry-run-choice.ts`, `src/run/dry-run.cell.ts`, `src/run/incremental-reuse.cell.ts`, `src/run/mutation-test-plan.cell.ts`, `src/run/prepare.ts`), `etc/stryker-js.api.md` (`Engine.Project` and `Engine.ProjectFile` change, `etc/stryker-js.api.md:19`), and a `.changeset` entry (major).
- Create `src/merge-preprocessed-files.workflow.ts`, `src/rewrite-sandbox-tsconfig.workflow.ts`, `src/disable-type-checks.cell.ts`, `src/sandbox-tsconfig.cell.ts`, `src/__tests__/merge-preprocessed-files.workflow.property.test.ts`, `src/__tests__/rewrite-sandbox-tsconfig.workflow.property.test.ts`.
- Modify `src/Project.schema.ts` (`files` and `filesToMutate` become `ReadonlyMap<string, ProjectFile>`; a production `ProjectFile` schema; `withPreprocessedFiles` with its laws), `src/read-project.cell.ts:428-449`, `src/run/instrument.ts:59-67,155-158,184-207`, `src/Sandbox.blueprint.ts:40-318` (the disableTypeChecks preprocessor and the tsconfig follow walk stay here until U3 splits the blueprint), `src/Sandbox.schema.ts:14-35` (with laws for `referencedEntriesOf` and `entriesOf`), the `Project` readers in P15 (`src/mutation-reporting.service.ts:271-280`, `src/run/dry-run-choice.ts`, `src/run/dry-run.cell.ts`, `src/run/incremental-reuse.cell.ts`, `src/run/mutation-test-plan.cell.ts`, `src/run/prepare.ts`), `etc/stryker-js.api.md` (`Engine.Project` and `Engine.ProjectFile` change, `etc/stryker-js.api.md:19`), and a `.changeset` entry (major).
- Create `src/rewrite-sandbox-tsconfig.workflow.ts`, `src/sandbox-tsconfig.cell.ts`, `src/__tests__/rewrite-sandbox-tsconfig.workflow.property.test.ts`, `tests/sandbox-tsconfig.integration.test.ts`.

**Approach:**

- `FilePreprocessor` takes and returns a `Project`; `combinePreprocessors` folds instead of looping for effect. `acquireSandbox` writes the folded project and hands nothing back to its caller (P15).
- `mergePreprocessedFiles` decides, from the project's file list, its mutate set and the updated files, the new `files` and `filesToMutate`: an updated file replaces its entry in `files`, and in `filesToMutate` only when it was already there (`Sandbox.blueprint.ts:47-53`). `disable-type-checks.cell.ts` reads the matched files and runs `Instrument.disableTypeChecks` (read), decides with the merge, and returns the new `Project` (write).
- `rewriteSandboxTsconfig` decides one tsconfig's rewrite. Its command carries the decoded config, `basePath`, and every reference the read resolved to an absolute path (resolution needs `Path.Path`, so it stays on the read side). The workflow decides which references escape `basePath` from those resolved paths, with the test the rewriter applies today (`:295-308`), so the escape classification is a mutated decision. The decision is the rewritten config plus the extends entries and project references to follow, with `/tsconfig.json` appended to a reference unless it ends in `.json` (`Sandbox.blueprint.ts:246-250`). Escaping references become `../../` plus the canonical name (`:295-308`). `sandbox-tsconfig.cell.ts` reads and parses the file, decides, writes a new `ProjectFile`, and recurses into the follow list from its write (KTD2). A parse failure logs the same warning as `:166-168`.
- `withPreprocessedFiles` (`Project.schema.ts`) returns a `Project` with fresh maps: an updated file replaces its entry in `files`, and in `filesToMutate` only when it was already there (`Sandbox.blueprint.ts:47-53`); an update whose name `files` lacks is added to `files` only, as `MutableHashMap.set` did. The disableTypeChecks preprocessor reads the matched files, runs `Instrument.disableTypeChecks`, and returns `withPreprocessedFiles(project, updates)`.
- `rewriteSandboxTsconfig` decides one tsconfig's rewrite. Its command carries the decoded config and, for every referenced entry, its path relative to `basePath` (resolution needs `Path.Path`, so it stays on the cell's read). The workflow decides which entries escape (`startsWith('..')`, the test at `:295-308`) and rewrites each escaping string in `include`, `exclude`, `files`, `extends` and `references[].path` to `../../` plus its canonical name. The decision is the rewritten config plus the follow list: the non-escaping extends entries and references, with `/tsconfig.json` appended to a reference unless it ends in `.json` (`Sandbox.blueprint.ts:246-250`). `sandbox-tsconfig.cell.ts` rewrites one tsconfig: it reads and parses the file, decides, and returns the rewritten `ProjectFile` and the resolved follow list; a parse failure logs the warning from `:166-168` and returns nothing to follow. The blueprint walks the follow list depth-first and rewrites followed files before the parent's write, the pre-refactor order; a cell never runs another cell.
- The `&&` and ternary helpers moved into workflows are restated with `Option`/`Match` to meet complexity 1 (P2).
- Operations over `TSConfig` and `ProjectFile` that a cell and a workflow both use go in `Sandbox.schema.ts` and `Project.schema.ts` (KTD1); none is copied.

**Patterns to follow:** `src/run/instrument.ts:59-67` (copy and return); `src/keep-temp-dir.workflow.ts` (a small decision with a Schema command).

**Test scenarios:**

- `mergePreprocessedFiles` (AE2): for a drawn project and drawn updates naming existing files, each updated name maps to its update in `files`; it is in `filesToMutate` iff it was before; every other entry is identical; no name is added. Fails if an update is added to `filesToMutate` unconditionally (the `onNone` arm at `:50`).
- `withPreprocessedFiles` (AE2, in-source law in `Project.schema.ts`): for a drawn project and drawn updates, including names `files` lacks, the caller's maps keep the entries snapshotted before the call, the returned maps are new objects, each updated name maps to its last update in `files`, it is in `filesToMutate` iff it was before, and every other entry is identical. Fails on an in-place merge, or if an update is added to `filesToMutate` unconditionally (the `onNone` arm at `:50`).
- `referencedEntriesOf` and `entriesOf` (in-source laws in `Sandbox.schema.ts`): the referenced entries are `include`, `exclude`, `files`, `extends` and reference paths in that order, an absent array contributing nothing.
- `rewriteSandboxTsconfig`, escape law: the draw holds resolved reference paths both inside and outside a drawn `basePath`. A string in `include`, `exclude`, `files`, `extends` or `references[].path` becomes `../../` plus its canonical name iff its resolved path escapes `basePath`, and every other string is unchanged. Fails if `files` is skipped, a non-escaping entry is rewritten, or an escaping one is kept.
- `rewriteSandboxTsconfig`, shape law: keys outside those five round-trip unchanged (the index signature at `Sandbox.schema.ts:14-19`), and an array holding a non-string is left as it was (`:119-120`). Fails if `compilerOptions` is dropped.
- `rewriteSandboxTsconfig`, follow law: the follow list is exactly the non-escaping extends entries and the non-escaping references, with the `.json` rule applied. Fails if an escaping entry is followed or the `.json` test is inverted. Targets the survivors `5d7353b99a24bd51` and `0e47ce21d936380f` (`Sandbox.schema.ts:26,34`), unless the schema edit removes those lines, in which case the PR reports them as deleted.
- Integration, unchanged: `tests/instrument-stage.integration.test.ts`, `tests/framework-run.integration.test.ts` (disableTypeChecks fixtures), `tests/compile-error-reuse.integration.test.ts` (tsconfig, `inPlace`), `tests/clean-temp-dir.integration.test.ts`, `tests/plan-without-sandbox.integration.test.ts`, and the e2e enterprise-monorepo lane (`tsconfigFile: 'tsconfig.json'`). No integration suite asserts a rewritten tsconfig directly, so the follow and escape laws are the primary guard.
- Boundary, new: `tests/sandbox-tsconfig.integration.test.ts` runs mutation runs in-process through the published `Engine.strykerCell` (the test spawns nothing; the engine starts its own vm-runner worker, as in every existing run suite) on a real temp project whose tsconfig extends and references files inside and outside it. It asserts the exact text written into the sandbox for the root and followed tsconfigs, the missing-file skip, the unparsable-file warning, and that an in-place run leaves every tsconfig unrewritten. Tests may not import `src/` (test-discipline), so the engine is the narrowest surface that reaches the cell. The schema-derived command generator emits `TsconfigMissing`, `TsconfigUnparsable` and `TsconfigParsed`, so the property test covers all three arms.
- Integration, unchanged: `tests/instrument-stage.integration.test.ts`, `tests/framework-run.integration.test.ts` (disableTypeChecks fixtures), `tests/compile-error-reuse.integration.test.ts` (tsconfig, `inPlace`), `tests/clean-temp-dir.integration.test.ts`, `tests/plan-without-sandbox.integration.test.ts`, and the e2e enterprise-monorepo lane (`tsconfigFile: 'tsconfig.json'`).

**Verification:** no `MutableHashMap` in `Project.schema.ts`; no `Object.assign` or property assignment on a `Project`, `ProjectFile` or decoded `TSConfig` in `src/`.
**Verification:** no `MutableHashMap` in `Project.schema.ts`; no `Object.assign` or property assignment on a `Project`, `ProjectFile` or decoded `TSConfig` in `src/`. `tests/sandbox-tsconfig.integration.test.ts` runs real mutation runs (vm runner) on a temp project and asserts the sandbox tsconfig text, the missing-file skip, the unparsable warning and the in-place no-rewrite; it passes unchanged against `origin/main`.

### U3. Sandbox acquisition cells (PR 3)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export namespace Format {

// @public (undocumented)
export namespace Instrument {
export { File, FileDescription, FileDescriptions, InstrumentError, InstrumentFileSkip, InstrumentResult, InstrumenterContext, InstrumenterOptions, MutateDescription, ParserOptions, disableTypeChecks, instrument };
export { File, FileDescription, FileDescriptions, InstrumentError, InstrumentFileSkip, InstrumentResult, InstrumenterContext, InstrumenterOptions, MutateDescription, MutateDescriptionSchema, ParserOptions, disableTypeChecks, instrument };
}

// @public (undocumented)
Expand Down
1 change: 1 addition & 0 deletions packages/stryker-js-instrumenter/src/Instrument/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export {
type FileDescriptions,
InstrumentFileSkip,
type MutateDescription,
MutateDescriptionSchema,
} from '../Instrument.schema.js'
export { disableTypeChecks, instrument } from '../Instrument.service.js'
export type { File, InstrumenterOptions, InstrumentResult } from '../Instrument.service.js'
Expand Down
Loading
Loading