Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
640304e
docs(repo): add the plain-ignorer split plan
systemfsoftware-maker Sep 13, 2026
f12cf19
feat(repo): land the plain ignorer contract package and the ignorers …
systemfsoftware-maker Sep 14, 2026
4f3c046
feat(repo): load plain ignorer modules through the engine plugin loader
systemfsoftware-maker Sep 14, 2026
1ae46c5
feat(repo): package the effect-schema-declarations ignorer on the pla…
systemfsoftware-maker Sep 14, 2026
eb7581c
feat(repo): package the in-source-vitest-block and workflow-make-boun…
systemfsoftware-maker Sep 14, 2026
2d84240
refactor(repo): cut over to the ignorer packages and delete stryker-p…
systemfsoftware-maker Sep 14, 2026
a933235
deps(ci): drop three plugin devDependencies the cli never imports
systemfsoftware-maker Sep 14, 2026
3409534
ci(ci): scope the gate to the pull request's affected packages
systemfsoftware-maker Sep 14, 2026
9764694
feat(repo): add private oxlint preset for the ignorer family
systemfsoftware-maker Sep 14, 2026
82b8745
feat(repo): rebuild ignorer contract as the standard-schema interface…
systemfsoftware-maker Sep 14, 2026
bc919c9
feat(repo): decide schema-declaration ignores over standard schema va…
systemfsoftware-maker Sep 14, 2026
54a6fcd
feat(repo): decide in-source test ignores over standard schema valida…
systemfsoftware-maker Sep 14, 2026
01bd207
feat(repo): decide workflow make-boundary ignores over standard schem…
systemfsoftware-maker Sep 14, 2026
bc0808f
docs(repo): drop superseded preset name from interface readme
systemfsoftware-maker Sep 14, 2026
8d7e074
docs(repo): retitle ignorer release intents for the standard-schema r…
systemfsoftware-maker Sep 14, 2026
5fabeb6
docs(repo): supersede the ignorer split plan with the standard-schema…
systemfsoftware-maker Sep 14, 2026
c480005
feat(repo): require a declared standard schema on plain ignorer modules
systemfsoftware-maker Sep 14, 2026
3ec46ed
refactor(repo): drop dead plugin loader declarations
systemfsoftware-maker Sep 14, 2026
ed8ee2c
deps(repo): reconcile lockfile with ignorer manifests
systemfsoftware-maker Sep 14, 2026
3e43505
refactor(repo): read struct fields without Reflect.get
systemfsoftware-maker Sep 14, 2026
341aa16
refactor(repo): drop the declared schema from the plain ignorer contract
systemfsoftware-maker Sep 14, 2026
45af8db
chore(repo): keep the ci gate with its owner and disambiguate a dupli…
systemfsoftware-maker Sep 14, 2026
991d544
docs(repo): record the plain-entry contract learning
systemfsoftware-maker Sep 14, 2026
8d8e9bc
refactor(repo): keep test scaffolding out of the published ignorer so…
systemfsoftware-maker Sep 14, 2026
d6b9541
refactor(repo): publish the AST vocabulary from oxc types, drop schem…
systemfsoftware-maker Sep 14, 2026
97624d8
refactor(repo): type the instrumenter AST on oxc types and rename its…
systemfsoftware-maker Sep 14, 2026
daf7997
refactor(repo): drop the workflow-make-boundary ignorer from this repo
systemfsoftware-maker Sep 14, 2026
3ba5cfe
feat(repo): instrument API Extractor for every subpath export
systemfsoftware-maker Sep 14, 2026
9b50572
fix(repo): keep API reports byte-exact so api:check survives a fresh …
systemfsoftware-maker Sep 14, 2026
408202b
fix(repo): hash the shared toolchain presets as task inputs
systemfsoftware-maker Sep 14, 2026
2c3e6c9
refactor(repo): make the ignorer interface a types-only package
systemfsoftware-maker Sep 14, 2026
9fbc1f0
feat(repo): publish the ignorer family's own lint preset
systemfsoftware-maker Sep 14, 2026
c9f54bc
fix(repo): let API Extractor own the preset declaration's bytes
systemfsoftware-maker Sep 14, 2026
2fbb574
refactor(repo): ancestors-based ignorer paths and oxc-walker traversal
systemfsoftware-maker Sep 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/oxlint-ignorer-config-debut.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@systemfsoftware/oxlint-ignorer-config": minor
---

First release. This is the preset an ignorer package is graded by: it holds a
decision to one path, refuses type assertions and `any` on data read from
outside, and rejects any import that would put the StrykerJS runtime behind an
ignorer — Effect and the family presets included.

It extends nothing, so adopting it is a single `extends: [preset]` entry in
your lint configuration.
10 changes: 10 additions & 0 deletions .changeset/stryker-ignorer-effect-schema-declarations-debut.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"@systemfsoftware/stryker-ignorer-effect-schema-declarations": minor
---

First release. This ignorer keeps mutants inside Effect Schema declarations out of
a mutation run: a symbol, a tagged error, an annotation or a class body only
declares a type, so no run can observe the change.

It has no runtime dependencies, and migrating from the plugin you use today is a
rename in your plugins list — the ignorer keeps the name it already had.
10 changes: 10 additions & 0 deletions .changeset/stryker-ignorer-in-source-vitest-block-debut.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"@systemfsoftware/stryker-ignorer-in-source-vitest-block": minor
---

First release. This ignorer keeps mutants inside a test-only block out of a
mutation run: those statements never reach the production path, so no run can
observe the change.

It has no runtime dependencies, and migrating from the plugin you use today is a
rename in your plugins list — the ignorer keeps the name it already had.
9 changes: 9 additions & 0 deletions .changeset/stryker-ignorer-interface-ancestors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@systemfsoftware/stryker-ignorer-interface": minor
---

An ignorer's path is now `{ node, ancestors }`: the ancestors arrive as data, nearest first, so a
`shouldIgnore` implementation no longer walks a parent chain itself. The parent-path member is gone.

The AST vocabulary is now re-exported from `@oxc-project/types` under that package's own names and
bundled into this package's declarations, so nothing extra needs installing.
11 changes: 11 additions & 0 deletions .changeset/stryker-ignorer-interface-debut.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@systemfsoftware/stryker-ignorer-interface": minor
---

First release. This is the contract an ignorer is written against: the
`PlainIgnorer` descriptor (`{ name, shouldIgnore }`), the structural `NodePath`
an entry is called with, `UnknownNode`, `AstNodeType`, and the canonical AST
node vocabulary, aliased from the parser that produces those nodes.

The package ships no runtime value and no runtime dependencies, so adopting it
costs nothing beyond the types.
5 changes: 5 additions & 0 deletions .changeset/stryker-js-engine-plain-ignorer-loader.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@systemfsoftware/stryker-js-engine": minor
---

the plugin loader accepts modules exporting `strykerIgnorers` (`{ name, shouldIgnore }`, each entry registered as an `Ignore` contribution) — no `effect` peer, no plugin-interface import needed for an ignorer module; a plain entry whose `name` is not a string or whose `shouldIgnore` is not callable is rejected at load with `PluginLoadFailedError` naming the module; native `strykerPlugins` modules load unchanged, and the default `plugins:` preset now names `@systemfsoftware/stryker-ignorer-effect-schema-declarations` and `@systemfsoftware/stryker-ignorer-workflow-make-boundary` instead of the `@systemfsoftware/stryker-plugins` subpaths, so configs using the old names must move to the new packages
5 changes: 0 additions & 5 deletions .changeset/stryker-plugins-home-migration.md

This file was deleted.

8 changes: 8 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,11 @@
*.dll binary
*.so binary
*.dylib binary

# API Extractor writes its reports with CRLF. Keep the bytes it produces: a
# checkout that normalises them to LF makes `api:check` see a spurious diff.
**/etc/*.api.md -text

# The preset's rolled-up declaration is written by API Extractor with CRLF, and
# it is committed (unlike the other packages' dist declarations). Keep its bytes.
packages/toolchain/oxlint-ignorer-config/lib/base.d.ts -text
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ coverage/
.direnv/
result
result-*
temp/
28 changes: 14 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ Code coverage only tells you which lines ran during tests; it cannot tell you if
| **Interrupted Runs** | `Ctrl+C` / cancel yields 0 reports | Emits partial report up to last completed mutant |
| **Incremental State** | `stryker-incremental.json` frequently goes stale | Reliable state validation surviving aborted runs |
| **Agent / CI Mode** | Generic CLI exit codes (`0` or `1`) | Machine mode auto-detection (`AGENT`, `CLAUDECODE`, `CODEX_SANDBOX`) + classified exit codes |
| **Effect-TS Ecosystem** | Equivalent mutant noise on Schema & Brand types | Dedicated `@systemfsoftware/stryker-plugins` ignorers |
| **Effect-TS Ecosystem** | Equivalent mutant noise on Schema & Brand types | Dedicated `@systemfsoftware/stryker-ignorer-*` ignorers |
| **Runtime Engine** | Procedural JavaScript with mutable state | Pure functional Effect 4 architecture with typed errors |

---
Expand Down Expand Up @@ -131,18 +131,18 @@ pnpm exec stryker run

This monorepo publishes a modular ecosystem of packages under the `@systemfsoftware` scope:

| Package | Role |
| ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------ |
| [`@systemfsoftware/stryker-js-cli`](packages/stryker-js-cli) | Terminal & CI runner binary with NDJSON streaming output |
| [`@systemfsoftware/stryker-js-language`](packages/stryker-js-language) | Pure domain core: mutant models, schemas, and exit classifications |
| [`@systemfsoftware/stryker-js-plugin-interface`](packages/stryker-js-plugin-interface) | Shared plugin contracts (`declarePlugin`, `composePlugins`) |
| [`@systemfsoftware/stryker-js-engine`](packages/stryker-js-engine) | Mutation run lifecycle engine and test orchestration |
| [`@systemfsoftware/stryker-js-instrumenter`](packages/stryker-js-instrumenter) | AST mutation engine powered by OXC parser |
| [`@systemfsoftware/stryker-js-vitest-runner`](packages/stryker-js-vitest-runner) | Vitest runner integration for mutation sandboxes |
| [`@systemfsoftware/stryker-js-typescript-checker`](packages/stryker-js-typescript-checker) | TypeScript type-checker plugin validating mutants pre-execution |
| [`@systemfsoftware/stryker-js-html-reporter`](packages/stryker-js-html-reporter) | Interactive HTML mutation report generator |
| [`@systemfsoftware/stryker-plugins`](packages/stryker-plugins) | Domain ignorers for Effect Schema brands & tagged unions |
| [`@systemfsoftware/stryker-test-contribution`](packages/stryker-test-contribution) | Suite hygiene plugin enforcing unique mutant kills per test file |
| Package | Role |
| ------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------- |
| [`@systemfsoftware/stryker-js-cli`](packages/stryker-js-cli) | Terminal & CI runner binary with NDJSON streaming output |
| [`@systemfsoftware/stryker-js-language`](packages/stryker-js-language) | Pure domain core: mutant models, schemas, and exit classifications |
| [`@systemfsoftware/stryker-js-plugin-interface`](packages/stryker-js-plugin-interface) | Shared plugin contracts (`declarePlugin`, `composePlugins`) |
| [`@systemfsoftware/stryker-js-engine`](packages/stryker-js-engine) | Mutation run lifecycle engine and test orchestration |
| [`@systemfsoftware/stryker-js-instrumenter`](packages/stryker-js-instrumenter) | AST mutation engine powered by OXC parser |
| [`@systemfsoftware/stryker-js-vitest-runner`](packages/stryker-js-vitest-runner) | Vitest runner integration for mutation sandboxes |
| [`@systemfsoftware/stryker-js-typescript-checker`](packages/stryker-js-typescript-checker) | TypeScript type-checker plugin validating mutants pre-execution |
| [`@systemfsoftware/stryker-js-html-reporter`](packages/stryker-js-html-reporter) | Interactive HTML mutation report generator |
| [`@systemfsoftware/stryker-ignorer-*`](packages/ignorers) | Decoupled domain ignorers (Schema declarations, vitest blocks, Workflow.make boundaries) |
| [`@systemfsoftware/stryker-test-contribution`](packages/stryker-test-contribution) | Suite hygiene plugin enforcing unique mutant kills per test file |

---

Expand All @@ -158,7 +158,7 @@ This monorepo publishes a modular ecosystem of packages under the `@systemfsoftw
<details>
<summary><strong>Why do mutants survive on my Effect Schema definitions?</strong></summary>

Schema definitions and branded type markers often produce equivalent mutants that cannot be observed or failed at runtime. Install `@systemfsoftware/stryker-plugins` and enable `effect-schema-ignorer` in your `stryker.config.json` to filter them out automatically.
Schema definitions and branded type markers often produce equivalent mutants that cannot be observed or failed at runtime. Install `@systemfsoftware/stryker-ignorer-effect-schema-declarations` and enable `effect-schema-declarations` in your `stryker.config.json` to filter them out automatically.

</details>

Expand Down
60 changes: 60 additions & 0 deletions docs/plans/2026-09-14-0520-refactor-ignorer-plain-entry-plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
---
title: Ignorer Plain Entry - Corrected Contract
type: refactor
date: 2026-09-14
artifact_contract: ce-unified-plan/v1
artifact_readiness: implementation-ready
execution: code
supersedes: docs/plans/2026-09-14-0150-refactor-ignorer-standard-schema-plan.md
---

# Ignorer Plain Entry - Corrected Contract

## Goal Capsule

**Objective.** A plain ignorer module exports `strykerIgnorers: PlainIgnorer[]` where
`PlainIgnorer = { name, shouldIgnore(path: NodePath): string | undefined }`. The host
registers each entry as an `Ignore` contribution and fails the load by name when an entry's
`name` is not a string or its `shouldIgnore` is not callable.

**Correction.** The earlier plan required a `schema` member on every entry and had the engine
re-declare that member's shape in `effect/Schema`. Both are removed at the user's direction:
the descriptor carries no schema, and no shape is mirrored across the boundary.

**Authority.** User direction in session (2026-09-14), then `AGENTS.md` and `CONSTITUTION.md`.

**Verification.** `pnpm check:ci`; each package's `pnpm typecheck`, `pnpm run build`,
`pnpm exec vitest run`, `pnpm run lint`; `attw` per package; engine loader suite proves the
rejection path.

## Requirements

- R1. `PlainIgnorer` is `{ name, shouldIgnore }`; the interface publishes no descriptor schema.
- R2. The engine validates exactly that shape (`S.Struct({ name: S.String, shouldIgnore: S.declare(isShouldIgnore) })`)
and registers entries as `Ignore` contributions.
- R3. Standard Schema types come from `@standard-schema/spec` (jsr, catalog-pinned), imported
as a devDependency and inlined into the published declarations — no runtime dependency, no
hand-copied spec source.
- R4. The interface keeps the AST vocabulary, `NodePath`, `ancestorsOf`, and the toolkit as the
ignorers' internal implementation.
- R5. Docs, changesets, and the carried case tables state this contract.

## Unit Map

| Unit | Change | Verification |
| ---- | ----------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
| C1 | Interface: drop `PlainIgnorerSchema` and the `schema` field; import spec types from the package | interface typecheck/build/tests/lint; `dist/*.d.ts` free of `standard-schema/spec` |
| C2 | Two ignorers: entries are `{ name, shouldIgnore }` | each package's suite (48/14 cases) |
| C3 | Engine: `PlainIgnorerSchema` = name + callable `shouldIgnore`; fixture set re-aimed | engine suite; the invalid-entry fixture fails the load by name |
| C4 | Docs/changesets | prose states the two-field contract |

## Destructive Review

**Assumptions.** (1) A host can meaningfully validate an ignorer without a declared schema;
(2) the interface's AST types suffice for authoring; (3) the toolkit remains worth shipping
internally. **Lens (inversion).** How would this be gamed? An entry could ignore nothing and
load green — accepted: ignoring nothing is visible as an unmutated run, and the case tables
pin each ignorer's real decisions. **Radical alternative.** Have the engine pass its own
Effect schema converted with `Schema.toStandardSchemaV1`. Rejected: it makes the host dictate
the input contract and re-couples authors to the host's toolkit. **Reconciliation.** No delta;
the corrected contract stands.
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
---
title: A boundary contract needs the function it calls, not a schema it cannot use
date: 2026-09-14
category: tooling-decisions
module: ignorer-family
problem_type: tooling_decision
component: tooling
severity: medium
applies_when:
- "Defining the descriptor a host loads from a plugin module"
- "Tempted to re-declare a foreign spec's types so a package can stay dependency-free"
- "Debating whether a loaded entry should carry a validator the host already calls"
tags: [standard-schema, boundary, plugin-contract, inlining, types]
---

# A boundary contract needs the function it calls, not a schema it cannot use

## Context

The plain-ignorer family had the host decode every loaded entry against a `schema` member the
ignorer declared, described as a Standard Schema validator (KTD1/KTD5 of the superseded plan).
Two costs surfaced in review:

- the host re-declared the member's shape in its own toolkit (`effect/Schema`), so the same
contract lived twice and could drift;
- the spec's types were hand-copied into the interface package, which then had to stay in step
with `@standard-schema/spec` by hand.

The host never used that validator for anything the decision function did not already do: a
loaded entry is registered as an `Ignore` contribution and `shouldIgnore(path)` is called.

## Guidance

Ship the smallest contract the host actually consumes — `{ name, shouldIgnore }` — and let the
package that owns the AST supply its types:

```ts
export interface PlainIgnorer {
readonly name: string
shouldIgnore(path: NodePath): string | undefined
}
```

The host then owns its own load check and mirrors nothing:

```ts
export const PlainIgnorerSchema = S.Struct({
name: S.String,
shouldIgnore: S.declare(isShouldIgnore),
})
```

Take the spec's types from the spec package, never from a hand copy, and inline them so the
published declarations stay dependency-free: `@standard-schema/spec` (jsr, catalog-pinned) as a
devDependency, resolved into the package's emitted declarations by the build (build output, not
tracked). Verified after `pnpm run build` in `packages/ignorers/interface` by reading its emitted
declarations — no `@standard-schema/spec` reference survives there.

The corollary for a host that wants a validator: convert the host's own schema where it needs
one (`Schema.toStandardSchemaV1`), rather than pushing a validator requirement onto the plugin
author.

## Applicability

Applies to any host/plugin boundary where the host validates an entry and then calls a function
on it. Where the host genuinely decides on the declared validator (filtering, generating, or
composing from it), keep the member — and keep it sourced from the spec package, inlined.
4 changes: 3 additions & 1 deletion dprint.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@
"**/dist",
"**/*-lock.json",
"pnpm-lock.yaml",
"**/etc/*.api.md",
"coverage/**",
"**/snapshots/*.md",
"repos/**",
"packages/stryker-js-typescript-checker/testResources/**"
"packages/stryker-js-typescript-checker/testResources/**",
"packages/toolchain/oxlint-ignorer-config/lib/base.d.ts"
],
"plugins": [
"https://plugins.dprint.dev/typescript-0.96.1.wasm",
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@
"clean": "turbo clean",
"format": "./bin/dprint fmt",
"format:check": "./bin/dprint check",
"gate:dist": "turbo --concurrency=${TURBO_CONCURRENCY:-100%} build",
"gate:tasks": "turbo --concurrency=${TURBO_CONCURRENCY:-50%} --continue lint typecheck typecheck:node test",
"gate:dist": "turbo --concurrency=${TURBO_CONCURRENCY:-100%} build ${TURBO_BASE:+--filter=...[$TURBO_BASE]}",
"gate:tasks": "turbo --concurrency=${TURBO_CONCURRENCY:-50%} --continue lint typecheck test ${TURBO_BASE:+--filter=...[$TURBO_BASE]}",
"lint": "turbo --concurrency=${TURBO_CONCURRENCY:-50%} lint",
"precommit": "pnpm exec lint-staged --no-revert --config .lintstagedrc.js",
"prepare": "husky",
Expand Down
4 changes: 4 additions & 0 deletions packages/ignorers/effect-schema-declarations/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
dist
temp
coverage
reports
Loading