Repository navigation
refactor(stryker-js): keep a run's Project read-only through the sandbox - #268
Merged
Merged
Conversation
…box preprocessors Project.files and filesToMutate are ReadonlyMaps (insertion order kept, as MutableHashMap iterated). Sandbox preprocessors take a Project and return a new one; acquireSandbox writes the folded result and the caller's Project is untouched. The tsconfig rewrite moves out of Sandbox.blueprint.ts into rewriteSandboxTsconfig (escape, rewrite and follow decision) and sandboxTsconfigCell (read, parse, follow, write).
Laws over drawn TsConfigSchema values: escaping entries in all five positions move two levels up and others stay; the follow list is the non-escaping extends then the non-escaping references' tsconfigs; keys outside the five survive encoding; each read outcome maps to its decision.
…rojectFile as a schema sandboxTsconfigCell now rewrites a single tsconfig and returns the rewritten file plus its follow list; the blueprint walks the follow list depth-first, so a cell never runs another cell (ADR-0001 *.cell.ts row). ProjectFile is a production schema in Project.schema.ts built on the newly exported Instrument.MutateDescriptionSchema, and the withPreprocessedFiles law snapshots the caller's maps, so an in-place merge now fails it. referencedEntriesOf and entriesOf gain in-source laws, and run/instrument.ts keeps its law helpers inside the import.meta.vitest guard.
…dary The workflow property test now derives its expected escapes from the drawn entries instead of the production referencedEntriesOf. A new integration suite runs real mutation runs on a temp project and asserts the exact tsconfig text written into the sandbox, the missing-file skip, the unparsable-file warning and the in-place no-rewrite; it passes unchanged against origin/main d2f018d.
U1 executed against 2026-10-09-1840, so the corrected contract is a new dated plan declaring supersedes: KTD4 records ReadonlyMap with fresh maps and withPreprocessedFiles in Project.schema.ts (ADR-0002) instead of HashMap and a merge workflow; U2 lists the boundary suite, the one-tsconfig cell and the blueprint-owned follow walk.
ryanleecode
approved these changes
Oct 10, 2026
ryanleecode
left a comment
Contributor
There was a problem hiding this comment.
Root verified: 10/10 green on 0776b03, no open threads, no skips/disables.
systemfsoftware-maker
added a commit
that referenced
this pull request
Oct 12, 2026
…#297 The plan has executed units (#260, #268, #272), so the change lands as a new dated plan declaring `supersedes:` (harness rule plan-discipline); REPO-D2 caps a PR at one added plan file, which a replacement satisfies. - KTD12: the verdict cell is a Sandwich under a new span, stryker.mutationReporting.judgeScore. ADR-0001's cell row refuses Cell.fromEffect stage wrappers; judging inline in reportAll keeps the verdict's I/O out of a cell (R10). The taxonomy entry edits two files #297 edits, so U6 stacks on U3 (OP13b, P25). - R14: main's spans keep name, attributes and ancestors; a new span is a SpanTaxonomy entry with a cli-contract patch changeset, as #297 did. - U6 adds tests/drain-failure-outranks-verdict.integration.test.ts with a revert proof; P14 rewritten, P23 refreshed, P24 and P25 added.
ryanleecode
added a commit
that referenced
this pull request
Oct 13, 2026
…d cell (#317) * refactor(stryker-js): acquire the sandbox through cells whose gates are workflows (U3) Sandbox.blueprint.ts (511 lines) now holds only the kind, its withPreprocessor step and its scoped/layer targets (67 lines, one Blueprint.make, no Effect.fn). Its targets run acquireSandboxCell. planSandboxAcquisition decides from inPlace, backupDirectory, symlinkNodeModules and buildCommand: in place with or without a restore, copied with or without node_modules links, and the build command when it is non-empty. acquire-sandbox.cell.ts writes one straight line per variant. The restore stays an Effect.addFinalizer on the caller's scope. Build and link run after it through Cell.gate, never through cell.run. judgeSandboxBuild (sandbox-build.cell.ts) passes a zero exit and fails with the same message main built. classifySandboxDirectory (link-node-modules.cell.ts) skips the temp directory before it matches node_modules, as main's walk did. planNodeModulesLinks links every node_modules the walk found, in order. MakeSandboxInput, SandboxSpec and FilePreprocessor move to Sandbox.schema.ts. Laws for the four workflows draw their production command schemas. * fix(stryker-js): keep the build and node_modules link inside the sandbox acquisition The Sandbox blueprint now runs the acquisition: a stryker.sandbox.acquire span around the prepare cell, the build cell under its own scope, and the link cell. prepare-sandbox.cell.ts (was acquire-sandbox.cell.ts) decides in place or copied under the new stryker.sandbox.prepare span. The link cell runs on every path and logs "Start symlink node_modules" each time, as main does; planNodeModulesLinks decides NodeModulesNotLinked when the sandbox does not link. The build cell's read phase no longer closes a scope; the blueprint scopes each build run. The unused withPreprocessor step and the preprocessors field are gone. The sandbox-acquisition-telemetry suite pins the span tree, step order, symlink log and in-place restore for three paths; it passes on main's code and fails when the build and link steps are swapped or dropped. * docs(repo): refresh plan entry 6 (mutation verdict) against main ddeb67a Re-grounds U6 on main ddeb67a: refreshed line references (P20), the log texts and exit precedence no suite pins (P21), the four classify-exit survivors re-read from mutation-report-444 (P22), overlap with #298, #271, #297 and #277 with a merge-tree probe (P23), and the cell's span choice (P24, KTD12). U6 no longer stacks on U2, which merged as #268. Raises Q1-Q3 for the conductor before ce-work. * docs(repo): supersede the grab-bag plan with U6 as a Sandwich layer on #297 The plan has executed units (#260, #268, #272), so the change lands as a new dated plan declaring `supersedes:` (harness rule plan-discipline); REPO-D2 caps a PR at one added plan file, which a replacement satisfies. - KTD12: the verdict cell is a Sandwich under a new span, stryker.mutationReporting.judgeScore. ADR-0001's cell row refuses Cell.fromEffect stage wrappers; judging inline in reportAll keeps the verdict's I/O out of a cell (R10). The taxonomy entry edits two files #297 edits, so U6 stacks on U3 (OP13b, P25). - R14: main's spans keep name, attributes and ancestors; a new span is a SpanTaxonomy entry with a cli-contract patch changeset, as #297 did. - U6 adds tests/drain-failure-outranks-verdict.integration.test.ts with a revert proof; P14 rewritten, P23 refreshed, P24 and P25 added. * refactor(stryker-js): judge the mutation score in its own workflow and cell U6 of the grab-bag decomposition plan. The score-vs-threshold decision leaves mutation-reporting.service.ts and classify-exit.workflow.ts: - judgeMutationScore (judge-mutation-score.workflow.ts) decides one of five variants, one per verdict log branch on main; each carries a single-valued exitClass, so no arm can return null or undefined. - mutationVerdictCell is a Sandwich under the new span stryker.mutationReporting.judgeScore (KTD12); its write handlers emit main's log lines verbatim and answer the variant's exit class. - classifyExit keeps only precedence over pending classes; every decision variant carries its exitClass, so reportAll's reverse maps go. - reportAll drains, runs the cell, then ranks [verdict, terminalDrain] once; a drain RuntimeError outranks a VerdictFail by precedence. - SpanTaxonomy gains mutationReportingJudgeScore; span-taxonomy.json is regenerated with generate:contract. tests/drain-failure-outranks-verdict.integration.test.ts pins the drain precedence no suite graded (P21): it passes on the pre-change code and fails with the drain class dropped from reportAll's pending list. * test(stryker-js): trace the judgeScore span and decode the judge oracle Review ruling r29 on U6 (grab-bag decomposition plan, entry 6): - G2: tests/drain-failure-outranks-verdict.integration.test.ts gains a scenario that runs the same in-process mutation run under an InMemorySpanExporter and asserts exactly one stryker.mutationReporting.judgeScore span, parented by stryker.mutationReporting.reportAll, with verdict VerdictFail. With the cell replaced by the bare workflow the scenario fails (parents []). - G4: the judgeMutationScore laws decode the oracle through the ScoreJudgement schema and compare with its schema equivalence instead of JSON.stringify. - F1: the plan says the classifyExit pair and single laws were consolidated into the one pending-array law, and records the span scenario and its revert proof. * docs(repo): exclude docs/plans from the U6 removal check Review ruling r30: the plan's U6 Verification grep matched the plan's own mentions of the removed symbols, so it could never return nothing. It now excludes lock files and docs/plans, as earlier plans do, and returns no matches at this head. * test(stryker-js): grade drain-over-verdict through the built CLI Intent r38 (operator rules 2026-10-12): an integration test drives the built CLI or the published API and asserts only what a user observes. - tests/drain-failure-outranks-verdict.integration.test.ts now runs dist/main.mjs on a consumer project with an on-disk reporter plugin (tests/__fixtures__/refusing-reporter/) loaded through `plugins`. One outline: with the refusing plugin the run exits 3, without it 1; each row decodes the stdout verdict record and checks the score is under the break threshold. - The in-process span-parentage scenario is removed: parentage is not in the published span contract. - Plan U6 test lines drop the hand-applied mutant proofs and name the mutant classes as targets for the next main Mutation report. * docs(repo): cite src/bin/main.ts for the CLI OTEL_* reads in plan U6 Debate r39: the span-parentage line named bin/main.ts, but the file is packages/stryker-js/src/bin/main.ts. * fix(stryker-js): decode the break threshold into a tagged union before the verdict decides Review of #317 (run 20261013-032741-afbae46e, validator-confirmed): JudgeMutationScoreCommand carried breakingThreshold as S.NullOr(Percentage) and decide probed its presence, which schema-laws tagged-unions-over-state-by-presence and CONST-D4 forbid. The field now decodes the options' null|Percentage into NoBreakThreshold | BreakAt { threshold } (encode forbidden: the threshold is only ever read from options), and decide dispatches with Match.valueTags. The cell's read is unchanged; laws draw the field's type side. * fix(stryker-js): encode the tagged break threshold back to its options value The command field decoded null|percentage into NoBreakThreshold|BreakAt but refused to encode, so the generated in-source codec laws (∀x_JudgeMutationScoreCommandEnc_=x, ∀x_JudgeMutationScoreCommand_=x) failed on CI run 38344123637. Encode now maps NoBreakThreshold to null and BreakAt to its threshold, the inverse every other decodeTo in this package defines (Plugins.schema.ts). * docs(plans): supersede U6 with the tagged break-threshold codec Review of 9cf18e7 confirmed two P3s: the 1732 plan's U6 approach and test-scenario bullets still described breakingThreshold as S.NullOr(Report.Percentage) with a constructor-defaulted null exitClass. The 0456 plan supersedes section U6 only (as the 2348 plan does for U9), stating BreakThresholdFromOption, the tag dispatch, the decoded-side law draw and the plain S.Null exitClass. --------- Co-authored-by: Ryan Lee <drdgvhbh@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A sandbox preprocessor can no longer write into the
Projectit receives:filesandfilesToMutateare nowReadonlyMaps, each preprocessor returns a newProject, and the decision about how a tsconfig is rewritten for the sandbox moves into therewriteSandboxTsconfigworkflow, where property tests can now reach it. Before this change,mergeUpdatedFilecalledMutableHashMap.seton the caller's maps, and the tsconfig rewriter usedObject.assignon the caller'sProjectFile.This is PR 2 (U2) of the grab-bag decomposition series. PR 1 (#260) retired the Checker plans and protocol modules. U3 (sandbox acquisition cells) builds on this branch next.
What a reviewer should check
Engine.Project.files/filesToMutatechange fromMutableHashMaptoReadonlyMap<string, ProjectFile>. The changeset bumps@systemfsoftware/stryker-jsmajor (BREAK-1).etc/stryker-js.api.mdlistsEngine.Projectby name only, so the report itself does not change.Instrument.MutateDescriptionSchemais newly exported (instrumenter minor) soProjectFilecan be a schema rather than a hand-written copy.withPreprocessedFilessits inProject.schema.ts(ADR-0002), not in amerge-preprocessed-files.workflow.ts. Its law snapshots the caller's entries before the call and requires the returned maps to be new objects, so an in-place merge fails.ReadonlyMapis only a type-level guarantee, which is why the law checks this. An update whose name is not infilesis added tofilesonly, asMutableHashMap.setdid.sandboxTsconfigCellreads, decides, and returns the rewritten file plus its follow list.Sandbox.blueprint.tswalks the follow list depth-first and rewrites followed files before the parent, which is the pre-refactor order. A cell never runs another cell (ADR-0001*.cell.tsrow).extendscycle still recurses. Adding a visited set would change behaviour, so both stay as follow-ups.2026-10-09-1840, so the corrected contract is a new dated plan (2026-10-10-0431,supersedes:) and the old file is removed. KTD4 now recordsReadonlyMap+withPreprocessedFilesinstead ofHashMap+ a merge workflow.Sandbox.blueprint.tsgoes from 734 to 511 lines. The 400-line bar for it belongs to U3, which splits acquisition into cells.Tests
src/__tests__/rewrite-sandbox-tsconfig.workflow.property.test.ts: escape, shape and follow laws, with the expectation built from the drawn entries rather than from production helpers. The schema-derived command generator emits all three arms (TsconfigMissing,TsconfigUnparsable,TsconfigParsed). Checked by sampling 300 commands.withPreprocessedFiles(non-aliasing, last update wins, unknown names joinfilesonly);referencedEntriesOfandentriesOf(order, absent arrays contribute nothing).tests/sandbox-tsconfig.integration.test.ts(new): real mutation runs throughEngine.strykerCellon a temp project whose tsconfig extends and references files both inside and outside it. It asserts the exact text written into the sandbox, the missing-file skip, the unparsable-file warning, and that an in-place run rewrites nothing. It also passes unchanged againstorigin/maind2f018d, which is the parity evidence for "behaviour unchanged".mutation-report-420): the twoSandbox.schema.tssurvivors5d7353b99a24bd51and0e47ce21d936380fare the[JsonRecord]rest arrays ofTsConfigSchema(main lines 26 and 34, now 14 and 19). The integration fixture carriescompilerOptionsand a referenceprependkey and asserts the exact written text, so dropping either rest array should fail it. This is inferred: no mutation run happens locally.Run locally on the merged head:
pnpm typecheck(exit 0);vitest run src(196 files, 1179 tests);instrument-stage,framework-run,clean-temp-dir,plan-without-sandbox,sandbox-tsconfigandcompile-error-reuseintegration suites each run on their own, all passing;pnpm buildandapi:checkfor stryker-js and the instrumenter. The e2e lanes and mutation run only in CI.Review record
Report-only
ce-code-review(correctness, adversarial, project-standards, testing, maintainability, learnings). The validator confirmed 7 findings and rejected 2. All 7 confirmed findings are fixed here, along with the below-bar cell-runs-cell finding and the test-helper placement residual. The two rejected findings were a refactor preference and a placement preference. The cross-model pass did not run because no second-family CLI is installed in the container.Known pre-existing bug, not fixed here
On
main, a successfulinPlacerun leaves the mutated source instrumented on disk. Reproduced on d2f018d with the new suite's fixture. Likely cause [inference]: the in-place backup writesfile.content(the instrumented text) rather than the original (project-files.service.tswriteInto→read). The in-place scenario therefore asserts only on tsconfig files. A fix is out of scope for a behaviour-unchanged PR.Stacking note
This branch is a one-layer stack on
main. It shares one line with #261 (stream-a/l1-ports-topology) inmutation-reporting.service.ts(MutableHashMap.get→files.get), a file #261 almost entirely removes. Per the operator's direction for this series (independent PRs offmainthat Stream A merges up), it is not stacked on #261.