Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
1ab117c
feat(repo): publish a dry-run-only run's coverage into the incrementa…
systemfsoftware-maker Oct 7, 2026
8fcfe38
feat(repo): reuse CompileError verdicts across incremental runs
systemfsoftware-maker Oct 7, 2026
003aee5
fix(repo): keep the reuse stream line decodable without the new count
systemfsoftware-maker Oct 7, 2026
b6031b2
chore(repo): keep the api report CI renders for the Plugin namespace
systemfsoftware-maker Oct 7, 2026
f618d77
docs(repo): mark the checker digest RPC as a breaking change
systemfsoftware-maker Oct 7, 2026
80ac9c8
Merge branch 'stryker/dry-run-only-coverage' into stryker/compile-err…
systemfsoftware-maker Oct 7, 2026
258edf7
docs(solutions): record the Plugin namespace Node alias split between…
systemfsoftware-maker Oct 7, 2026
acdcb34
fix(repo): hash the tsconfig extends chain into the program digest
systemfsoftware-maker Oct 7, 2026
7189468
fix(repo): key the program digest on project-relative paths
systemfsoftware-maker Oct 7, 2026
bd0c88b
fix(repo): canonicalize checker options for the digest and fail closed
systemfsoftware-maker Oct 7, 2026
67cc1da
fix(repo): name a failed closure analysis in the reuse refusal reason
systemfsoftware-maker Oct 7, 2026
0a76c3f
refactor(repo): consolidate the reuse key and version helpers
systemfsoftware-maker Oct 7, 2026
b1efbd7
test(repo): pin the new closure-analysis refusal count
systemfsoftware-maker Oct 7, 2026
a677aed
chore(repo): regenerate the stream contract with the refusal count
systemfsoftware-maker Oct 7, 2026
9211add
refactor(repo): satisfy the cell and effect lint rules
systemfsoftware-maker Oct 7, 2026
d691cff
Merge branch 'stryker/dry-run-only-coverage' into stryker/compile-err…
systemfsoftware-maker Oct 7, 2026
137563a
build(release): name the changesets versioning strategy the release t…
systemfsoftware-maker Oct 7, 2026
d82f758
Merge branch 'stryker/dry-run-only-coverage' into stryker/compile-err…
systemfsoftware-maker Oct 7, 2026
d11fda6
Merge remote-tracking branch 'origin/main' into stryker/compile-error…
systemfsoftware-maker Oct 7, 2026
ef64cb5
ci(repo): rerun CI after two e2e lanes hit the lane limit on slow run…
systemfsoftware-maker Oct 7, 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
17 changes: 17 additions & 0 deletions .changeset/checker-reports-program-digest.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
"@systemfsoftware/stryker-js-plugin-interface": major
"@systemfsoftware/stryker-js-typescript-checker": minor
---

A checker plugin must now report the program it loaded. `CheckerService` gains a
`digest` holding the digest of that program, the checker RPC exposes it as
`digest`, and a TypeScript checker answers with the digest of the TypeScript
program it built: the hashed content of every source file the program loaded
(project files, declaration files and the ones TypeScript ships), the tsconfig
files it was built from, the TypeScript version, the checker's own version, and
its configured options. The digest is stable for an unchanged program and moves
when any of those change.

Breaking for checker plugin authors: a checker worker that does not answer the
`digest` RPC no longer works with the engine. Add a `digest` handler that
returns the digest of everything your check reads.
17 changes: 17 additions & 0 deletions .changeset/compile-error-verdicts-reusable.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
"@systemfsoftware/stryker-js": major
"@systemfsoftware/stryker-js-cli-contract": minor
---

An incremental run now reuses `CompileError` verdicts instead of type-checking
every one again. A verdict is kept only while nothing that fed the check
changed: the mutant and its replacement, every file the checker's TypeScript
program loaded (including transitive and bundled declaration files), every
tsconfig in the `extends` chain and its project references, the TypeScript
version, the checker version and its options. Otherwise the mutant is
re-checked. The reuse stream line reports such refusals as `programChanged`
(or `closureAnalysisFailed` when the closure analysis failed); lines without a
count still decode.

Breaking: a configured checker must answer the new `digest` RPC, so upgrade
checker plugins together with this release.
28 changes: 28 additions & 0 deletions packages/stryker-js-cli-contract/contract/span-taxonomy.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,15 @@
"name": "stryker.checker.check_plans",
"attributes": {}
},
{
"id": "stryker.checker.digest",
"name": "stryker.checker.digest",
"attributes": {
"stryker.checker.name": {
"type": "string"
}
}
},
{
"id": "stryker.checker.group",
"name": "stryker.checker.group",
Expand Down Expand Up @@ -43,6 +52,11 @@
"name": "stryker.checker_pool.check_plans",
"attributes": {}
},
{
"id": "stryker.checker_pool.program_digest",
"name": "stryker.checker_pool.program_digest",
"attributes": {}
},
{
"id": "stryker.checker_pool.split_checked",
"name": "stryker.checker_pool.split_checked",
Expand Down Expand Up @@ -657,6 +671,15 @@
"name": "RpcClient.onEventBatch",
"attributes": {}
},
{
"id": "rpc.digest",
"name": "rpc.digest",
"attributes": {
"rpc.method": {
"type": "string"
}
}
},
{
"id": "rpc.dryRun",
"name": "rpc.dryRun",
Expand Down Expand Up @@ -1059,6 +1082,11 @@
"name": "typescript-checker.compiler.nodes",
"attributes": {}
},
{
"id": "typescript-checker.compiler.programDigest",
"name": "typescript-checker.compiler.programDigest",
"attributes": {}
},
{
"id": "typescript-checker.compiler.scoped",
"name": "typescript-checker.compiler.scoped",
Expand Down
16 changes: 16 additions & 0 deletions packages/stryker-js-cli-contract/contract/stream.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,14 @@
"type": "integer",
"minimum": 0
},
"closureAnalysisFailed": {
"type": "integer",
"minimum": 0
},
"programChanged": {
"type": "integer",
"minimum": 0
},
"timeoutUnreproduced": {
"type": "integer",
"minimum": 0
Expand Down Expand Up @@ -776,6 +784,14 @@
"type": "integer",
"minimum": 0
},
"closureAnalysisFailed": {
"type": "integer",
"minimum": 0
},
"programChanged": {
"type": "integer",
"minimum": 0
},
"timeoutUnreproduced": {
"type": "integer",
"minimum": 0
Expand Down
11 changes: 11 additions & 0 deletions packages/stryker-js-cli-contract/src/SpanTaxonomy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,18 @@ export const Spans = {
attributes: { 'stryker.checker.name': SpanAttributeText, 'stryker.mutants.count': SpanAttributeCount },
},
checkerCheckPlans: { name: 'stryker.checker.check_plans', attributes: {} },
checkerDigest: {
name: 'stryker.checker.digest',
attributes: { 'stryker.checker.name': SpanAttributeText },
},
checkerGroup: {
name: 'stryker.checker.group',
attributes: { 'stryker.checker.name': SpanAttributeText, 'stryker.mutants.count': SpanAttributeCount },
},
checkerGroupPlans: { name: 'stryker.checker.group_plans', attributes: {} },
checkerLogSkipped: { name: 'stryker.checker.log_skipped', attributes: {} },
checkerPoolCheckPlans: { name: 'stryker.checker_pool.check_plans', attributes: {} },
checkerPoolProgramDigest: { name: 'stryker.checker_pool.program_digest', attributes: {} },
checkerPoolSplitChecked: { name: 'stryker.checker_pool.split_checked', attributes: {} },
checkerRecordSkipped: { name: 'stryker.checker.record_skipped', attributes: {} },
cliRun: {
Expand Down Expand Up @@ -159,6 +164,7 @@ export const Spans = {
rpcClientInit: { name: 'RpcClient.init', attributes: {} },
rpcClientMutantRun: { name: 'RpcClient.mutantRun', attributes: {} },
rpcClientOnEventBatch: { name: 'RpcClient.onEventBatch', attributes: {} },
rpcDigest: { name: 'rpc.digest', attributes: { 'rpc.method': SpanAttributeText } },
rpcDryRun: { name: 'rpc.dryRun', attributes: { 'rpc.method': SpanAttributeText } },
rpcFlush: { name: 'rpc.flush', attributes: { 'rpc.method': SpanAttributeText } },
rpcGroup: { name: 'rpc.group', attributes: { 'rpc.method': SpanAttributeText } },
Expand Down Expand Up @@ -240,6 +246,10 @@ export const Spans = {
typescriptCheckerCompilerLineAndCharacter: { name: 'typescript-checker.compiler.lineAndCharacter', attributes: {} },
typescriptCheckerCompilerMake: { name: 'typescript-checker.compiler.make', attributes: {} },
typescriptCheckerCompilerNodes: { name: 'typescript-checker.compiler.nodes', attributes: {} },
typescriptCheckerCompilerProgramDigest: {
name: 'typescript-checker.compiler.programDigest',
attributes: {},
},
typescriptCheckerCompilerScoped: { name: 'typescript-checker.compiler.scoped', attributes: {} },
typescriptCheckerRuntimeMakeChecker: { name: 'typescript-checker.runtime.makeChecker', attributes: {} },
typescriptCheckerSoloRound: {
Expand Down Expand Up @@ -274,6 +284,7 @@ export const spanMembers: ReadonlyArray<SpanMember> = Object.values(Spans)
const rpcServedSpans: ReadonlyMap<string, SpanMember> = new Map<string, SpanMember>([
['capabilities', Spans.rpcCapabilities],
['check', Spans.rpcCheck],
['digest', Spans.rpcDigest],
['dryRun', Spans.rpcDryRun],
['flush', Spans.rpcFlush],
['group', Spans.rpcGroup],
Expand Down
2 changes: 2 additions & 0 deletions packages/stryker-js-cli-contract/src/run-event.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ export const ReuseRefusals = S.Struct({
policyChanged: Report.NonNegativeInt,
runInputsChanged: Report.NonNegativeInt,
closureChanged: Report.NonNegativeInt,
closureAnalysisFailed: S.optionalKey(Report.NonNegativeInt),
programChanged: S.optionalKey(Report.NonNegativeInt),
timeoutUnreproduced: Report.NonNegativeInt,
flakyDependency: Report.NonNegativeInt,
noPriorRecord: Report.NonNegativeInt,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

// @public (undocumented)
export namespace Checker {
export { CheckResult, CheckResultSchema, CheckStatus, Checker, CheckerFailed, CheckerMutantWire, CheckerService, FailedCheckResult, IgnoredCheckResult, PassedCheckResult };
export { CheckResult, CheckResultSchema, CheckStatus, Checker, CheckerFailed, CheckerMutantWire, CheckerService, FailedCheckResult, IgnoredCheckResult, PassedCheckResult, ProgramDigest };
}

// @public (undocumented)
Expand Down Expand Up @@ -36,7 +36,7 @@ export namespace Options {

// @public (undocumented)
export namespace Plugin {
export { BoundaryError, BoundaryErrorSchema, BoundaryPayloadRejected, BoundaryUnrecognizedSignal, CheckerCheckResult, CheckerGroupResult, CheckerRequest, CheckerRpcs, EvaluatorPluginKind, ExitClass, ExitCode, ExitCodeFromClass, PluginKind, PluginKindSchema, ReporterAck, ReporterDrained, ReporterEventBatch, ReporterInitOptions, ReporterRpcs, TestRunnerDryRunRequest, TestRunnerMutantRunRequest, TestRunnerRpcs, WorkerEntryUrl, WorkerPluginKind, WorkerPluginSpawn, WorkerPluginSpawnSchema };
export { BoundaryError, BoundaryErrorSchema, BoundaryPayloadRejected, BoundaryUnrecognizedSignal, CheckerCheckResult, CheckerDigestRequest, CheckerGroupResult, CheckerRequest, CheckerRpcs, EvaluatorPluginKind, ExitClass, ExitCode, ExitCodeFromClass, PluginKind, PluginKindSchema, ReporterAck, ReporterDrained, ReporterEventBatch, ReporterInitOptions, ReporterRpcs, TestRunnerDryRunRequest, TestRunnerMutantRunRequest, TestRunnerRpcs, WorkerEntryUrl, WorkerPluginKind, WorkerPluginSpawn, WorkerPluginSpawnSchema };
}

// @public (undocumented)
Expand Down
14 changes: 14 additions & 0 deletions packages/stryker-js-plugin-interface/src/Checker.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,20 @@ export const CheckerMutantWire = S.Struct({
})
export type CheckerMutantWire = typeof CheckerMutantWire.Type

/**
* The digest of the TypeScript program a checker loaded: SHA-256 over the sorted
* hashes of every source file in the program (lib and node_modules declarations
* included), every tsconfig in its chain, the TypeScript version, the checker
* plugin version, and the checker's options. A `CompileError` verdict is reusable
* only when two runs' digests are byte-equal, so a missing digest is never a match.
*/
export const ProgramDigest = S.String.check(
S.isPattern(/^[0-9a-f]{64}$/u, {
expected: 'a 64-character lowercase hexadecimal program digest',
}),
).pipe(S.brand('ProgramDigest'))
export type ProgramDigest = typeof ProgramDigest.Type

export const CheckResultSchema = S.Union([
S.Struct({ status: S.Literal('passed') }),
S.Struct({ status: S.Literal('compileError'), reason: S.String }),
Expand Down
3 changes: 2 additions & 1 deletion packages/stryker-js-plugin-interface/src/Checker.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as Context from 'effect/Context'
import type * as Effect from 'effect/Effect'
import type * as HashMap from 'effect/HashMap'

import type { CheckerMutantWire, CheckResult } from './Checker.schema.js'
import type { CheckerMutantWire, CheckResult, ProgramDigest } from './Checker.schema.js'
import { CheckerFailed } from './Checker.schema.js'

export interface CheckerService {
Expand All @@ -13,6 +13,7 @@ export interface CheckerService {
readonly group: (
mutants: readonly CheckerMutantWire[],
) => Effect.Effect<readonly (readonly string[])[], CheckerFailed>
readonly digest: Effect.Effect<ProgramDigest, CheckerFailed>
}

export class Checker extends Context.Service<Checker, CheckerService>()(
Expand Down
1 change: 1 addition & 0 deletions packages/stryker-js-plugin-interface/src/Checker/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ export {
type FailedCheckResult,
type IgnoredCheckResult,
type PassedCheckResult,
ProgramDigest,
} from '../Checker.schema.js'
export * from '../Checker.service.js'
9 changes: 8 additions & 1 deletion packages/stryker-js-plugin-interface/src/Mutant.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,14 @@ export type MutantStatus = typeof MutantStatusSchema.Type
export const SurvivorStatusSchema = S.Literals(['Survived', 'NoCoverage'])
export type SurvivorStatus = typeof SurvivorStatusSchema.Type

export const RememberedStatusSchema = S.Literals(['Survived', 'Killed', 'Timeout', 'NoCoverage', 'Ignored'])
export const RememberedStatusSchema = S.Literals([
'Survived',
'Killed',
'Timeout',
'NoCoverage',
'Ignored',
'CompileError',
])
export type RememberedStatus = typeof RememberedStatusSchema.Type

export const EphemeralStatusSchema = S.Literals(['CompileError', 'RuntimeError', 'Pending'])
Expand Down
3 changes: 3 additions & 0 deletions packages/stryker-js-plugin-interface/src/Plugin.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ export type CheckerRequest = typeof CheckerRequest.Type
export const CheckerCheckResult = S.Record(S.String, CheckResultSchema)
export const CheckerGroupResult = S.String.pipe(S.Array, S.Array)

export const CheckerDigestRequest = S.Struct({ checkerName: S.String })
export type CheckerDigestRequest = typeof CheckerDigestRequest.Type

export const ReporterInitOptions = S.Struct({
traceparent: S.optionalKey(Traceparent),
tracestate: S.optionalKey(Tracestate),
Expand Down
1 change: 1 addition & 0 deletions packages/stryker-js-plugin-interface/src/Plugin/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export {
BoundaryPayloadRejected,
BoundaryUnrecognizedSignal,
CheckerCheckResult,
CheckerDigestRequest,
CheckerGroupResult,
CheckerRequest,
EvaluatorPluginKind,
Expand Down
15 changes: 13 additions & 2 deletions packages/stryker-js-plugin-interface/src/PluginRpcs.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import * as RpcGroup from 'effect/rpc/RpcGroup'

import type * as RpcSchema from 'effect/rpc/RpcSchema'

import { CheckerFailed } from './Checker.schema.js'
import { CheckerFailed, ProgramDigest } from './Checker.schema.js'
import { ReporterFailed } from './ReporterEvent.schema.js'
import {
DryRunResultSchema,
Expand All @@ -15,6 +15,7 @@ import {

import {
CheckerCheckResult,
CheckerDigestRequest,
CheckerGroupResult,
CheckerRequest,
ReporterAck,
Expand Down Expand Up @@ -67,6 +68,12 @@ const group: TracedRpc<'group', typeof CheckerRequest, typeof CheckerGroupResult
)
.middleware(TraceContextMiddleware)

const digest: TracedRpc<'digest', typeof CheckerDigestRequest, typeof ProgramDigest, typeof CheckerFailed> = Rpc.make(
'digest',
{ payload: CheckerDigestRequest, success: ProgramDigest, error: CheckerFailed },
)
.middleware(TraceContextMiddleware)

const init: TracedRpc<'init', typeof ReporterInitOptions, typeof ReporterAck, typeof ReporterFailed> = Rpc.make(
'init',
{ payload: ReporterInitOptions, success: ReporterAck, error: ReporterFailed },
Expand All @@ -89,7 +96,11 @@ export const TestRunnerRpcs: RpcGroup.RpcGroup<typeof capabilities | typeof dryR
mutantRun,
)

export const CheckerRpcs: RpcGroup.RpcGroup<typeof check | typeof group> = RpcGroup.make(check, group)
export const CheckerRpcs: RpcGroup.RpcGroup<typeof check | typeof group | typeof digest> = RpcGroup.make(
check,
group,
digest,
)

export const ReporterRpcs: RpcGroup.RpcGroup<typeof init | typeof onEventBatch | typeof flush> = RpcGroup.make(
init,
Expand Down
1 change: 1 addition & 0 deletions packages/stryker-js-typescript-checker/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"license": "Apache-2.0",
"dependencies": {
"@effect/opentelemetry": "catalog:",
"@noble/hashes": "catalog:",
"@opentelemetry/exporter-trace-otlp-http": "catalog:",
"@opentelemetry/sdk-node": "catalog:",
"@opentelemetry/sdk-trace-base": "catalog:",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { checkCell } from './Checker.cell.js'
import { CheckMutantsCommand } from './Checker.schema.js'
import { type CompilerError, DryRunCompileErrors } from './Compiler.schema.js'
import { make as makeCompilerBlueprint } from './ts-compiler.blueprint.js'
import { describeDiagnostics, groups, init, type TSCompiler } from './ts-compiler.handle.js'
import { describeDiagnostics, groups, init, programDigest, type TSCompiler } from './ts-compiler.handle.js'
import { TypeScriptCompiler } from './ts-compiler.service.js'

type CheckEvent = CheckMutantsAnswer[number]
Expand Down Expand Up @@ -93,6 +93,8 @@ const makeChecker = Effect.fn(SpanTaxonomy.Spans.typescriptCheckerRuntimeMakeChe
),

group: (mutants) => groups([...mutants]),

digest: programDigest(compiler).pipe(Effect.mapError((cause) => refuse({ mutantIds: [], cause }))),
}

yield* service.init
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ export const checkerHandlers = Plugin.CheckerRpcs.toLayer(
readonly checkerName: string
readonly mutants: readonly Checker.CheckerMutantWire[]
}) => resolve(runtime, checkerName, mutants).pipe(Effect.flatMap((checker) => checker.group([...mutants]))),

digest: ({ checkerName }: { readonly checkerName: string }) =>
resolve(runtime, checkerName, []).pipe(Effect.flatMap((checker) => checker.digest)),
}
}),
)
5 changes: 5 additions & 0 deletions packages/stryker-js-typescript-checker/src/Compiler.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export class CompilerFailed extends S.TaggedError<CompilerFailed>()('CompilerFai
'unknown-file-node',
'file-not-in-project',
'mutant-outside-file',
'program-digest-unavailable',
]),
subject: S.optional(S.String),
}) {
Expand All @@ -75,6 +76,10 @@ export class CompilerFailed extends S.TaggedError<CompilerFailed>()('CompilerFai
'mutant-outside-file',
() => `A mutant's location falls outside '${this.subject ?? 'a file'}'`,
),
Match.when(
'program-digest-unavailable',
() => `The loaded TypeScript program could not be identified: ${this.subject ?? 'unknown input'}`,
),
Match.exhaustive,
)
}
Expand Down
Loading
Loading