Skip to content

ci(checker-parity): compare main's and the branch's TypeScript checker verdicts - #266

Merged
ryanleecode merged 50 commits into
mainfrom
stryker/tsgo-checker-parity-lane
Oct 10, 2026
Merged

ryanleecode merged 50 commits into
mainfrom
stryker/tsgo-checker-parity-lane

Conversation

@systemfsoftware-maker

@systemfsoftware-maker systemfsoftware-maker commented Oct 10, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

CI now runs main's TypeScript checker and this branch's checker over the same mutants and fails the pull request on any verdict difference. Before this, a checker change could only be judged by a full mutation run on main, after merge. This is the evidence lane the TypeScript 7 checker work needs: layer 4's importer shortcut and batching land behind it and must keep every verdict equal to main's.

This is layer 2 of 4 of the TypeScript 7 native checker stack (base: layer 1, stryker/tsgo-checker, #264). Layer 3 adds the type-query port and the lane's wrong-not-assignable gate; layer 4 adds the edit-location shortcut and turns on the shortcut-count and speed gates. Plan: docs/plans/2026-10-09-2106-perf-typescript-checker-tsgo-native-plan.md (U8, U9, U10).

Lane budget (root parity-budget ruling)

The first full-corpus PR run (38026090265, head b91f2c66a) put every one of 6 legs into the 2400 s shell timeout with 2 to 7 of 37 projects finished, and no verdict cache was ever saved. Step 0 split: setup and both builds 50 to 95 s per leg, the run step 97% of every leg; check cost spans 0.07 s per mutant (median) to 4.1 s per mutant in files whose program pulls the repo's sources; projects ran one at a time, main then branch.

The lane is now scoped by event:

  • Pull request: mutants in the files the PR changes (capped per changed file), plus a fixed, seeded drift sample stratified across corpus projects and mutator kinds. Settings: test/checker-parity/pr-scope.json (seed checker-parity-pr-1, 6 drift projects, 4 mutants each, at most 6 per changed file); the step summary prints seed and sizes. A PR touching no checker, instrumenter, tsgo or driver input skips the lane and the report passes with not-applicable.
  • Push to main and workflow_dispatch: the full corpus. The plan job folds the leg measurements of every recent ci.yml run into the cost table, estimates each file (measured rate, else its project's p90 per-mutant rate, else the corpus p90, else 10 s per mutant), and packs 16-mutant blocks into as many legs as the 70% budget needs (36 min step deadline × 0.7 = 1512 s per leg).
  • Every leg: a step timeout below the job timeout (PR: 270 s shell, 5 min step, 8 min job). Verdicts are cached per file, written as each file finishes, saved with if: always() under checker-parity-<k>-<run_id>-<attempt> and restored by the checker-parity-<k>- prefix, so a rerun of a failed leg resumes. Both sides check concurrently.
  • No assertion was dropped or weakened: the parity rule (R7), the zero-snapshot-updates refusal and nothing-compared are unchanged.
leg cold: 38036865136 (lane 292 s) warm: 38037569511 (lane 291 s) green: 38038127731, 819d513e0 (lane 286 s)
1 118 s (run 31 s, 0/8 cached/fresh files, 22 mutants) 119 s (run 29 s, 0/8, 22) 121 s (run 30 s, 2/6, 22)
2 134 s (run 47 s, 0/6, 16) 105 s (run 26 s, 0/4, 10) 117 s (run 25 s, 2/2, 10)
3 107 s (run 40 s, 0/14, 42) 142 s (run 52 s, 4/12, 46) 132 s (run 51 s, 6/10, 46)
4 107 s (run 27 s, 0/6, 18) 101 s (run 34 s, 0/8, 22) 108 s (run 32 s, 2/6, 22)
5 117 s (run 38 s, 0/10, 28) 106 s (run 33 s, 6/4, 30) 100 s (run 29 s, 6/4, 30)
6 126 s (run 32 s, 0/12, 34) 104 s (run 32 s, 2/10, 34) 133 s (run 42 s, 4/8, 34)

Projects not measured on the green PR run (8 of 10 measured); neither is a failure:

  • test/e2e/testResources/typescript-checker-fixture/tsconfig.references.json: no mutant of this PR's changed files or of the drift sample lies in it ("no mutants in scope on this leg"). The full-corpus run (push to main, workflow_dispatch) checks every source file it owns.
  • test/e2e/testResources/typescript-checker-fixture/non-existent-tsconfig.json: the e2e fixture names a tsconfig that does not exist on purpose, so R6 skips and lists it on every run ("tsconfig not found"). The full-corpus run does not cover it either; there is nothing to compile.

Lane = checker-parity-changes start to checker-parity-report end. Change detection 71 to 75 s, report 72 to 74 s. The cold and warm runs measured the lane before the normalisation fix below and ended red in compare (28 and 31 position-only differences), so their times stand but their verdict does not; 38038127731 is the green head (ParityHolds, 164 mutants per side, 8 of 10 projects measured). Warm is barely faster than cold because each push edits this PR's own test/checker-parity sources, which are corpus files and are checked fresh by design, and because the run step is 25 to 52 s of a 100 to 142 s leg: setup is about 30 s and the two builds 35 to 50 s.

Head d5007c565 (P3 fix): CI 38039704581 green, every job. Lane 272 s; legs 1 to 6 took 113, 88, 114, 112, 121 and 130 s; change detection 68 s, report 70 s.

What changes

  • Corpus (R6, U8). Every workspace tsconfig.app.json, every tsconfigFile named by a checker-enabled test/e2e/testResources/*/stryker*.config.ts, plus a new isolatedDeclarations fixture (test/checker-parity/__fixtures__/isolated-declarations/). Each source is mutated only in the project that owns it.
  • Driver (U9). Private package @systemfsoftware/stryker-checker-parity. run --scope pr|full drives each side's shipped worker bundle (dist/main.mjs) over Plugin.CheckerRpcs, reads the check-span counts from layer 1 through a local OTLP/HTTP receiver, and writes NDJSON plus scope-<k>.json. Pure decisions: select-scope.workflow.ts (PR scope), assign-drift-legs.workflow.ts (drift project to leg), reuse-cached-verdicts.workflow.ts, compare-sides.workflow.ts, report-parity-outcome.workflow.ts; the seeded order lives in Parity.schema.ts (ADR-0002).
  • File naming: deferred, not settled here. src/run-side.ts and src/main.ts stay where they are. Moving them into a drivers/ / bin/ folder was ruled out (a technical-kind folder, with no grounded drivers/ convention, and about 2,250 lines of churn across every layer for no behaviour). The test tool's file naming goes to the repo-wide placement work.
  • Lane (U10). checker-parity-changes runs trigger (pnpm dependency closure of the checker and driver, the e2e fixtures, the lockfile, ci.yml) and outputs run and mode. Six checker-parity (k) legs; checker-parity-report is the one context to require.
  • Own-file positions (OQ-P1). The checker writes absolute paths in diagnostics while a Verdict carries the repo-relative fileName; the own-file match is now a path suffix at a path boundary (819d513e0). Replayed over the two red runs' real rows: 28 → 0 and 31 → 0 violations.

How an agent consumes this

  • Red lane. Each failure prints a ::error annotation and a $GITHUB_STEP_SUMMARY block with a reason code and next action: shard-timeout / shard-failed per leg, cache-miss (warning), shard-incomplete in the report. Artifacts: checker-parity-<run_id>-<k> (files shard-<k>.ndjson, scope-<k>.json), checker-parity-summary-<run_id> (checker-parity-summary.json).
  • NDJSON records (_tag + schemaVersion: 1): Verdict, CheckCall, GroupCall, DigestCall, Counts, ProjectBootFailed, ProjectSkipped, CacheEntry, TelemetryMissing; LegStarted (written when a leg begins) then LegScope (when it finishes) in scope-<k>.json. Schema: test/checker-parity/src/Parity.schema.ts.
  • Compare decision: ParityHolds or ParityBroken with violations (first 50 displayed, omittedCount), each with code and nextAction: verdict-mismatch, boot-asymmetry, zero-snapshot-updates, telemetry-missing, nothing-compared; from layer 4 zero-shortcuts, slower-than-main.
  • Exit codes: 0 holds, 1 broken, 2 driver failure (<code>: <reason> + next action: on stderr; codes usage-error, refused-outside-ci, shard-incomplete, decode-failed, worker-boot-failed, telemetry-missing, telemetry-version-unsupported, rpc-failed, io-failed). Local runs are refused unless CI=true or --allow-local.

Local verification

  • test/checker-parity at 819d513e0: tsc -b exit 0; oxlint . 0 errors; lint:tsgo 0 errors; vitest run 15 files, 178 tests passed; build ok.
  • Falsification probes (rule broken, law observed failing, reverted): drift-leg assignment (rank % shards, driftProjects bound, single-project collapse), seededOrder permutation, per-file cap and per-project cap in select-scope, own-file position law against the pre-fix anchor.
  • Built dist/main.mjs: trigger --base <merge-base> → run=true reason=closure-changed mode=pr; with GITHUB_EVENT_NAME=workflow_dispatch → run=true reason=full-corpus mode=full.
  • No local corpus or mutation run; the lane runs only in CI.

Review

ce-code-review (report-only, focused: correctness in-context plus a local adversarial read; no cross-model peer, none installed) on base layer 1 + main (3eb05925a) to 819d513e0, 41 files:

  • P1 "fully cached scope passes with zero check calls": dismissed. Reuse needs both workers' bundle hash, the program digest (TypeScript and checker version, options, sources, tsconfigs) and the per-file wires to match, so a replayed verdict is the answer the checker would give; the summary states reused vs fresh files. Kept as P3.
  • P1 "changed fixture file owned by no corpus project is dropped": dismissed. Ownership is the same rule on PR and full scope (corpus.ts keeps program files inside the project directory), so such a file is never mutated on the full corpus either; the cost is a wasted trigger. Kept as P3.
  • P1 "own-file normalisation erases the (1,1) sentinel": dismissed. The sentinel (ts-compiler.handle.ts:1766) is an own-file (line,col), which R7/OQ-P1 exempts; the plan already records the unbounded exemption and that a magnitude check needs GATE1 approval (plan Risks). 819d513e0 only lets the rule match absolute paths.
  • P2 "frozen drift seed keeps the isolatedDeclarations fixture out of PR scope": binding on layer 4 (sub-conductor ruling). With --shortcut-gate on, R8's fixture-alone zero-count refusal would fail every PR whose scope misses the fixture; layer 4 must include the fixture in PR scope when it turns the gate on.
  • P2 "8 min job timeout can cancel a leg during setup before its annotation": accepted as is (sub-conductor ruling). Setup plus builds measured 60 to 90 s, so the 5 min step timeout leaves about 2 min; the report still prints shard-incomplete with next action when a leg is cancelled.
  • P3 scope-<k>.json written only at leg end: fixed in d5007c565 (sub-conductor ruling). The leg writes a tagged LegStarted record (shard, scope, settings, the leg's projects, discovery time) before its first project and replaces it with the tagged LegScope when it finishes; compare refuses a started-only leg with shard-incomplete. Smoke on the built driver with run 38038127731's leg 1 rows: LegScope → exit 0; LegStarted → exit 2 shard-incomplete ... started 2 project(s) but never finished; an untagged file → exit 2.

Review, round 2 (ce-code-review mode:agent base:stryker/tsgo-checker, report-only)

Head 9e111409a, seven reviewers (correctness, adversarial, project-standards, testing, maintainability, reliability, learnings) plus the validator; verdict "Not ready". The reviewers ran on the host model: no cross-model reviewer is installed here (recorded as a finding, per ruling). Every finding is fixed in b91f2c66a unless noted:

Sev Finding Disposition
P0 throw in the pure workflow compare-sides.workflow.ts Fixed. No throw in any *.workflow.ts or *.schema.ts file.
P0 throw inside Parity.schema.ts Fixed, as above.
P0 Schema file named for a layer (Shell.schema.ts) Fixed. Renamed to DriverFailure.schema.ts.
P1 The trigger step masks a driver failure, so the report job goes green Fixed. The trigger step runs under pipefail, and 9ea37fce3 fails the report when change detection fails (the ruling's P1 "report-skipped-reads-as-pass", accepted).
P1 Exit 0 when no project was checked; an empty corpus passes; zero projects compared; parity holds with zero mutants (4 findings, one cause) Fixed. compare refuses a run that compared zero mutants with nothing-compared (exit 1).
P2 The layering lint guard covers only 3 pure-core modules Fixed. The pure-core import ban covers every pure module.
P2 The merged compare lists per-shard skips as project skips Fixed. A project measured in another shard is no longer listed as skipped.
P2 The phase-line discriminator is declared in two modules Fixed. PhaseLine is declared once, in Parity.schema.ts.
P2 Shard NDJSON is written only after every project finishes Fixed. Rows are appended per project, so a killed leg keeps them.

Sub-conductor rulings on this round:

  • P2 "stale cache / narrow trigger": fixed in this layer. Each side's verdict cache is keyed on the hash of the worker bundle that runs (a218cf71b). The trigger is the checker's workspace dependency closure from pnpm's own graph (0d5beb237). Both decisions are pure and have laws (reuse-cached-verdicts, trigger-parity-lane).
  • FAIL-path coverage: fixed in 41118a4b9. The outcome → exit code, annotations and summary decision is pure core, with a property law in report-parity-outcome.workflow.property.test.ts: broken parity gives exit 1 and one bounded ::error per mismatch kind; holding parity gives 0; a driver failure gives 2 with a reason code and next action.
  • Cost-planner convergence (82f343af4, 5ed4efd87, 6d1ee1dfa, f9cc2221f): accepted by root on proof run 38063951675 (below).

CI on the exact head

905187ef4 (merges origin/main 56426ca94, which is #264 squashed): CI run 38086327338 is 15/15 green, including check, the five e2e legs, all six checker-parity legs and checker-parity-report. Nix, Changeset Check and Commitlint are green.

  • Bench, inherited (Stream G's lane): bench · test/e2e/testResources/enterprise-monorepo-fixture fails setup-timed-out (run 38086327459). It fails the same way on every layer of this stack and on other PRs, root's fix: restore main's build after the #276/#283 and #282/#274 merges #295 included; main's CI does not run Bench. The other four bench legs are green.
  • check on the previous head dbe8e5777 (run 38081142087) was red on stryker-js tests/incremental-reuse.integration.test.ts › "A refused re-run starts a mutant with its previous killing test": the scenario's second run exited with a failure (runSucceeded: false) right after "Found 2 of 5 file(s) to be mutated". The cause is not in this layer. stryker-js, its instrumenter, plugin interface and vitest runner are byte-identical to main and to fix(stryker-js-typescript-checker): own error positions, check counts #264's head, and this layer's ci.yml only adds jobs. Six concurrent local copies of the scenario on dbe8e5777 gave one failure of a different kind (the third run's killed verdicts and killers changed); eight more passed. It is a load-sensitive scenario in the engine's incremental reuse. Its assertion drops the second run's exit cause, so the CI failure mode cannot be read from the log. On 905187ef4 the stryker-js test task was a turbo cache hit, so this green check did not re-run that scenario.

Full corpus

Proof run of the cost planner on the main path: workflow_dispatch 38063951675 on f9cc2221f, started cold, converged in 1 attempt.

legs planned 39
parity legs green 39 of 39
slowest leg 1860 s (step deadline 2160 s; over the 1512 s planning budget, under the limit)
attempts 1
checker-parity-report green

The only red jobs in that run are the 5 e2e legs (exit 124), the same failure main has today (Stream K). The previous full-corpus run, 38049913120, needed two attempts and most of its 6 legs timed out.

This stack shares packages/stryker-js-typescript-checker/README.md, tests/check-mutants.integration.test.ts (layer 1) and pnpm-lock.yaml with Stream A's #261. Whichever lands second merges the other.

Dogfood mutant ids: none. test/checker-parity is outside every dogfood mutate glob. No changeset: the package is private.


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

Gap table against origin/main, measured updateSnapshot cost on the TS 7.0.2
unstable/async API, the edit-location soundness rule for the importer
re-check shortcut, the CI verdict-parity design, the api-extractor TS5
ruling, and six open questions for the supervisor.
…unit

Twelve stacked units: per-produce diagnostic rendering, snapshot/emit
counters, the edit-location Soundness Rule shortcut, file-bounded rounds,
one emit build per call, a sharded checker-parity CI lane against the
merge-base checker, and the B10 type-query port for Stream I.
Report-only ce-doc-review findings with proposed dispositions appended.
Applies every document-review disposition the supervisor ruled in
.omp-brief/rulings-plan.md (F1-F11, F13-F18; F13 and F17 narrowed; F12
rejected) and records OQ-P1..OQ-P10 as ruled. Replaces the nine-layer
sequencing with four linear layers: docs+U1-U3, lane U8-U10, type query
U11-U12, shortcut U4-U7.
… correct the 8.1.0 importer note

U1 of docs/plans/2026-10-09-2106-perf-typescript-checker-tsgo-native-plan.md (R11). README prerequisites match minimumTypeScriptVersion; the changeset corrects the released importer re-check claim without rewriting CHANGELOG.md.
…against its own text

U2 of the TS7 checker plan (R14, KTD3). checkOne renders its mutant's diagnostics while that mutant's snapshot is current; MutantCheck carries rendered lines and Checker.cell no longer renders after the call. AE10 (per-mutant-position fixture) fails on the old code (line 16 instead of 20). The join property targets dogfood mutant 47892392410b616f.
…ces and emit builds per check

U3 of the TS7 checker plan (R3, KTD6). A per-call tally on the compiler runtime counts snapshot updates, parenthesized re-splices and TCE emit builds and sums emit time; check annotates them on typescript-checker.compiler.check with typescript.counts.schema_version 1. No new span names. No package test (Test Admission refused); the parity lane's zero-updates refusal catches a tally that is never set.
U8 of the TS7 checker plan (R6, B1). A four-file project with isolatedDeclarations: true whose exported functions carry explicit return types and call each other across files, so the edit-location shortcut has a non-empty proof set on the parity lane. tsc -p (TS 7.0.2) passes; an unannotated export fails with TS9013.
…e and compares sides

U9 of the TS7 checker plan (R3, R6-R10, KTD5, KTD6, KTD8, KTD9). Private package @systemfsoftware/stryker-checker-parity: 'run' discovers the R6 corpus, instruments a shard of it with the branch instrumenter, drives main's and the branch's checker worker over Plugin.CheckerRpcs, collects counts from the check spans through a local OTLP/HTTP receiver and writes versioned NDJSON; 'compare' merges every shard and fails on a verdict difference, a missing shard, or (when on) the shortcut-count and speed gates. Every record and failure carries schemaVersion 1, a closed reason code and a next action. The package's lint debt is cleared in the next commit; switch flags (--allow-local, --shortcut-gate, --speed-gate) no longer swallow the next positional.
U10 of the TS7 checker plan (R7, R9, KTD5, KTD8). checker-parity-changes decides whether a pull request reaches the corpus (every push to main runs); six checker-parity legs build main's checker at the merge-base and this commit's checker, then run both over one shard; checker-parity-report merges the shards and fails on any verdict difference or a missing shard. Every failure writes a ::error annotation and a step summary naming the reason code and the artifact to read. The shortcut-count and speed gates stay off until layer 4.
…1 added

The per-mutant-position fixture's padding variable is now _b (oxlint no-unused-vars); lines and the shift mutant's body span are unchanged. The reason-format law is deleted: the OwnRenderedDiagnostics law already pins every mutant's reason over the full CheckMutantsInput schema, and the duplicate declared a module-scope schema in a test file (schema-declaration-location).
…pack

The isolatedDeclarations fixture moves to __fixtures__/, which gritlint and the package's oxlint config ignore: it is corpus input with its own standalone tsconfig, not package source. The driver's export map is now generated by tsdown through @systemfsoftware/tsdown-config's sourceExports, like every other tsdown package, so publishConfig.exports exists and carries no source condition.
compare-sides.workflow.ts combines conditions with effect/Boolean every/some (complexity 1) and indexes verdicts and skipped projects through HashMap, keeping last-wins values and first-seen order. Parity.schema.ts exports schemas only; the shell applies S.fromJsonString(ParityLine) and Shard at its boundary. shard.ts hashes with @noble/hashes and offers dual signatures. The property test draws the production VerdictStatus schema. shard.property.test.ts is deleted: inShard is defined through shardOf, so its two partition laws restated the implementation, and its decode law copied the Shard refinement as its oracle.
…vices

The imperative shell (run-side.ts, main.ts, otlp-receiver.ts, corpus.ts) now reads env through Config, files through FileSystem/Path, children through ChildProcessSpawner, serves OTLP on effect/http, and parses argv with effect/cli. Exit codes are mapped once at the runMain edge: 0 for parity or --help, 1 for a parity break, 2 for a driver failure. OTLP and shell-failure schemas move to Otlp.schema.ts and Shell.schema.ts; span counting is the pure span-counts.ts.

compare-sides: normalising a reason now replaces the own-file position with a replacer function, so a file name holding a String.replace pattern ($&, $1) stays literal. The OwnFilePositionOnly law found it with the file name "\0\0$&".

The three src/__tests__ files tested the hand-rolled parser, receiver and corpus helpers this replaces, and the test-layout rules forbid them there; their behaviour is internal to a private CLI whose only surface is the bin, so they are deleted, not moved.
checker-parity-report ran only after checker-parity-changes succeeded, so a failed change detection skipped the report and the required context read as passing. The report now always runs, fails with code shard-incomplete when change detection failed or was cancelled, and still passes when the bot-push guard skipped it.
A cached verdict is reused only when the stored identity (hash of the worker bundle directory that actually runs, the checker's program digest, and the mutant wires) equals the current one. Reuse is decided by the pure reuse-cached-verdicts workflow; the identity is a sidecar written after the verdicts, so a torn write re-checks. Replaces the hash of the checker's src tree, which missed bundled dependencies (plugin-interface, cli-contract, effect-cell-types).
The trigger subcommand runs the pure trigger-parity-lane workflow over the workspace closure pnpm reports for the checker and the driver, plus shared inputs (lockfile, workspace manifest, flake.lock, ci.yml, e2e testResources). Replaces the hand-maintained path regex in ci.yml.
… pure core

report-parity-outcome maps a compare result or driver failure to exit code (0 holds, 1 broken, 2 driver failure), one ::error per violation kind, and the step summary; property laws pin each. ci.yml calls the trigger subcommand and drops the removed --main-source/--branch-source flags.
…nore-rule change

Main (#262) widened the check RPC to CheckAnswer, whose ignored reason is unknown. The driver records it as the reason text so ignored verdicts still compare exactly.
…2 review

Review of #266 (ce-code-review mode:agent, 7 reviewers + validator): compare refuses a run that compared zero mutants (nothing-compared, exit 1); per-shard skips no longer list a project measured in another shard; the trigger step fails on a driver failure (pipefail); shard NDJSON is appended per project so a killed leg leaves its rows; no throw in the workflow or schema files; Shell.schema.ts renamed DriverFailure.schema.ts; PhaseLine declared once; the pure-core import ban covers every pure module.
A workspace package's sources appear in the program of every project that imports it, so the first lane run mutated the instrumenter's files once per importing project (html-reporter spent 790 of its 835 s per side on them) and four shards hit the 2400 s timeout. The listing now keeps only files under the project's own directory: 581 -> 306 files over four sampled projects.
…current sides

Work in progress on the root parity-budget ruling: PR legs check changed-file mutants (capped per file) plus a seeded drift sample from pr-scope.json; full corpus runs on push and workflow_dispatch; verdict cache is per file and saved with if: always() under run id + attempt + shard; legs write scope-<k>.json with the wall-time split.
…der in the schema file

Per ADR-0001's closed taxonomy and ADR-0002: the unsuffixed select-scope.ts is gone. The drift-leg assignment is a decision, so it is the assign-drift-legs workflow (mutation-graded, laws in its own suite); the seeded order is a pure operation over ScopeSettings, so it lives in Parity.schema.ts with its law in-source, and both the scope workflow and the run-side shell call it instead of private copies.
Sub-conductor ruling on dispatch 38039482257 (legs 3 and 4 made no progress over two attempts).
Evidence: both stuck legs stopped after packages/stryker-js-html-reporter in corpus order, so they sat in packages/stryker-js-instrumenter, which leg 1 measured at 1066 s for 309 mutants on main's side; the per-file verdict cache never saved a file that never finished. scope-3/4.json were missing because the dispatched head 819d513 predates LegStarted (d5007c5).
- Cache slot is one check batch (<= 16 mutants of one file), stored when the batch finishes.
- run --deadline: no batch starts after it, a running batch is interrupted at it; deferred work is written as Deferred lines and the leg exits 2 with code leg-deadline, so the rerun resumes at the first unchecked batch.
- A batch interrupted on two attempts in a row is cached as UnitOverBudget and compare fails with unit-over-budget naming side, file and mutant ids.
- Full-corpus legs are balanced by measured file cost (greedy LPT over test/checker-parity/file-costs.json, seeded from run 38039482257: 165 files, 6158 s); compare --costs-out writes the merged table.
- Every batch logs a heartbeat line (elapsed, side, project, file, batch); the step summary names the leg's planned load.
- CI passes --deadline (2160 s full, 210 s PR) inside the unchanged step budgets; the failure-pointer step defers to the driver's own annotation on exit 2.
systemfsoftware-maker added a commit that referenced this pull request Oct 10, 2026
…nction-body sites (WIP)

Parked per sub-conductor ruling 2026-10-10 09:58 (#266 convergence round 2 first). Not yet reviewed; package gates pass locally: checker 39 files/255 tests, parity 17/245, plugin-interface laws 48.
Per sub-conductor ruling 2026-10-10 09:58 (#266 convergence round 2). The plan step packs contiguous 64-mutant blocks of each file into legs by first-fit-decreasing at 70% of the leg deadline, using a cost table merged from every finished run's leg artifacts (cache, bootstrapped from the newest earlier run on a miss). Matrix width comes from total measured cost. Verdict caches merge across legs. Replaces the hand-edited file-costs.json and the hash balancer.
…e core

Both are effect-only pure modules, so they join pureCore in oxlint.config.ts (the package's convention for unsuffixed pure modules, beside shard.ts, span-counts.ts, corpus.ts); plan-legs.defaults.ts carried a suffix outside any convention and its one constant moves to the --fill flag default.
Sub-conductor ruling 2026-10-10 10:58 (#266): the proof dispatch 38061043178 bootstrapped its costs from one PR-scope run (31 files) and planned 5 legs. Every FileRate and ProjectOverhead now carries the run that measured it, and mergeCosts keeps the newest measurement per file, so fold order is irrelevant and a narrower run never drops files it did not touch (cost table v3, cache prefix checker-parity-costs-v3-). The plan job folds every recent completed ci.yml run with leg artifacts (workflow_dispatch, push to main, this branch; every attempt) and saves the folded table for the report job. An unmeasured file is estimated at its project's p90 per-mutant rate, else the corpus p90. Legs are a greedy contiguous segmentation of the (project, file, block) sequence, so adding a file never lowers the width; full-corpus plans use 16-mutant blocks. The step summary shows width, each leg's estimate against the 70% budget, and measured vs estimated files.
@systemfsoftware-maker
systemfsoftware-maker marked this pull request as ready for review October 10, 2026 17:16
Base automatically changed from stryker/tsgo-checker to main October 10, 2026 20:22

@ryanleecode ryanleecode left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified at 905187e: all checks green except the inherited enterprise-fixture bench leg (Stream G), 0 open threads, no skips/disables/secrets; parity lane 1-2 min on PRs.

@ryanleecode
ryanleecode merged commit ec9dcf6 into main Oct 10, 2026
26 of 27 checks passed
@ryanleecode
ryanleecode deleted the stryker/tsgo-checker-parity-lane branch October 10, 2026 22:31
systemfsoftware-maker added a commit that referenced this pull request Oct 11, 2026
…pull-request run

checker-parity-report on fef35ce (run 38092982281) failed zero-shortcuts isolatedDeclarations because no leg checked the fixture: PR scope checks changed files plus 6 seeded drift projects, the fixture ranks 22nd in the seeded order, and once #266 landed on main its files were no longer changed against the merge base. The shortcut gate judges that fixture on every run, so its files now count as changed on every pull-request leg (checkedOnEveryPullRequest in corpus.ts). No guard refused its sites.
systemfsoftware-maker added a commit that referenced this pull request Oct 11, 2026
…roject's closure

Adds the failing acceptance scenarios for the run-inputs digest scope:

- AE1 replays the repository's real lockfile across #266
  (56426ca -> ec9dcf6): each of the four mutated projects must keep
  its saved verdicts. Today all four are refused as runInputsChanged.
- AE2 uses a pnpm-generated oracle workspace where only project a moves
  a dependency: a must re-test and b must keep its verdicts. Today b is
  refused too.

Both drive the published Engine surface and observe the second run's
ReuseReported event. The oracle locks come from the repository's pinned
pnpm via `pnpm --filter @systemfsoftware/stryker-js fixtures:lockfile-oracle`,
which writes a lock only when two runs agree byte for byte.
systemfsoftware-maker added a commit that referenced this pull request Oct 11, 2026
…rkflow

lockfileClosure walks the project's importer and its ancestors, every dependency field of linked importers, and the snapshots and packages entries they reach, with a visited set so cycles terminate. Every unusable input (undecodable lockfile or install settings, lockfileVersion other than 9, no manifest, missing importer, dangling ref) decides the whole-file digest with a tagged reason. Laws pin the #266 shape (an unlinked importer changes nothing), exact reach for version and integrity moves, root devDependencies and install settings moving every project, and cycle termination.
systemfsoftware-maker added a commit that referenced this pull request Oct 11, 2026
…inputs digest

runInputsDigestOf now reads pnpm-lock.yaml and pnpm-workspace.yaml, builds a
LockfileClosureCommand, and hashes the closure the lockfile-closure workflow
decides: the project's importer, its ancestor importers, linked importers, the
snapshots and package entries they reach, the hoisting settings and the root
packageManager. Other lockfiles and an unusable pnpm-lock.yaml keep the
whole-file digest; the unusable case logs one warning naming the reason code
and the next action.

Canonical JSON orders keys by code unit (Order.String) instead of
localeCompare, and INCREMENTAL_CACHE_VERSION moves 5 -> 6 so digests from the
old layout are discarded once.

AE1/AE2 (the #266 lockfile edit and the oracle bump) now pass, plus AE3-AE13
as integration scenarios and in-source laws.
ryanleecode added a commit that referenced this pull request Oct 12, 2026
…303)

* docs(stryker-js): scope the run-inputs digest to each project's lockfile closure

Requirements for Stream K: verified premises (file:line), closure rule, whole-file fallback, cache version bump, rollout through release and the stryker-published pin.

* docs(stryker-js): plan the per-project lockfile closure for the run-inputs digest

Enriches the brainstorm with the planning contract: Workflow.make closure decision, pnpm-lock schema, real-tool lockfile oracles, failing-first AE1/AE2, cache version 5->6, patch changeset.

* docs(stryker-js): apply the plan review rulings

Records hidden hoisting as an accepted gap (KD6) with a mechanical trigger:
the closure input carries pnpm-workspace.yaml's hoisting keys and the root
packageManager (R11, AE11, AE12). Adds the live-lockfile closure assertion
(AE13) and the oracle regeneration script, corrects the P1 probe counts,
and fills the U4 AE4 scenario.

* test(stryker-js): pin verdict reuse across lockfile edits outside a project's closure

Adds the failing acceptance scenarios for the run-inputs digest scope:

- AE1 replays the repository's real lockfile across #266
  (56426ca -> ec9dcf6): each of the four mutated projects must keep
  its saved verdicts. Today all four are refused as runInputsChanged.
- AE2 uses a pnpm-generated oracle workspace where only project a moves
  a dependency: a must re-test and b must keep its verdicts. Today b is
  refused too.

Both drive the published Engine surface and observe the second run's
ReuseReported event. The oracle locks come from the repository's pinned
pnpm via `pnpm --filter @systemfsoftware/stryker-js fixtures:lockfile-oracle`,
which writes a lock only when two runs agree byte for byte.

* feat(stryker-js): declare the pnpm v9 lockfile schema and its ref grammar

Decodes pnpm-lock.yaml's importers, snapshots and packages with strict dependency maps, keeping every other entry field as JSON so a closure hashes whole entries. lockRefOf follows pnpm's refToRelative rule (aliases, peer suffixes, patch_hash, file: and link: refs) and packageKeyOf drops the trailing peer and patch groups; both are pinned against the forms the oracle lock records.

* feat(stryker-js): decide each project's lockfile closure in a pure workflow

lockfileClosure walks the project's importer and its ancestors, every dependency field of linked importers, and the snapshots and packages entries they reach, with a visited set so cycles terminate. Every unusable input (undecodable lockfile or install settings, lockfileVersion other than 9, no manifest, missing importer, dangling ref) decides the whole-file digest with a tagged reason. Laws pin the #266 shape (an unlinked importer changes nothing), exact reach for version and integrity moves, root devDependencies and install settings moving every project, and cycle termination.

* fix(stryker-js): hash each project's own lockfile closure in the run-inputs digest

runInputsDigestOf now reads pnpm-lock.yaml and pnpm-workspace.yaml, builds a
LockfileClosureCommand, and hashes the closure the lockfile-closure workflow
decides: the project's importer, its ancestor importers, linked importers, the
snapshots and package entries they reach, the hoisting settings and the root
packageManager. Other lockfiles and an unusable pnpm-lock.yaml keep the
whole-file digest; the unusable case logs one warning naming the reason code
and the next action.

Canonical JSON orders keys by code unit (Order.String) instead of
localeCompare, and INCREMENTAL_CACHE_VERSION moves 5 -> 6 so digests from the
old layout are discarded once.

AE1/AE2 (the #266 lockfile edit and the oracle bump) now pass, plus AE3-AE13
as integration scenarios and in-source laws.

* chore(stryker-js): record the run-inputs closure digest as a patch

* test(stryker-js): fail lockfile-closure scenarios whose engine run dies

Each staged run now records whether its Exit succeeded, and every scenario
expects completed runs. The live-lockfile scenario only collected fallback
warnings, so a run that died before logging passed with an empty list; under
a stubbed early exit it now fails on completed: false (review #303, P1).

* test(stryker-js): prove each install setting by law, not by twenty engine runs

∀wmo_PnpmWorkspaceSettings_≡EachInstallSettingEditMovesEveryProject stages two
projects and checks that editing hoist, hoistPattern, publicHoistPattern,
nodeLinker, shamefullyHoist or the root packageManager moves both projects'
run-inputs digests; blinding the shell to any one of the six refutes it.

The integration file keeps one engine scenario for the end-to-end path
(nodeLinker: hoisted, 4 runs) in place of the five-setting scenario and the
packageManager scenario (24 runs), so it no longer leans on the 60 s default
timeout (review #303, P1). File: 54.09 s -> 33.57 s; hoisting scenario:
11.23 s -> 2.18 s.

* test(stryker-js): vary the lockfile settings block in the closure laws

∀lok_PnpmLockfile_≡SettingsMoveEveryProjectWhileAnOutsideEditMovesNone: moving
any key of the lockfile's own settings block moves every importer's closure
decision, and adding a snapshot no importer reaches moves none. The closure
carries settings verbatim, yet no law varied it; dropping settings from the
closure now fails this law and only this law (review #303, P2).

---------

Co-authored-by: Ryan Lee <drdgvhbh@gmail.com>
ryanleecode added a commit that referenced this pull request Oct 12, 2026
…gate wrong NotAssignable in the parity lane (#277)

* docs(brainstorms): requirements for the TypeScript 7 native checker unit

Gap table against origin/main, measured updateSnapshot cost on the TS 7.0.2
unstable/async API, the edit-location soundness rule for the importer
re-check shortcut, the CI verdict-parity design, the api-extractor TS5
ruling, and six open questions for the supervisor.

* docs(plans): implementation plan for the TypeScript 7 native checker unit

Twelve stacked units: per-produce diagnostic rendering, snapshot/emit
counters, the edit-location Soundness Rule shortcut, file-bounded rounds,
one emit build per call, a sharded checker-parity CI lane against the
merge-base checker, and the B10 type-query port for Stream I.
Report-only ce-doc-review findings with proposed dispositions appended.

* docs(plans): apply review dispositions and the four-layer stack

Applies every document-review disposition the supervisor ruled in
.omp-brief/rulings-plan.md (F1-F11, F13-F18; F13 and F17 narrowed; F12
rejected) and records OQ-P1..OQ-P10 as ruled. Replaces the nine-layer
sequencing with four linear layers: docs+U1-U3, lane U8-U10, type query
U11-U12, shortcut U4-U7.

* docs(stryker-js-typescript-checker): state the TypeScript 7 floor and correct the 8.1.0 importer note

U1 of docs/plans/2026-10-09-2106-perf-typescript-checker-tsgo-native-plan.md (R11). README prerequisites match minimumTypeScriptVersion; the changeset corrects the released importer re-check claim without rewriting CHANGELOG.md.

* docs(plans): record the root rulings on OQ-P5, OQ-P8 and OQ-P9

* docs(plans): record the agentic-quality standing rule in R9, U9, U10 and U12

* fix(stryker-js-typescript-checker): render each mutant's diagnostics against its own text

U2 of the TS7 checker plan (R14, KTD3). checkOne renders its mutant's diagnostics while that mutant's snapshot is current; MutantCheck carries rendered lines and Checker.cell no longer renders after the call. AE10 (per-mutant-position fixture) fails on the old code (line 16 instead of 20). The join property targets dogfood mutant 47892392410b616f.

* feat(stryker-js-typescript-checker): report snapshot updates, re-splices and emit builds per check

U3 of the TS7 checker plan (R3, KTD6). A per-call tally on the compiler runtime counts snapshot updates, parenthesized re-splices and TCE emit builds and sums emit time; check annotates them on typescript-checker.compiler.check with typescript.counts.schema_version 1. No new span names. No package test (Test Admission refused); the parity lane's zero-updates refusal catches a tally that is never set.

* test(checker-parity): add an isolatedDeclarations corpus fixture

U8 of the TS7 checker plan (R6, B1). A four-file project with isolatedDeclarations: true whose exported functions carry explicit return types and call each other across files, so the edit-location shortcut has a non-empty proof set on the parity lane. tsc -p (TS 7.0.2) passes; an unannotated export fails with TS9013.

* feat(checker-parity): add the parity driver that runs one checker side and compares sides

U9 of the TS7 checker plan (R3, R6-R10, KTD5, KTD6, KTD8, KTD9). Private package @systemfsoftware/stryker-checker-parity: 'run' discovers the R6 corpus, instruments a shard of it with the branch instrumenter, drives main's and the branch's checker worker over Plugin.CheckerRpcs, collects counts from the check spans through a local OTLP/HTTP receiver and writes versioned NDJSON; 'compare' merges every shard and fails on a verdict difference, a missing shard, or (when on) the shortcut-count and speed gates. Every record and failure carries schemaVersion 1, a closed reason code and a next action. The package's lint debt is cleared in the next commit; switch flags (--allow-local, --shortcut-gate, --speed-gate) no longer swallow the next positional.

* ci: add the checker-parity lane

U10 of the TS7 checker plan (R7, R9, KTD5, KTD8). checker-parity-changes decides whether a pull request reaches the corpus (every push to main runs); six checker-parity legs build main's checker at the merge-base and this commit's checker, then run both over one shard; checker-parity-report merges the shards and fails on any verdict difference or a missing shard. Every failure writes a ::error annotation and a step summary naming the reason code and the artifact to read. The shortcut-count and speed gates stay off until layer 4.

* test(stryker-js-typescript-checker): clear the two lint errors layer 1 added

The per-mutant-position fixture's padding variable is now _b (oxlint no-unused-vars); lines and the shift mutant's body span are unchanged. The reason-format law is deleted: the OwnRenderedDiagnostics law already pins every mutant's reason over the full CheckMutantsInput schema, and the duplicate declared a module-scope schema in a test file (schema-declaration-location).

* build(checker-parity): satisfy the repo's source-resolution gritlint pack

The isolatedDeclarations fixture moves to __fixtures__/, which gritlint and the package's oxlint config ignore: it is corpus input with its own standalone tsconfig, not package source. The driver's export map is now generated by tsdown through @systemfsoftware/tsdown-config's sourceExports, like every other tsdown package, so publishConfig.exports exists and carries no source condition.

* refactor(checker-parity): clear the pure core's lint debt

compare-sides.workflow.ts combines conditions with effect/Boolean every/some (complexity 1) and indexes verdicts and skipped projects through HashMap, keeping last-wins values and first-seen order. Parity.schema.ts exports schemas only; the shell applies S.fromJsonString(ParityLine) and Shard at its boundary. shard.ts hashes with @noble/hashes and offers dual signatures. The property test draws the production VerdictStatus schema. shard.property.test.ts is deleted: inShard is defined through shardOf, so its two partition laws restated the implementation, and its decode law copied the Shard refinement as its oracle.

* refactor(checker-parity): run the driver shell on Effect platform services

The imperative shell (run-side.ts, main.ts, otlp-receiver.ts, corpus.ts) now reads env through Config, files through FileSystem/Path, children through ChildProcessSpawner, serves OTLP on effect/http, and parses argv with effect/cli. Exit codes are mapped once at the runMain edge: 0 for parity or --help, 1 for a parity break, 2 for a driver failure. OTLP and shell-failure schemas move to Otlp.schema.ts and Shell.schema.ts; span counting is the pure span-counts.ts.

compare-sides: normalising a reason now replaces the own-file position with a replacer function, so a file name holding a String.replace pattern ($&, $1) stays literal. The OwnFilePositionOnly law found it with the file name "\0\0$&".

The three src/__tests__ files tested the hand-rolled parser, receiver and corpus helpers this replaces, and the test-layout rules forbid them there; their behaviour is internal to a private CLI whose only surface is the bin, so they are deleted, not moved.

* ci(checker-parity): fail the lane report when change detection fails

checker-parity-report ran only after checker-parity-changes succeeded, so a failed change detection skipped the report and the required context read as passing. The report now always runs, fails with code shard-incomplete when change detection failed or was cancelled, and still passes when the bot-push guard skipped it.

* fix(checker-parity): key each side's verdict cache on its worker bundle

A cached verdict is reused only when the stored identity (hash of the worker bundle directory that actually runs, the checker's program digest, and the mutant wires) equals the current one. Reuse is decided by the pure reuse-cached-verdicts workflow; the identity is a sidecar written after the verdicts, so a torn write re-checks. Replaces the hash of the checker's src tree, which missed bundled dependencies (plugin-interface, cli-contract, effect-cell-types).

* fix(checker-parity): trigger the lane from the pnpm dependency closure

The trigger subcommand runs the pure trigger-parity-lane workflow over the workspace closure pnpm reports for the checker and the driver, plus shared inputs (lockfile, workspace manifest, flake.lock, ci.yml, e2e testResources). Replaces the hand-maintained path regex in ci.yml.

* fix(checker-parity): decide exit code, annotations and summary in the pure core

report-parity-outcome maps a compare result or driver failure to exit code (0 holds, 1 broken, 2 driver failure), one ::error per violation kind, and the step summary; property laws pin each. ci.yml calls the trigger subcommand and drops the removed --main-source/--branch-source flags.

* fix(checker-parity): read checker answers as CheckAnswer after the ignore-rule change

Main (#262) widened the check RPC to CheckAnswer, whose ignored reason is unknown. The driver records it as the reason text so ignored verdicts still compare exactly.

* fix(checker-parity): fail when no mutant was compared; resolve layer 2 review

Review of #266 (ce-code-review mode:agent, 7 reviewers + validator): compare refuses a run that compared zero mutants (nothing-compared, exit 1); per-shard skips no longer list a project measured in another shard; the trigger step fails on a driver failure (pipefail); shard NDJSON is appended per project so a killed leg leaves its rows; no throw in the workflow or schema files; Shell.schema.ts renamed DriverFailure.schema.ts; PhaseLine declared once; the pure-core import ban covers every pure module.

* feat(stryker-js-plugin-interface): add the provisional type-query port

New ./type-query entry: TypeQuery service tag and the schema-version-1 request, response, answer and refusal shapes (plan U11, OQ-P8: port only, no Layer). Refusals carry a version literal and a next action per the root standing rule.

* fix(checker-parity): mutate each source only in the project that owns it

A workspace package's sources appear in the program of every project that imports it, so the first lane run mutated the instrumenter's files once per importing project (html-reporter spent 790 of its 835 s per side on them) and four shards hit the 2400 s timeout. The listing now keeps only files under the project's own directory: 581 -> 306 files over four sampled projects.

* feat(stryker-js-typescript-checker): type-query implementation and lane gate, work in progress

Work in progress, paused by root ruling to fix the layer 2 parity lane budget. Checker and parity packages typecheck, lint and pass their unit suites locally; TypeQueryLive not yet smoke-run against tsgo; no PR.

* feat(checker-parity): pull request scope, per-file verdict cache, concurrent sides

Work in progress on the root parity-budget ruling: PR legs check changed-file mutants (capped per file) plus a seeded drift sample from pr-scope.json; full corpus runs on push and workflow_dispatch; verdict cache is per file and saved with if: always() under run id + attempt + shard; legs write scope-<k>.json with the wall-time split.

* refactor(checker-parity): assign drift legs in a workflow and seed order in the schema file

Per ADR-0001's closed taxonomy and ADR-0002: the unsuffixed select-scope.ts is gone. The drift-leg assignment is a decision, so it is the assign-drift-legs workflow (mutation-graded, laws in its own suite); the seeded order is a pure operation over ScopeSettings, so it lives in Parity.schema.ts with its law in-source, and both the scope workflow and the run-side shell call it instead of private copies.

* chore(checker-parity): drop the property seed file the in-source law run left behind

* fix(checker-parity): match the mutant's own file in absolute diagnostic paths

The checker writes absolute paths in diagnostics while each Verdict carries the repo-relative fileName, so the OQ-P1 own-file position normalisation anchored at the start of the line never fired in CI: runs 38036865136 and 38037569511 failed on 28 and 31 position-only differences. The own file now matches as a path suffix at a path boundary; the position laws draw absolute roots, and a sibling sharing the name's suffix is still refused.

* fix(checker-parity): write the leg scope file when the leg starts

Per the sub-conductor's l2 budget ruling (P3): scope-<k>.json was written only after the leg's last project, so a timed-out leg's shard-timeout annotation named a file that did not exist. The leg now writes a tagged LegStarted record (shard, scope, settings, the leg's projects, discovery time) before the first project and replaces it with the tagged LegScope when it finishes. compare decodes either and refuses a started-only leg with shard-incomplete naming the unfinished project count.

* fix(stryker-js-typescript-checker): refuse NotAssignable under any generic callee, pin the type query against tsgo

The generic-call guard read getResolvedSignature, which tsgo 7.0.2 returns already instantiated, so id('x') under declare function id<T>(x: T): T reported no type parameters and answered "" NotAssignable to "x" although id('') compiles. The CallArgument fact is now declaredGeneric: any declared signature of the callee (getSignaturesOfType) has type parameters. This corrects plan KTD11's condition, which named the resolved signature.

tests/type-query-pins.integration.test.ts runs the real TypeQueryLive against tsgo: AE12-AE14 answers, contextual types unchanged by the appended probe statements, a file ending in a line comment, AE15 (the #4804 panic file refused server-crashed while the next file is answered and a concurrent check keeps its verdicts), and unsupported-version and not-in-project refusals. tests/type-query.differential.test.ts applies every answered candidate through CheckerRuntime and fails on a NotAssignable whose verdict is not CompileError; it went red on the reintroduced generic-call bug.

* test(stryker-js-plugin-interface): state the type-query request's refusal boundary

The generated laws prove TypeQuery's schemas round-trip but never present a value they must refuse. The request refuses a fractional version and an empty file list; the in-source law draws the unrefined base with boundary seeds and fails when either refinement is widened (checked: S.Array for files, S.Finite for version).

* fix(stryker-js-typescript-checker): answer NotAssignable only where a declared type enforces the context

CI run 38040840996 failed wrong-not-assignable twice: a 'true' inside '[[..., true] as const]' returned from an arrow passed to Option.match, and a literal inside S.Literals([...]). Both sites had a contextual type that was inference, not a constraint, so the mutant compiled. The KTD11 condition 'not an argument of an overloaded or generic call' only looked at the immediate parent.

The handle now walks from the site to where its contextual type comes from (ContextOrigin): an annotated declaration, assignment, satisfies or annotated return is DeclaredContext; a call argument carries its callee's signature facts; anything else is UnenforcedContext, answered Unknown 'context-not-enforced' (new UnknownReason, last in KTD11 order). The walk climbs parentheses, array/object literals, spreads, conditional branches, ??/||/&&, 'as const' and unannotated arrow/function bodies. Plain 'as T' / <T> assertions stop as unenforced.

Pins and the differential spec gain the two CI shapes plus a plain assertion, an annotated array element and a const-asserted tuple under an annotation; both were red against the previous handle. The lane's unknown-reason counts are now keyed by the port's UnknownReason. Probed with the built TypeQueryLive on the two CI sites: both now Unknown overloaded-or-generic-call.

* fix(stryker-js-typescript-checker): resolve layer-3 review findings

ce-code-review (report-only, base stryker/tsgo-checker-parity-lane) on ddda557; validator confirmed 13 of 14.

- project-open-failed: a tsconfig tsgo cannot open used to refuse every file not-in-project (wrong next action); openServer now refuses the request project-open-failed when the snapshot holds no project for the tsconfig. Pinned (red before).
- Handler reasons: a separate type-query-reasons fixture drives no-contextual-type, site-not-expression, error-type, instantiable-target and site-not-found through the real handler, plus a second answered file on the same server.
- Differential oracle now fails when the query refuses the fixture instead of passing vacuously.
- Probe slots are taken from the text being appended, not re-derived from the join layout.
- answerTypeQuery decides its own AnswerDecision classes (shared type id, as check-mutants does); the handle maps them to the port's TypeAnswer. The constructor shim answer-type-query.schema.ts is gone: it exported functions from a *.schema.ts.
- Harness schemas moved to tests/__fixtures__/*-law.fixture.ts (schema-declaration-location).
- classify-candidate law renders the Parenthesized case as '(x)', not 'x)'.
- checker-parity: the type-query driver moved out of run-side.ts into type-query-side.ts (run-side 1412 -> 1210 lines); TypeQueryRefusalReason is the union of the port's two refusal schemas.

* feat(checker-parity): report each project's type-query answers, reasons and refusals

Sub-conductor ruling 1 on #277: R18 stays (a project with no answers passes), and the step summary and summary JSON must show a dark project.

- TypeQueryProjectShare gains unknownReasons (keyed by the port's UnknownReason), refusedFiles and refusedMutants.
- The step summary gains a type-query line and one line per project: answered, NotAssignable, Unknown with its non-zero reasons, refused files and mutants, and 'no answers' when nothing was answered.
- The type-query count schemas are NonNegativeInt (exported from Parity.schema.ts): the new summary law drew a negative Unknown count, a state no run can produce.
- Plan U11: TypeQueryLive is a fixed Layer (ruling 2), amended in place as the ruling directs.

* fix(checker-parity): make every full-corpus attempt advance

Sub-conductor ruling on dispatch 38039482257 (legs 3 and 4 made no progress over two attempts).
Evidence: both stuck legs stopped after packages/stryker-js-html-reporter in corpus order, so they sat in packages/stryker-js-instrumenter, which leg 1 measured at 1066 s for 309 mutants on main's side; the per-file verdict cache never saved a file that never finished. scope-3/4.json were missing because the dispatched head 819d513 predates LegStarted (d5007c5).
- Cache slot is one check batch (<= 16 mutants of one file), stored when the batch finishes.
- run --deadline: no batch starts after it, a running batch is interrupted at it; deferred work is written as Deferred lines and the leg exits 2 with code leg-deadline, so the rerun resumes at the first unchecked batch.
- A batch interrupted on two attempts in a row is cached as UnitOverBudget and compare fails with unit-over-budget naming side, file and mutant ids.
- Full-corpus legs are balanced by measured file cost (greedy LPT over test/checker-parity/file-costs.json, seeded from run 38039482257: 165 files, 6158 s); compare --costs-out writes the merged table.
- Every batch logs a heartbeat line (elapsed, side, project, file, batch); the step summary names the leg's planned load.
- CI passes --deadline (2160 s full, 210 s PR) inside the unchanged step budgets; the failure-pointer step defers to the driver's own annotation on exit 2.

* feat(stryker-js-typescript-checker): typeQuery RPC, capability and function-body sites (WIP)

Parked per sub-conductor ruling 2026-10-10 09:58 (#266 convergence round 2 first). Not yet reviewed; package gates pass locally: checker 39 files/255 tests, parity 17/245, plugin-interface laws 48.

* fix(checker-parity): plan full-corpus legs from measured per-file cost

Per sub-conductor ruling 2026-10-10 09:58 (#266 convergence round 2). The plan step packs contiguous 64-mutant blocks of each file into legs by first-fit-decreasing at 70% of the leg deadline, using a cost table merged from every finished run's leg artifacts (cache, bootstrapped from the newest earlier run on a miss). Matrix width comes from total measured cost. Verdict caches merge across legs. Replaces the hand-edited file-costs.json and the hash balancer.

* fix(stryker-js-typescript-checker): name the served type-query versions from the schema

The unsupported-version refusal referenced a deleted constant and threw instead of refusing; a version 3 request now gets the refusal, pinned in type-query-pins. typeQueryServingOf moves out of TypeQuery.schema.ts into type-query-serving.ts with a pipeable signature; TYPE_QUERY_VERSIONS is gone (TypeQueryVersion.literals). Plugin-interface oxlint and lint:tsgo now exit 0.

* refactor(checker-parity): hold blocks.ts and file-costs.ts to the pure core

Both are effect-only pure modules, so they join pureCore in oxlint.config.ts (the package's convention for unsuffixed pure modules, beside shard.ts, span-counts.ts, corpus.ts); plan-legs.defaults.ts carried a suffix outside any convention and its one constant moves to the --fill flag default.

* fix(checker-parity): plan from every run's measurements, conservatively

Sub-conductor ruling 2026-10-10 10:58 (#266): the proof dispatch 38061043178 bootstrapped its costs from one PR-scope run (31 files) and planned 5 legs. Every FileRate and ProjectOverhead now carries the run that measured it, and mergeCosts keeps the newest measurement per file, so fold order is irrelevant and a narrower run never drops files it did not touch (cost table v3, cache prefix checker-parity-costs-v3-). The plan job folds every recent completed ci.yml run with leg artifacts (workflow_dispatch, push to main, this branch; every attempt) and saves the folded table for the report job. An unmeasured file is estimated at its project's p90 per-mutant rate, else the corpus p90. Legs are a greedy contiguous segmentation of the (project, file, block) sequence, so adding a file never lowers the width; full-corpus plans use 16-mutant blocks. The step summary shows width, each leg's estimate against the 70% budget, and measured vs estimated files.

* fix(stryker-js-cli-contract): name the rpc.typeQuery span

The checker RPC group gained typeQuery, and the span-taxonomy law (every protocol method has an rpc.<method> span) refuted it. Review finding (learnings, api-contract, adversarial; P1).

* refactor(stryker-js-plugin-interface)!: serve the type-query port as the root TypeQuery namespace

The ./type-query subpath is gone; every caller imports the TypeQuery namespace from the package root, the way Checker and Plugin are reached. The checker RPC group carries the type-query classes, and a consumer's declaration emit can name them only through a root export.

* fix(stryker-js): build against the checker RPC group with typeQuery

The engine's checker handle now names only the RPCs it calls (check, group, digest), so its declaration no longer needs the type-query answer classes (TS2883). The in-repo fake checkers answer capabilities with no type-query versions and refuse every typeQuery with unsupported-version. The RPC changeset is major: every checker worker must answer the two new RPCs (BREAK-1). Review findings: learnings/api-contract P1, changeset bump P2.

* fix(stryker-js-typescript-checker): serialize type queries per tsgo server

Two queries on one tsconfig interleaved the probe update and the reads: query 1 read a program holding query 2's probe text and answered candidate-not-found. Each server now has a Semaphore(1) held for the whole per-file probe; a fiber that waited on a server another fiber discarded takes the current one; a crash discards only its own server; close is bounded by a 1 s grace like the checker's.

Regression: metamorphic relation in type-query.differential.test.ts (concurrent per-site queries answer as the same queries one at a time) failed before (seed 767247522: Unknown candidate-not-found vs overloaded-or-generic-call) and passes after. Review #277 P2 rows 4 and 5.

* test(checker-parity): pin the type-query recall counters

compileErrorTotal counts branch-side compileError verdicts only, and compileErrorAnsweredNotAssignable counts those whose own project answered NotAssignable. The law fails when the side filter is dropped or the project join is reduced to the mutant id. Review #277 P2 row 6.

* test(checker-parity): draw the recall law's answer split in the generator

745bce9 failed oxlint (no-nested-quantification). The law now draws mutants answered NotAssignable and mutants answered Assignable, Unknown or nothing as two arrays, builds lines in a module helper, and counts branch compileErrors per group. It still fails when the side filter is dropped or the project join is reduced to the mutant id.

* refactor(checker-parity): type the function-body mutator name against the stock catalog

A renamed stock mutator now fails to compile (TS2820) instead of silently turning every function-body site into an expression site. Review #277 P3 row 10.

* test(stryker-js-typescript-checker): assert the served type-query version

The worker scenario now carries the version out of each serving answer and checks versions 1 and 2, served and refused. It fails when typeQueryServingOf answers the wrong version. Review #277 P3 row 11.

* refactor(stryker-js-plugin-interface)!: require TypeQuerySite.kind

A site's kind was an optional key whose absence meant expression, resolved at a distance in the checker (site.kind ?? 'expression') and written by presence in the lane. Every site now names its kind; the checker reads it directly and the lane writes it for every site. Review #277 P3 row 9 (schema-laws: tagged unions over state by presence).

* chore(changeset): record the stryker-js RPC-group touch as a none intent

#267's release consumed the intents that covered stryker-js's Checker.handle.ts typing change (a436092), so Changeset Check on 6f99d56 (run 38092173764) found stryker-js moved with no intent. The change is type-only: the engine's client is narrowed to check, group and digest.

---------

Co-authored-by: Ryan Lee <drdgvhbh@gmail.com>
ryanleecode added a commit that referenced this pull request Oct 13, 2026
…s inside function bodies (#286)

* docs(brainstorms): requirements for the TypeScript 7 native checker unit

Gap table against origin/main, measured updateSnapshot cost on the TS 7.0.2
unstable/async API, the edit-location soundness rule for the importer
re-check shortcut, the CI verdict-parity design, the api-extractor TS5
ruling, and six open questions for the supervisor.

* docs(plans): implementation plan for the TypeScript 7 native checker unit

Twelve stacked units: per-produce diagnostic rendering, snapshot/emit
counters, the edit-location Soundness Rule shortcut, file-bounded rounds,
one emit build per call, a sharded checker-parity CI lane against the
merge-base checker, and the B10 type-query port for Stream I.
Report-only ce-doc-review findings with proposed dispositions appended.

* docs(plans): apply review dispositions and the four-layer stack

Applies every document-review disposition the supervisor ruled in
.omp-brief/rulings-plan.md (F1-F11, F13-F18; F13 and F17 narrowed; F12
rejected) and records OQ-P1..OQ-P10 as ruled. Replaces the nine-layer
sequencing with four linear layers: docs+U1-U3, lane U8-U10, type query
U11-U12, shortcut U4-U7.

* docs(stryker-js-typescript-checker): state the TypeScript 7 floor and correct the 8.1.0 importer note

U1 of docs/plans/2026-10-09-2106-perf-typescript-checker-tsgo-native-plan.md (R11). README prerequisites match minimumTypeScriptVersion; the changeset corrects the released importer re-check claim without rewriting CHANGELOG.md.

* docs(plans): record the root rulings on OQ-P5, OQ-P8 and OQ-P9

* docs(plans): record the agentic-quality standing rule in R9, U9, U10 and U12

* fix(stryker-js-typescript-checker): render each mutant's diagnostics against its own text

U2 of the TS7 checker plan (R14, KTD3). checkOne renders its mutant's diagnostics while that mutant's snapshot is current; MutantCheck carries rendered lines and Checker.cell no longer renders after the call. AE10 (per-mutant-position fixture) fails on the old code (line 16 instead of 20). The join property targets dogfood mutant 47892392410b616f.

* feat(stryker-js-typescript-checker): report snapshot updates, re-splices and emit builds per check

U3 of the TS7 checker plan (R3, KTD6). A per-call tally on the compiler runtime counts snapshot updates, parenthesized re-splices and TCE emit builds and sums emit time; check annotates them on typescript-checker.compiler.check with typescript.counts.schema_version 1. No new span names. No package test (Test Admission refused); the parity lane's zero-updates refusal catches a tally that is never set.

* test(checker-parity): add an isolatedDeclarations corpus fixture

U8 of the TS7 checker plan (R6, B1). A four-file project with isolatedDeclarations: true whose exported functions carry explicit return types and call each other across files, so the edit-location shortcut has a non-empty proof set on the parity lane. tsc -p (TS 7.0.2) passes; an unannotated export fails with TS9013.

* feat(checker-parity): add the parity driver that runs one checker side and compares sides

U9 of the TS7 checker plan (R3, R6-R10, KTD5, KTD6, KTD8, KTD9). Private package @systemfsoftware/stryker-checker-parity: 'run' discovers the R6 corpus, instruments a shard of it with the branch instrumenter, drives main's and the branch's checker worker over Plugin.CheckerRpcs, collects counts from the check spans through a local OTLP/HTTP receiver and writes versioned NDJSON; 'compare' merges every shard and fails on a verdict difference, a missing shard, or (when on) the shortcut-count and speed gates. Every record and failure carries schemaVersion 1, a closed reason code and a next action. The package's lint debt is cleared in the next commit; switch flags (--allow-local, --shortcut-gate, --speed-gate) no longer swallow the next positional.

* ci: add the checker-parity lane

U10 of the TS7 checker plan (R7, R9, KTD5, KTD8). checker-parity-changes decides whether a pull request reaches the corpus (every push to main runs); six checker-parity legs build main's checker at the merge-base and this commit's checker, then run both over one shard; checker-parity-report merges the shards and fails on any verdict difference or a missing shard. Every failure writes a ::error annotation and a step summary naming the reason code and the artifact to read. The shortcut-count and speed gates stay off until layer 4.

* test(stryker-js-typescript-checker): clear the two lint errors layer 1 added

The per-mutant-position fixture's padding variable is now _b (oxlint no-unused-vars); lines and the shift mutant's body span are unchanged. The reason-format law is deleted: the OwnRenderedDiagnostics law already pins every mutant's reason over the full CheckMutantsInput schema, and the duplicate declared a module-scope schema in a test file (schema-declaration-location).

* build(checker-parity): satisfy the repo's source-resolution gritlint pack

The isolatedDeclarations fixture moves to __fixtures__/, which gritlint and the package's oxlint config ignore: it is corpus input with its own standalone tsconfig, not package source. The driver's export map is now generated by tsdown through @systemfsoftware/tsdown-config's sourceExports, like every other tsdown package, so publishConfig.exports exists and carries no source condition.

* refactor(checker-parity): clear the pure core's lint debt

compare-sides.workflow.ts combines conditions with effect/Boolean every/some (complexity 1) and indexes verdicts and skipped projects through HashMap, keeping last-wins values and first-seen order. Parity.schema.ts exports schemas only; the shell applies S.fromJsonString(ParityLine) and Shard at its boundary. shard.ts hashes with @noble/hashes and offers dual signatures. The property test draws the production VerdictStatus schema. shard.property.test.ts is deleted: inShard is defined through shardOf, so its two partition laws restated the implementation, and its decode law copied the Shard refinement as its oracle.

* refactor(checker-parity): run the driver shell on Effect platform services

The imperative shell (run-side.ts, main.ts, otlp-receiver.ts, corpus.ts) now reads env through Config, files through FileSystem/Path, children through ChildProcessSpawner, serves OTLP on effect/http, and parses argv with effect/cli. Exit codes are mapped once at the runMain edge: 0 for parity or --help, 1 for a parity break, 2 for a driver failure. OTLP and shell-failure schemas move to Otlp.schema.ts and Shell.schema.ts; span counting is the pure span-counts.ts.

compare-sides: normalising a reason now replaces the own-file position with a replacer function, so a file name holding a String.replace pattern ($&, $1) stays literal. The OwnFilePositionOnly law found it with the file name "\0\0$&".

The three src/__tests__ files tested the hand-rolled parser, receiver and corpus helpers this replaces, and the test-layout rules forbid them there; their behaviour is internal to a private CLI whose only surface is the bin, so they are deleted, not moved.

* ci(checker-parity): fail the lane report when change detection fails

checker-parity-report ran only after checker-parity-changes succeeded, so a failed change detection skipped the report and the required context read as passing. The report now always runs, fails with code shard-incomplete when change detection failed or was cancelled, and still passes when the bot-push guard skipped it.

* fix(checker-parity): key each side's verdict cache on its worker bundle

A cached verdict is reused only when the stored identity (hash of the worker bundle directory that actually runs, the checker's program digest, and the mutant wires) equals the current one. Reuse is decided by the pure reuse-cached-verdicts workflow; the identity is a sidecar written after the verdicts, so a torn write re-checks. Replaces the hash of the checker's src tree, which missed bundled dependencies (plugin-interface, cli-contract, effect-cell-types).

* fix(checker-parity): trigger the lane from the pnpm dependency closure

The trigger subcommand runs the pure trigger-parity-lane workflow over the workspace closure pnpm reports for the checker and the driver, plus shared inputs (lockfile, workspace manifest, flake.lock, ci.yml, e2e testResources). Replaces the hand-maintained path regex in ci.yml.

* fix(checker-parity): decide exit code, annotations and summary in the pure core

report-parity-outcome maps a compare result or driver failure to exit code (0 holds, 1 broken, 2 driver failure), one ::error per violation kind, and the step summary; property laws pin each. ci.yml calls the trigger subcommand and drops the removed --main-source/--branch-source flags.

* fix(checker-parity): read checker answers as CheckAnswer after the ignore-rule change

Main (#262) widened the check RPC to CheckAnswer, whose ignored reason is unknown. The driver records it as the reason text so ignored verdicts still compare exactly.

* fix(checker-parity): fail when no mutant was compared; resolve layer 2 review

Review of #266 (ce-code-review mode:agent, 7 reviewers + validator): compare refuses a run that compared zero mutants (nothing-compared, exit 1); per-shard skips no longer list a project measured in another shard; the trigger step fails on a driver failure (pipefail); shard NDJSON is appended per project so a killed leg leaves its rows; no throw in the workflow or schema files; Shell.schema.ts renamed DriverFailure.schema.ts; PhaseLine declared once; the pure-core import ban covers every pure module.

* feat(stryker-js-plugin-interface): add the provisional type-query port

New ./type-query entry: TypeQuery service tag and the schema-version-1 request, response, answer and refusal shapes (plan U11, OQ-P8: port only, no Layer). Refusals carry a version literal and a next action per the root standing rule.

* fix(checker-parity): mutate each source only in the project that owns it

A workspace package's sources appear in the program of every project that imports it, so the first lane run mutated the instrumenter's files once per importing project (html-reporter spent 790 of its 835 s per side on them) and four shards hit the 2400 s timeout. The listing now keeps only files under the project's own directory: 581 -> 306 files over four sampled projects.

* feat(stryker-js-typescript-checker): type-query implementation and lane gate, work in progress

Work in progress, paused by root ruling to fix the layer 2 parity lane budget. Checker and parity packages typecheck, lint and pass their unit suites locally; TypeQueryLive not yet smoke-run against tsgo; no PR.

* feat(checker-parity): pull request scope, per-file verdict cache, concurrent sides

Work in progress on the root parity-budget ruling: PR legs check changed-file mutants (capped per file) plus a seeded drift sample from pr-scope.json; full corpus runs on push and workflow_dispatch; verdict cache is per file and saved with if: always() under run id + attempt + shard; legs write scope-<k>.json with the wall-time split.

* refactor(checker-parity): assign drift legs in a workflow and seed order in the schema file

Per ADR-0001's closed taxonomy and ADR-0002: the unsuffixed select-scope.ts is gone. The drift-leg assignment is a decision, so it is the assign-drift-legs workflow (mutation-graded, laws in its own suite); the seeded order is a pure operation over ScopeSettings, so it lives in Parity.schema.ts with its law in-source, and both the scope workflow and the run-side shell call it instead of private copies.

* chore(checker-parity): drop the property seed file the in-source law run left behind

* fix(checker-parity): match the mutant's own file in absolute diagnostic paths

The checker writes absolute paths in diagnostics while each Verdict carries the repo-relative fileName, so the OQ-P1 own-file position normalisation anchored at the start of the line never fired in CI: runs 38036865136 and 38037569511 failed on 28 and 31 position-only differences. The own file now matches as a path suffix at a path boundary; the position laws draw absolute roots, and a sibling sharing the name's suffix is still refused.

* fix(checker-parity): write the leg scope file when the leg starts

Per the sub-conductor's l2 budget ruling (P3): scope-<k>.json was written only after the leg's last project, so a timed-out leg's shard-timeout annotation named a file that did not exist. The leg now writes a tagged LegStarted record (shard, scope, settings, the leg's projects, discovery time) before the first project and replaces it with the tagged LegScope when it finishes. compare decodes either and refuses a started-only leg with shard-incomplete naming the unfinished project count.

* fix(stryker-js-typescript-checker): refuse NotAssignable under any generic callee, pin the type query against tsgo

The generic-call guard read getResolvedSignature, which tsgo 7.0.2 returns already instantiated, so id('x') under declare function id<T>(x: T): T reported no type parameters and answered "" NotAssignable to "x" although id('') compiles. The CallArgument fact is now declaredGeneric: any declared signature of the callee (getSignaturesOfType) has type parameters. This corrects plan KTD11's condition, which named the resolved signature.

tests/type-query-pins.integration.test.ts runs the real TypeQueryLive against tsgo: AE12-AE14 answers, contextual types unchanged by the appended probe statements, a file ending in a line comment, AE15 (the #4804 panic file refused server-crashed while the next file is answered and a concurrent check keeps its verdicts), and unsupported-version and not-in-project refusals. tests/type-query.differential.test.ts applies every answered candidate through CheckerRuntime and fails on a NotAssignable whose verdict is not CompileError; it went red on the reintroduced generic-call bug.

* test(stryker-js-plugin-interface): state the type-query request's refusal boundary

The generated laws prove TypeQuery's schemas round-trip but never present a value they must refuse. The request refuses a fractional version and an empty file list; the in-source law draws the unrefined base with boundary seeds and fails when either refinement is widened (checked: S.Array for files, S.Finite for version).

* fix(stryker-js-typescript-checker): answer NotAssignable only where a declared type enforces the context

CI run 38040840996 failed wrong-not-assignable twice: a 'true' inside '[[..., true] as const]' returned from an arrow passed to Option.match, and a literal inside S.Literals([...]). Both sites had a contextual type that was inference, not a constraint, so the mutant compiled. The KTD11 condition 'not an argument of an overloaded or generic call' only looked at the immediate parent.

The handle now walks from the site to where its contextual type comes from (ContextOrigin): an annotated declaration, assignment, satisfies or annotated return is DeclaredContext; a call argument carries its callee's signature facts; anything else is UnenforcedContext, answered Unknown 'context-not-enforced' (new UnknownReason, last in KTD11 order). The walk climbs parentheses, array/object literals, spreads, conditional branches, ??/||/&&, 'as const' and unannotated arrow/function bodies. Plain 'as T' / <T> assertions stop as unenforced.

Pins and the differential spec gain the two CI shapes plus a plain assertion, an annotated array element and a const-asserted tuple under an annotation; both were red against the previous handle. The lane's unknown-reason counts are now keyed by the port's UnknownReason. Probed with the built TypeQueryLive on the two CI sites: both now Unknown overloaded-or-generic-call.

* fix(stryker-js-typescript-checker): resolve layer-3 review findings

ce-code-review (report-only, base stryker/tsgo-checker-parity-lane) on ddda557; validator confirmed 13 of 14.

- project-open-failed: a tsconfig tsgo cannot open used to refuse every file not-in-project (wrong next action); openServer now refuses the request project-open-failed when the snapshot holds no project for the tsconfig. Pinned (red before).
- Handler reasons: a separate type-query-reasons fixture drives no-contextual-type, site-not-expression, error-type, instantiable-target and site-not-found through the real handler, plus a second answered file on the same server.
- Differential oracle now fails when the query refuses the fixture instead of passing vacuously.
- Probe slots are taken from the text being appended, not re-derived from the join layout.
- answerTypeQuery decides its own AnswerDecision classes (shared type id, as check-mutants does); the handle maps them to the port's TypeAnswer. The constructor shim answer-type-query.schema.ts is gone: it exported functions from a *.schema.ts.
- Harness schemas moved to tests/__fixtures__/*-law.fixture.ts (schema-declaration-location).
- classify-candidate law renders the Parenthesized case as '(x)', not 'x)'.
- checker-parity: the type-query driver moved out of run-side.ts into type-query-side.ts (run-side 1412 -> 1210 lines); TypeQueryRefusalReason is the union of the port's two refusal schemas.

* feat(checker-parity): report each project's type-query answers, reasons and refusals

Sub-conductor ruling 1 on #277: R18 stays (a project with no answers passes), and the step summary and summary JSON must show a dark project.

- TypeQueryProjectShare gains unknownReasons (keyed by the port's UnknownReason), refusedFiles and refusedMutants.
- The step summary gains a type-query line and one line per project: answered, NotAssignable, Unknown with its non-zero reasons, refused files and mutants, and 'no answers' when nothing was answered.
- The type-query count schemas are NonNegativeInt (exported from Parity.schema.ts): the new summary law drew a negative Unknown count, a state no run can produce.
- Plan U11: TypeQueryLive is a fixed Layer (ruling 2), amended in place as the ruling directs.

* feat(stryker-js-typescript-checker): importer shortcut, check rounds and option (in progress)

Edit-site facts, admit-importer-shortcut and plan-check-rounds workflows, the check-rounds runner over CheckSteps, the importerCheck option and a shortcut integration test that has not run yet. Paused per sub-conductor ruling to fix the layer-2 full-corpus lane first.

* fix(checker-parity): make every full-corpus attempt advance

Sub-conductor ruling on dispatch 38039482257 (legs 3 and 4 made no progress over two attempts).
Evidence: both stuck legs stopped after packages/stryker-js-html-reporter in corpus order, so they sat in packages/stryker-js-instrumenter, which leg 1 measured at 1066 s for 309 mutants on main's side; the per-file verdict cache never saved a file that never finished. scope-3/4.json were missing because the dispatched head 819d513 predates LegStarted (d5007c5).
- Cache slot is one check batch (<= 16 mutants of one file), stored when the batch finishes.
- run --deadline: no batch starts after it, a running batch is interrupted at it; deferred work is written as Deferred lines and the leg exits 2 with code leg-deadline, so the rerun resumes at the first unchecked batch.
- A batch interrupted on two attempts in a row is cached as UnitOverBudget and compare fails with unit-over-budget naming side, file and mutant ids.
- Full-corpus legs are balanced by measured file cost (greedy LPT over test/checker-parity/file-costs.json, seeded from run 38039482257: 165 files, 6158 s); compare --costs-out writes the merged table.
- Every batch logs a heartbeat line (elapsed, side, project, file, batch); the step summary names the leg's planned load.
- CI passes --deadline (2160 s full, 210 s PR) inside the unchanged step budgets; the failure-pointer step defers to the driver's own annotation on exit 2.

* feat(stryker-js-typescript-checker): finish the importer shortcut layer

The integration test read spans after the OpenTelemetry layer shut down and reset the in-memory exporter, so every count came back empty. It now reads them inside the scope. AE7 edited the parameter, which is outside the function body, so it now edits the return statement. Checked against a planted defect: when admitImporterShortcut always takes the shortcut, AE2, AE3 and AE9 change from compileError to passed and the scenario fails.

Adds the importerCheck changeset (minor).

* feat(stryker-js-typescript-checker): typeQuery RPC, capability and function-body sites (WIP)

Parked per sub-conductor ruling 2026-10-10 09:58 (#266 convergence round 2 first). Not yet reviewed; package gates pass locally: checker 39 files/255 tests, parity 17/245, plugin-interface laws 48.

* fix(checker-parity): plan full-corpus legs from measured per-file cost

Per sub-conductor ruling 2026-10-10 09:58 (#266 convergence round 2). The plan step packs contiguous 64-mutant blocks of each file into legs by first-fit-decreasing at 70% of the leg deadline, using a cost table merged from every finished run's leg artifacts (cache, bootstrapped from the newest earlier run on a miss). Matrix width comes from total measured cost. Verdict caches merge across legs. Replaces the hand-edited file-costs.json and the hash balancer.

* fix(stryker-js-typescript-checker): name the served type-query versions from the schema

The unsupported-version refusal referenced a deleted constant and threw instead of refusing; a version 3 request now gets the refusal, pinned in type-query-pins. typeQueryServingOf moves out of TypeQuery.schema.ts into type-query-serving.ts with a pipeable signature; TYPE_QUERY_VERSIONS is gone (TypeQueryVersion.literals). Plugin-interface oxlint and lint:tsgo now exit 0.

* refactor(checker-parity): hold blocks.ts and file-costs.ts to the pure core

Both are effect-only pure modules, so they join pureCore in oxlint.config.ts (the package's convention for unsuffixed pure modules, beside shard.ts, span-counts.ts, corpus.ts); plan-legs.defaults.ts carried a suffix outside any convention and its one constant moves to the --fill flag default.

* fix(checker-parity): plan from every run's measurements, conservatively

Sub-conductor ruling 2026-10-10 10:58 (#266): the proof dispatch 38061043178 bootstrapped its costs from one PR-scope run (31 files) and planned 5 legs. Every FileRate and ProjectOverhead now carries the run that measured it, and mergeCosts keeps the newest measurement per file, so fold order is irrelevant and a narrower run never drops files it did not touch (cost table v3, cache prefix checker-parity-costs-v3-). The plan job folds every recent completed ci.yml run with leg artifacts (workflow_dispatch, push to main, this branch; every attempt) and saves the folded table for the report job. An unmeasured file is estimated at its project's p90 per-mutant rate, else the corpus p90. Legs are a greedy contiguous segmentation of the (project, file, block) sequence, so adding a file never lowers the width; full-corpus plans use 16-mutant blocks. The step summary shows width, each leg's estimate against the 70% budget, and measured vs estimated files.

* fix(stryker-js-cli-contract): name the rpc.typeQuery span

The checker RPC group gained typeQuery, and the span-taxonomy law (every protocol method has an rpc.<method> span) refuted it. Review finding (learnings, api-contract, adversarial; P1).

* refactor(stryker-js-plugin-interface)!: serve the type-query port as the root TypeQuery namespace

The ./type-query subpath is gone; every caller imports the TypeQuery namespace from the package root, the way Checker and Plugin are reached. The checker RPC group carries the type-query classes, and a consumer's declaration emit can name them only through a root export.

* fix(stryker-js): build against the checker RPC group with typeQuery

The engine's checker handle now names only the RPCs it calls (check, group, digest), so its declaration no longer needs the type-query answer classes (TS2883). The in-repo fake checkers answer capabilities with no type-query versions and refuse every typeQuery with unsupported-version. The RPC changeset is major: every checker worker must answer the two new RPCs (BREAK-1). Review findings: learnings/api-contract P1, changeset bump P2.

* fix(stryker-js-typescript-checker): check an edited file in the project that owns it

Under project references every referencing program holds the referenced project's sources but reports no semantic errors for them. projectOfFile returned the first program holding the file, so on the enterprise fixture the importer shortcut read gates.ts diagnostics from analytics, saw none, and passed real compile errors; the TCE emit for those false passes then wrote core/src/*.js into the fixture, which turned check's oxlint red. The project whose rootFiles list the file now answers first.

Regression: importer-shortcut scenario over a two-project fixture (app references lib) fails before, passes after. e2e-core placement suite 7/7 with no stray files.

* fix(stryker-js-typescript-checker): serialize type queries per tsgo server

Two queries on one tsconfig interleaved the probe update and the reads: query 1 read a program holding query 2's probe text and answered candidate-not-found. Each server now has a Semaphore(1) held for the whole per-file probe; a fiber that waited on a server another fiber discarded takes the current one; a crash discards only its own server; close is bounded by a 1 s grace like the checker's.

Regression: metamorphic relation in type-query.differential.test.ts (concurrent per-site queries answer as the same queries one at a time) failed before (seed 767247522: Unknown candidate-not-found vs overloaded-or-generic-call) and passes after. Review #277 P2 rows 4 and 5.

* test(checker-parity): pin the type-query recall counters

compileErrorTotal counts branch-side compileError verdicts only, and compileErrorAnsweredNotAssignable counts those whose own project answered NotAssignable. The law fails when the side filter is dropped or the project join is reduced to the mutant id. Review #277 P2 row 6.

* test(checker-parity): draw the recall law's answer split in the generator

745bce9 failed oxlint (no-nested-quantification). The law now draws mutants answered NotAssignable and mutants answered Assignable, Unknown or nothing as two arrays, builds lines in a module helper, and counts branch compileErrors per group. It still fails when the side filter is dropped or the project join is reduced to the mutant id.

* refactor(checker-parity): type the function-body mutator name against the stock catalog

A renamed stock mutator now fails to compile (TS2820) instead of silently turning every function-body site into an expression site. Review #277 P3 row 10.

* test(stryker-js-typescript-checker): assert the served type-query version

The worker scenario now carries the version out of each serving answer and checks versions 1 and 2, served and refused. It fails when typeQueryServingOf answers the wrong version. Review #277 P3 row 11.

* refactor(stryker-js-plugin-interface)!: require TypeQuerySite.kind

A site's kind was an optional key whose absence meant expression, resolved at a distance in the checker (site.kind ?? 'expression') and written by presence in the lane. Every site now names its kind; the checker reads it directly and the lane writes it for every site. Review #277 P3 row 9 (schema-laws: tagged unions over state by presence).

* test(stryker-js-typescript-checker): refuse an unknown importerCheck value

A scenario in importer-shortcut.integration.test.ts checks the annotated
body edit under importerCheck 'sometimes', 1, 'always', 'location-rule'
and no option, on real tsgo. An unknown value must fail the check with
the invalid-checker-options refusal naming the mutant; 'always' takes no
shortcut; the rule, set or defaulted, takes one. It closes the planted
option-schema gap on #286: emptying the importerCheck struct lets an
unknown value decode, so the check no longer refuses it.

* fix(stryker-js-typescript-checker): keep the importer shortcut off a body's closing brace and off files with syntax errors

Edit-site facts move into edit-site.schema.ts beside their schemas (ADR-0002); edit-site.ts is gone. The scan stops descending into nodes that end before the span, check() keeps only the last round as lastMutants, and projectsOf is no longer exported. One fixture and one scenario row per Soundness Rule guard (declare global, require, typeof import, non-module file, removed terminator, syntax error) plus shared-round solo fallback and unheld-member scenarios. Review #286 rows 2, 5, 7, 10, 11, R1, R3 and the shared-round gaps.

* test(stryker-js-typescript-checker): pin one-update two-file snapshots and the importerCheck digest

The U6 pin drives typescript/unstable/async over a virtual file system: one updateSnapshot serves both changed texts and re-checks a rewritten file. The digest scenario shows importerCheck moves the program digest. Review #286 gaps.

* fix(checker-parity): judge speed only on projects checked fresh on both sides

speedViolationsFor compares projects with a non-cached check on main and branch; with none, parity holds and warns speed-gate-unmeasured with a next action in the step summary and an Actions annotation. Review #286 row 3 and R4.

* chore(changeset): record the stryker-js RPC-group touch as a none intent

#267's release consumed the intents that covered stryker-js's Checker.handle.ts typing change (a436092), so Changeset Check on 6f99d56 (run 38092173764) found stryker-js moved with no intent. The change is type-only: the engine's client is narrowed to check, group and digest.

* fix(checker-parity): check the isolatedDeclarations fixture on every pull-request run

checker-parity-report on fef35ce (run 38092982281) failed zero-shortcuts isolatedDeclarations because no leg checked the fixture: PR scope checks changed files plus 6 seeded drift projects, the fixture ranks 22nd in the seeded order, and once #266 landed on main its files were no longer changed against the merge base. The shortcut gate judges that fixture on every run, so its files now count as changed on every pull-request leg (checkedOnEveryPullRequest in corpus.ts). No guard refused its sites.

* test(checker-parity): the shortcut gate fixture takes the importer shortcut

Real tsgo checks every stock mutant of the isolatedDeclarations fixture with importerCheck location-rule and always: the shortcut decides at least one mutant and every verdict matches. Planting admitImporterShortcut to refuse every site fails it (shortcutTaken false).

* fix(checker-parity): keep the isolatedDeclarations fixture on every pull-request leg

checker-parity-report on ad5769a (run 38097541662) still failed zero-shortcuts
for isolatedDeclarations: 8e4c82b made changedOnThisLeg select every fixture
file, but processProject first asks inScopeOf, which only admits a project that
samples on the leg or has a changed file in its directory. The fixture did
neither once main held it, so it never reached the file filter (no leg's
scope-k.json lists it).

One pure rule now decides both questions: checkedOnLeg and projectOnLeg in
corpus.ts over a ProjectLeg schema (Parity.schema.ts), and run-side builds a
ProjectLeg once per project. Laws in corpus.workflow.property.test.ts draw
ProjectLeg: a fixture file checked on its shard puts the fixture project on the
leg, fixture files are checked on their shard whatever the pull request
changed, and an unsampled project with no change is off the leg. Removing the
fixture clause from projectOnLeg (the shipped bug) or from checkedOnLeg fails
them.

* fix(checker-parity): count in the summary exactly the projects the speed gate compares

The summary's measuredProjectCount and the main/branch timing totals used
measuredOnBoth (any fresh phase line on both sides), while the speed gate
compares checkedOnBoth (a fresh check call on both sides). A run could
report projects as measured that the gate never compared. summaryOf now
uses checkedOnBoth, and measuredOnBoth is deleted. The law
SummaryMeasuredCount pins the count and totals to the compared set; it
fails when the old set is restored.

* refactor(stryker-js-typescript-checker): move round and shortcut decisions into their schema files

Ruling 1 (ADR-0002): CheckRound.schema.ts holds SoloRound, SharedRound and
roundOfLayer, which takes a non-empty layer, so an empty round cannot be built
(ruling 9). ImporterShortcut.schema.ts holds the decision union with
takesShortcut and fallbackKeyOf. The effect-schema lint
(schema-file-exports-schemas-only) refuses the counting helper whose parameter
is an array of optional decisions, so the span counts stay in check-rounds.ts.

RoundCandidate ids are branded MutantId. A check call that names one mutant twice
is refused by the PlanCheckRoundsCommand schema and fails with reason
duplicate-mutant-ids and a next action (BREAK-1, major changeset), not a
defect. The importer-shortcut feature pins the refusal.

* fix(stryker-js-typescript-checker): read file facts without a fake span, and stop the edit-site scan at the edit

Ruling 7: FileFacts (typescriptModule, declaresGlobal) is its own schema read by
fileFactsOf, and the handle's declare-global check uses it in place of an
end-of-file span. Ruling 6: the scan returns at the first node that starts after
the edit, and a counter of inspected nodes is checked against an unpruned model.
Ruling 2: in-file laws state that an admitted span stops before the closing
brace and that widening an edit never turns a refusal into an admit. Planting
the unpruned scan or a brace-inclusive editable end fails these laws.

* fix(stryker-js-typescript-checker): never take the importer shortcut for a constructor body

An unannotated class property takes its type from the constructor's
assignments, so a constructor body edit can change what an importer sees
while the edited file stays clean. Real tsgo 7.0.2: 'this.start = 1' to
"'one'" leaves constructed.ts clean and fails consumer.ts with TS2322.
Constructors leave SIGNATURE_FREE_KINDS; the rule scenario gains a
constructor row that fails if constructors are admitted again (review
cycle 3, finding 2).

* refactor(stryker-js-typescript-checker): give decided one exported home

check-rounds.ts and type-query.handle.ts each carried a private copy of
decided; both now import the one ts-compiler.handle.ts exports (ADR-0002:
each pure operation has one home; review cycle 3, finding 6).

* test(stryker-js-typescript-checker): laws for the shortcut schema operations and refusals

takesShortcut, fallbackKeyOf and fileFactsOf each get an in-source law
(finding 3); fileFactsOf runs real tsgo over drawn extensions, export and
declare-global choices. SharedRound ids, MutantBound, distinct round
candidates and EditSpan state what they refuse against named domain
predicates (finding 7). Each law fails on a planted change to its subject.

* fix(stryker-js-typescript-checker): refuse the importer shortcut for a file another project compiles

Review cycle 3, finding 5. The shortcut read the mutated file's errors from
its owning project only. A second open project that includes or imports the
same file checks it under its own compiler options and can reject an edit the
owner accepts, so the shortcut passed a mutant that 'always' rejects.

Probed on tsgo 7.0.2: a project that reaches the owner through references,
directly or through another project, holds the file but reports none of its
diagnostics; a project that includes or imports it without a reference path
reports them under its own options. So the refusal counts only holders with no
reference path to the owner. A literal "more than one holder" refusal would
turn the shortcut off for every project-references monorepo.

- The tsconfig walk records each config's references; otherCompilersOf
  (Tsconfig.schema.ts) is the pure count, with a transitive-closure law.
- DecideImporterShortcutCommand carries compiledElsewhere; the new
  shared-file clause (tree original) reports under its own fallback key.
- importer-shortcut-overlap: two referenced projects include one file, one
  with noImplicitReturns and one with noUncheckedIndexedAccess; each edit is
  rejected by one of them only, so the scenario fails whichever owns the file.
  Before the fix the rule run passed one edit that 'always' rejected.

* test(stryker-js-typescript-checker): pin the unread fallback count for a file no project includes

Review cycle 3 gap: nothing asserted typescript.importer_shortcut.fallback.unread.count.
A mutant in a file the tsconfig excludes is read from disk but held by no
project, so its edit-site facts are never read; both modes pass it with nothing
blamed and only the rule run counts it as unread. Dropping the unread key fails
the scenario.

* test(stryker-js-typescript-checker): check two calls on one checker so each starts from the original files

Review cycle 3 gap: no scenario sent a second check call to the same checker.
The harness now runs a list of calls on one checker. The first call checks an
edit that retypes a function the consumer reads; the second checks an edit in
the consumer, which must pass. Skipping the reset of the previous call's last
round, or leaving its files out of the snapshot refresh, fails the scenario.

* fix(stryker-js-typescript-checker): give invalid options and duplicate ids their own tagged errors

InvalidCheckerOptions and DuplicateMutantIds replace the two CompilerFailed
reasons this layer added (CONST-D2, review round 3 P1). Each carries its
code and next action in the message; the scenarios assert both. Raising
the duplicate refusal as InvalidCheckerOptions fails the duplicate scenario.

* refactor(stryker-js-typescript-checker): move check-rounds decisions into a workflow and schema operations

Round-3 ruling Q1 step 1 and Q2. Each decision check-rounds.ts made inline now
lives where the mutation set and a law reach it:
- concludeSharedRound (new workflow): a shared round is kept only when every
  member took the shortcut, else redone one mutant at a time.
- importerCheckOf / readsEditSites (Checker.schema.ts): the location-rule default
  and which mode reads edit sites.
- shortcutCountsOf (ImporterShortcut.schema.ts): per-outcome shortcut and
  fallback counts, zero under 'always'.
- ShortcutClause / ShortcutTree move beside the decision they tag.

Planted bugs: default flipped to 'always', counts under 'always', unread folded
into none each fail an in-source law; every->some in concludeSharedRound fails
the shared-round integration scenario (the vacuity gate refused a redo law, so
it was deleted rather than reshaped).

* refactor(stryker-js-typescript-checker): run the check rounds on the compiler handle and delete check-rounds.ts

Round-3 ruling Q1 step 2. With every decision moved into a workflow or schema
operation (previous commit), what is left of check-rounds.ts is sequencing over
the handle's lastMutants and snapshot state, so it moves back into
ts-compiler.handle.ts and the unsuffixed module is deleted (no shim).

check keeps its SpanTaxonomy span (typescriptCheckerCompilerCheck), as every
Effect.fn in the repo names a taxonomy span. The helpers only the round loop
used (CheckSteps, checkStepsOf, classifyBatchTce, annotateDiagnosticSample,
CompilerState, EditTree, emptyTally, runtimeOf, TSCompilerRuntime) are no longer
exported.

* test(stryker-js-typescript-checker): pin the importer shortcut for a set accessor body

Round-3 finding P2 (testing). stored.ts pairs an annotated getter with a set
accessor; the rule scenario gains a row whose edit sits in the setter body. It
takes the shortcut and gets the same verdict under both importerCheck values.
Dropping SET_ACCESSOR from SIGNATURE_FREE_KINDS fails the scenario.

* refactor(stryker-js-typescript-checker): group mutant ids by file in one schema operation

fileIdsOf in FileIds.schema.ts groups ids by file, files in first-seen
order and ids in input order. groupMutants and planCheckRounds both use it
instead of two copies of the same accumulator. Law:
∀m_FileIdsOf_≡FilesInFirstSeenOrderEachHoldingItsIdsInInputOrder; planting
prepend for append fails it.

* refactor(stryker-js-typescript-checker): pick a mutant's diagnostics from its decision in one place

decidedOutcomeOf reads own-file errors when the decision takes the
importer shortcut and every program's errors otherwise; the solo path and
kept shared rounds both use it. Swapping its branches fails three
importer-shortcut scenarios.

* fix(stryker-js-instrumenter): render an error as errno only when it names its syscall

An error with a string code but no syscall (Node ERR_* errors, the
checker's InvalidCheckerOptions and DuplicateMutantIds) rendered as
'<name>: <code> (undefined) <stack>'. It now renders as '<name>: <message>'
and its stack. Law ∀e_ErrorTextOf_≡ErrnoFormatOnlyWhenTheErrorNamesItsSyscall;
the checker refusal scenarios assert the rendered cause starts with
'<Tag>: typescript checker (<code>): ' and holds the next action. Planting
the old predicate fails the law and both refusal scenarios.

* fix(stryker-js-typescript-checker): refuse unknown typescriptChecker keys and split the speed gate out of compare-sides

Review fixes for the full-path ce-code-review of 8457ce7 (run 20261013-173047-a6b753e3), validator-confirmed findings 1, 3 and 6:

- 1 (P1): compare-sides.workflow.property.test.ts crossed 1000 lines (849 -> 1033) in this diff. The speed gate moves into its own speed-gate.workflow.ts with its six laws in speed-gate.workflow.property.test.ts; compare-sides calls it and reads the decision. Same violations, warnings and summary values; 35 laws before and after.
- 3 (P2): the typescriptChecker block was decoded with the default excess-property policy, so a misspelled key (importercheck) was silently ignored while the published JSON schema refuses it. The block is now decoded on its own with onExcessProperty: 'error' and fails with InvalidCheckerOptions; the option scenario gains a misspelled-key and an extra-key row, red under the old decode.
- 6 (P3): the isolatedDeclarations zero-shortcuts nextAction told the operator to exercise an importer re-check path, the opposite of what the gate checks.

---------

Co-authored-by: Ryan Lee <drdgvhbh@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants