Skip to content

refactor(repo)!: move every Layer out of service ports into driver modules - #261

Closed
systemfsoftware-maker wants to merge 26 commits into
mainfrom
stream-a/l1-ports-topology
Closed

systemfsoftware-maker wants to merge 26 commits into
mainfrom
stream-a/l1-ports-topology

Conversation

@systemfsoftware-maker

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

Copy link
Copy Markdown
Collaborator

Summary

A consumer can now import any service contract without pulling in its implementation. No *.service.ts file that declares a Context.Service tag builds or exports a Layer any more. Each of those Layers now lives in a driver module (src/drivers/<what-it-binds>.ts) in the owning package, and the e2e harness follows the same pattern. This is layer L1 of the plan in docs/plans/2026-10-09-2237-refactor-ports-split-capability-packages-plan.md. L2 splits packages/stryker-js into one package per capability, and it depends on this change.

  • ADR-0003 (docs/adr/0003-service-ports-declare-no-layer.md) supersedes ADR-0001's *.service.ts row and extends its src/drivers/ row. It was drafted as 0002, but refactor(stryker-js): retire the Checker plans and protocol modules #260 took that number on main first, so it is now 0003 and cites ADR-0001's rows by name rather than by line.
  • Gate: gritlint.json enables the cell-architecture pack, and flake.lock pins the root systemfsoftware flake input, which builds gritlint, to upstream main ba6664c18 (systemfsoftware#701 then #704). The two 45f2deb entries that arrive through the published stryker-js release's own lock are unchanged. pnpm lint:conventions in check:ci now refuses a service module that builds, re-exports, or aliases a Layer.
  • attw joins check:ci (pnpm attw, through each package's existing attw --pack .). @systemfsoftware/oxlint-ignorer-config was the one published package without the script. It now has one, plus the ESM-only .attw.json that the other ESM packages carry. The first CI run on this PR then showed that stryker-js-plugin-interface and stryker-js-cli-contract had the script but no .attw.json, so they fail NoResolution and CJSResolvesToESM. They get the same file, and a local turbo run attw --continue now passes all 51 tasks.
  • Packs: .compound-engineering/config.yaml declares all four: cell-architecture, boundary-testing, schema-laws, and package-topology.
  • Breaking changes: static layer members and the *Live aliases are gone. The replacements are Engine.stage/Engine.forStream (previously RunEnvironment.stage/forStream), RunEvent.drainLayer, Worker.workerOptionsLayer/workerTelemetryLayer, and the ./runtime entry checkerRuntimeLayer(options). Changesets: core-service-ports-declare-no-layer, plugin-runtime-worker-layers, and typescript-checker-runtime-layer are major; vitest-runner-session-driver, oxlint-ignorer-config-attw, and plugin-interface-attw-config are none.

How an agent consumes this

  • gritlint finding. gritlint check --format json emits findings[] with rule (cell-architecture/service-exports-no-layer, a stable reason code), path, line, and message, plus a summary with per-rule file counts. The exit code is 1 on a finding and 0 when clean.
    • The rule is multifile. It reports once per directory, naming the alphabetically first *.service.ts file at line 1, so an agent has to search that directory's service modules for the Layer. The message's trailing (directory ...) names the directory, and the message says so.
    • The message ends with the next action, verbatim from the pinned pack: Next: move the Layer to src/drivers/<what-it-binds>.ts as layer, delete its export from the service module, and import the driver at the composition root.
    • The rule refuses a Layer.* call, a class field named layer or *Layer, a *Live export, a re-export of layer/*Layer/*Live, and an exported binding (const, let, var, default, including annotated, as, and satisfies forms) bound to one. A type-only re-export is allowed. export * from and a Layer under an unrelated name are not reported; the pack README lists these gaps.
  • attw step in check:ci.
    • pnpm attw runs turbo's attw task, which runs attw --pack . in each published package.
    • The @systemfsoftware/arethetypeswrong-cli fork prints one JSON line per package: status, packageName, packageVersion, problems[] (each with kind, entrypoint, and resolutionKind, where kind is attw's stable problem code such as NoResolution), and problemCounts. It then prints a one-line hint naming the --include flags that add more detail.
    • A failing package exits non-zero, and check:ci records s=1 and keeps running the remaining steps.
    • The fix for NoResolution on an ESM-only package is the .attw.json shown in the attw output section below. The envelope carries no schema-version field; that is the fork's format, and this PR does not change it.
  • ADR-0003's rule.
    • The *.service.ts row ("Never holds: a Layer value, a function returning a Layer, a static layer or *Layer member, a *Live name; in a file that declares a tag, driver imports") is enforced by the gritlint rule id above, except for the driver-import clause.
    • The driver-import clause is checked by review. Its check is the command in the ADR's Confirmation section, git grep -nE "from '(\./|\.\./)+drivers/" -- '*.service.ts', whose expected output is only plugin-loader.service.ts, a module with no tag that plan U11/U12 rehomes. In L2, tsc -b on the contracts package takes over.
  • CI. This PR adds or changes no CI job. check:ci gains the pnpm attw step inside the existing job, so there is no new $GITHUB_STEP_SUMMARY or annotation surface.

Review

  • Run: report-only ce-code-review, full spine, at 5d8a97ee4. Reviewers: correctness, adversarial, project-standards, testing, maintainability, api-contract, and learnings, plus a validator.
  • Coverage gap: the cross-model review did not run, because no other-model CLI (codex, grok, cursor-agent, opencode) is installed on the host. The adversarial lens ran in-process on the same model as the other reviewers, so no finding here has independent cross-model corroboration.
  • Verdict: Ready with fixes. R6-R8 and R11 belong to L2, as the plan says.
# Sev Finding Disposition
1 P2 stryker-js-plugin-runtime/README.md named the removed Worker.WorkerOptions.layer Fixed: it now names Worker.workerOptionsLayer. A repo-wide sweep for removed names found one more, docs/solutions/runtime-errors/stage-scope-closed-with-exit-void-hides-run-outcome.md, which now names Engine.stage and temporaryDirectoryLayer.
2 P2 The gate misses a re-exported or aliased Layer Fixed upstream in systemfsoftware#704 (merged as ba6664c18) and re-locked here in 0bb9c503e. #704 also refuses the annotated, as, satisfies, and export default forms. With the re-lock, the clean tree still exits 0, so no *.service.ts here re-exports or aliases a Layer. The probes below show each form refused.
3 P2 attw skipped the published @systemfsoftware/oxlint-ignorer-config Fixed: the package now has the attw script, the fork devDependency (catalog, one pnpm-lock.yaml importer entry), and .attw.json. Output is below.
4/5 P3 ADR's driver-import clause was unenforced and broken by plugin-loader.service.ts Fixed in the ADR: the clause now binds only files that declare a tag, which is true today (grep below). The tagless plugin-loader.service.ts is named as moving in plan U11/U12, and Confirmation names the check that takes over.
6 P3 JSDoc on VitestSessionInput and VitestSessionShape was dropped in the move Fixed: both blocks were restored verbatim from main.

Driver imports in *.service.ts files (the grep for #4/#5):

$ git grep -nE "from '(\./|\.\./)+drivers/" -- '*.service.ts'
packages/stryker-js/src/plugin-loader.service.ts:17:import { importModule } from './drivers/config.js'

plugin-loader.service.ts declares no tag.

Evidence

attw on @systemfsoftware/oxlint-ignorer-config

Without .attw.json:

{"status":"ok","packageName":"@systemfsoftware/oxlint-ignorer-config","packageVersion":"0.1.2","types":{"kind":"included"},"problems":[{"kind":"NoResolution","entrypoint":".","resolutionKind":"node10"},{"kind":"NoResolution","entrypoint":".","resolutionKind":"node16-cjs"}],"problemCounts":{"NoResolution":2}}
[ELIFECYCLE] Command failed with exit code 1.

With .attw.json, byte-identical to the 14 sibling ESM packages:

$ attw --pack .
{"status":"ok","packageName":"@systemfsoftware/oxlint-ignorer-config","packageVersion":"0.1.2","types":{"kind":"included"},"problems":[],"problemCounts":{}}
exit 0

pnpm lint:conventions probes

These runs use gritlint built from the locked input ba6664c18 (/nix/store/wmsl4wda…-gritlint-0.1.0). Each probe file was packages/stryker-js/src/zz-probe.service.ts, deleted after its run, and the tree was clean afterwards.

Tree Exit Output
Clean head 0 cell-architecture/service-exports-no-layer: evaluated 39 file(s); clean: 1119 file(s) selected
Probe export { layer as gitDiffLayer } from './drivers/git-diff.js' 1 packages/stryker-js/src/Sandbox.service.ts:1: cell-architecture/service-exports-no-layer (the directory's first service module); evaluated 40 files
Probe import { layer } from './drivers/git-diff.js' plus export const gitDiff = layer 1 Same finding
Probe export { layer as GitDiffLive } from './drivers/git-diff.js' 1 Same finding
Probe deleted 0 evaluated 39 file(s), clean

With the 6a7c247b3 lock that came before, the first two probes exited 0. The gritlint check --format json . finding for the re-export probe:

{"rule":"cell-architecture/service-exports-no-layer","path":"packages/stryker-js/src/Sandbox.service.ts","line":1,"message":"A `*.service.ts` module in this directory builds or exports a `Layer`: a\n`Layer.*` call, a class field named `layer` or `*Layer`, a `*Live` export, a\nre-export of `layer`, `*Layer` or `*Live`, or an export bound to one. The\nfinding names the directory's first service module; search every `*.service.ts`\nin the directory. Next: move the Layer to `src/drivers/<what-it-binds>.ts` as\n`layer`, delete its export from the service module, and import the driver at\nthe composition root. (directory `packages/stryker-js/src`)"}

Local verification

Verification was targeted because the host has little memory.

  • Merged head (origin/main merged up, ADR renumbered):
    • tsc -b in packages/stryker-js passes.
    • The core unit and property suite passes: 191 files, 1,137 tests.
    • turbo build for @systemfsoftware/stryker-js passes, as does api:check.
    • lint:conventions exits 0.
  • Earlier on this branch:
    • tsc -b and oxlint pass for each changed package.
    • attw passes for each changed package.
    • Plugin-runtime, typescript-checker, and vitest-runner tests passed, with 6, 191, and 23 tests respectively.
    • After the comments-only JSDoc restore, vitest-runner was rerun: 5 of 6 files passed. kill-first-ordering.integration.test.ts failed to start because a vitest pool worker timed out ([vitest-pool-runner]: Timeout waiting for worker to respond). That is host memory pressure, not an assertion failure. CI runs the file.
  • Mutation: no local mutation run; that happens in CI on main.

Merge order

This PR shares 8 files with #258 (stryker/mutant-quality-l1-reasons, also based on main): packages/stryker-js/etc/stryker-js.api.md, src/Serve/Serve.cell.ts, src/mutation-reporting.service.ts, and five integration tests and fixtures. Whichever PR lands second will need origin/main merged in, and the API report regenerated.

Brainstorm and plan for Stream A L1 (ports split from layers, ADR-0002
superseding ADR-0001's service row, package-topology pack declared) and
L2 (core split into packages derived from the import graph). The L1 code
PR carries this plan to main.
Supervisor rulings on the doc review: the service-file Layer check becomes a
bundled gritlint pack (U20) with violating fixtures, attw joins check:ci in L1
(U21), publint waits for operator approval (U17), the ADR takes the next free
number and leaves ADR-0001 line 60 alone, and the L2 cycle claim waits for a
real turbo run. A tsc probe showed composite projects already refuse a
relative import that escapes the package (TS6059, TS6307), so no rule is
added for that.
All four Stream A packs now resolve for CE skills (cell-architecture, boundary-testing, schema-laws, package-topology).
ADR-0002 supersedes ADR-0001's *.service.ts row: every Layer that satisfies a service moves to src/drivers/, following cell-architecture ports-separate-from-layers over the tier-2 static-layer clause.
Every published package already has an attw --pack script and turbo defines the task; check:ci now runs it so a broken published type shape fails the gate.
…ce ports

WorkerOptions and WorkerTelemetry declare only their tags; their layers live in src/drivers/ and the Worker namespace exports them as workerOptionsLayer and workerTelemetryLayer. trace-context-rpc, which declared no tag, moves to src/drivers/. ADR-0002.
…yer to a driver

CheckerRuntime.service.ts declares only the tag, so importing the port no longer loads typescript/unstable/async. The layer is checkerRuntimeLayer in ./runtime; the tagless CheckerWorker handlers move to src/drivers/. ADR-0002.
… drivers

VitestSession.service.ts declares only the tag; the session layer and the tagless VitestRunner and TestRunnerWorker modules move to src/drivers/. ADR-0002.
The fixture-cache, guest-job and stryker-cli-runner ports declare only their tags; their layers and the tagless harness-telemetry layer live in src/Harness/drivers/. ADR-0002.
…ings

L1 units already ran against the 1848 plan, so the corrected contract is a
new dated plan that declares supersedes and replaces it.

- Config: @systemfsoftware/stryker-js/config stays the one access path and
  owns defineConfig and mergeConfig; the config schema goes to contracts,
  loading to the engine, and no config package is created (U11). The
  loader's flag overlay becomes a parameter so the engine never imports
  the CLI's mergeConfig.
- mutation.yml: the L2 PR enrols the new packages itself under a root
  ruling that overrides the read-only note for this change (U18).
- The package-topology checks attw does not cover stay gritlint rules.
…, and checker streams

Supersedes the 1955 plan with KTD12. Four streams (F native instrumenter,
G execution engine, H tsgo checker, I subsumption and SMT) now work off
main. L2 freezes the Checker and TestRunner port shapes in
stryker-js-plugin-interface and adds a driver-agnostic Instrumenter port to
stryker-js-contracts (U22): selection crosses it as data, while the JS
mutators, ignorers, and format registry become construction inputs of the
JS driver's Layer. The native addon package is reserved as a driver beside
stryker-js-instrumenter; only the CLI depends on a driver.
Thirteen core *.service.ts ports (U6 platform-facing, U7 run-scoped) now declare only their tags; their layers live in src/drivers/. The run-stage composition moves to drivers/run-stage.ts and is exported as Engine.stage and Engine.forStream; RunEventDrainLive becomes RunEvent.drainLayer. Moved bodies are unchanged. ADR-0002.
The TypeScript grammar pinned in gritlint fails to parse two generic call signatures separated only by a newline, and dprint strips the separators that would fix it. A parse failure stops the gritlint scan, so the cell-architecture pack (ADR-0002) could not grade the repo. The intersection type resolves calls in the same order; the body is unchanged.
…cture gritlint pack

lint:conventions now fails when a *.service.ts module makes a Layer.* call, declares a layer or *Layer class field, or exports a *Live name (ADR-0002). The systemfsoftware input is locked to the head of systemfsoftware/systemfsoftware#701, which adds the pack; it moves to the merge commit on main before this lands.
fixture-cache.service.ts declares a port and imported the harness telemetry driver for its span helpers, so the port reached a driver (ADR-0002). The span names and helpers move to seam-span.ts; drivers/harness-telemetry.ts keeps only its layer.
portLayer is Layer.succeed with no requirements, so providing fileDrainLayer to it built a drain nobody read; the same memoized fileDrainLayer is already a member of the probe group.
systemfsoftware#701 merged as 6a7c247b3, so gritlint's cell-architecture pack now comes from upstream main instead of the PR branch commit b2c5078a. The pack source is unchanged (same narHash).
Supersedes the 2025 plan. Stream H adds a TypeQuery port as the ./type-query entry of stryker-js-plugin-interface; when L2 moves ports into stryker-js-contracts, that port moves with them.
Review fixes on the L1 head:
- plugin-runtime README names Worker.workerOptionsLayer, not the removed
  Worker.WorkerOptions.layer.
- The stage-scope solution note names Engine.stage and
  temporaryDirectoryLayer, the names that replaced RunEnvironment.stage
  and TemporaryDirectory.layer.
- VitestSession.service.ts gets back the two JSDoc blocks the move
  dropped, verbatim from main.
- ADR-0002 (not yet on main) binds driver imports only in files that
  declare a tag, which is what holds today: the one *.service.ts that
  imports a driver, plugin-loader.service.ts, declares no tag and moves
  in plan U11/U12. Confirmation names the grep that shows it and the
  check that takes over in L2 (contracts tsc -b, U10/KTD8).
…kage

check:ci runs attw through each package's script, and this published
package had none, so turbo skipped it. It gets the same script, the
arethetypeswrong fork devDependency, and the ESM-only .attw.json the
14 other ESM packages carry byte for byte. attw --pack . exits 0.
main merged ADR-0002 (schema-file operations, #260) while this record
waited under the same number. Plan U2 takes the next free number, so it
is now ADR-0003. It cites ADR-0001's rows by name, since #260 moved
their lines, and names ADR-0002 as the owner of the one clause it
supersedes, so "every other clause stands" is true again.
check:ci now runs attw, and CI's check job failed on
stryker-js-plugin-interface (NoResolution under node10,
CJSResolvesToESM under node16-cjs). turbo stopped there; a local
`turbo run attw --continue` showed stryker-js-cli-contract fails the
same way. Both are ESM-only and were the two published packages without
the .attw.json the other ESM packages carry. They get it byte for byte,
and all 51 attw tasks pass.
…lease change

Changeset Check counts .attw.json as a move of the package. The file changes no published byte.
… rule

systemfsoftware#704 merged as ba6664c18. It extends cell-architecture/service-exports-no-layer to refuse re-exported, aliased, annotated, asserted and default-exported Layers, and its finding message names the next action. Only the root systemfsoftware input moves (6a7c247b3 -> ba6664c18); the two 45f2deb entries from the stryker-published lock stay.
ryanleecode
ryanleecode previously approved these changes Oct 10, 2026

@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: CI green on 0bb9c50, diff read, no open threads.

Brings in #262, #263 and #265 (main 4bf5952).

One conflict, in packages/stryker-js/src/mutation-reporting.service.ts.
#262 added `rememberedMutantIds` to MutationReportingInput and a
stampRemembered step to the incremental-report writers. On this branch
the service file declares the port only and the implementation lives in
drivers/mutation-reporting.ts, so the input field goes on the port and
stampRemembered plus its two call sites go into the driver.

#262's check-cost-workspace fixture called Engine.RunEnvironment.stage,
which this branch renamed to Engine.stage; the fixture now uses that.
@ryanleecode

Copy link
Copy Markdown
Contributor

Closing without merging, superseded: this moves Layers into src/drivers/.ts, a technical-kind folder, which the cell-architecture placement settled in systemfsoftware#708 and #728 rejects. The rule now: a pure implementation stays as static layer on the service class, a driver-backed implementation sits in a flat sibling module beside its service that only composition roots and tests import, and there are no drivers/ folders. It's also 106 files on a day-old base. The rework comes as a fresh PR off main. The branch stream-a/l1-ports-topology is kept.

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