Skip to content

refactor(machines,tooling): give the state machines their own folder - #98

Merged
Theauxm merged 6 commits into
mainfrom
refactor/ws46-state-machine-folder
Oct 20, 2026
Merged

Theauxm merged 6 commits into
mainfrom
refactor/ws46-state-machine-folder

Conversation

@Theauxm

@Theauxm Theauxm commented Oct 20, 2026

Copy link
Copy Markdown
Member

The state machines get a folder of their own, laid out by language

Background

A Trax machine is a multi-step flow (a checkout, a form) whose state the server stores and
whose transitions it applies. It has two engines that must give the same answer for every trigger:
the C# one, which the server runs and which is the source of truth, and a TypeScript twin, which a
browser runs to predict what the server will do. A conformance kit (shared machines, fixtures,
canonical-wire vectors, refusal codes, the IR's JSON Schema) and a runner that sends every case to
an engine hold the two together.

Until now the C# engine (Trax.Effect.StateMachine, .Persistence, .Testing and their three
test projects) lived inside Trax.Effect/, and the TypeScript engine, with the kit, lived in a
top-level folder named Trax.Api.StateMachine/ after the place it was first used. CI decides what
to run by top-level folder: a folder runs when it, or a folder upstream of it, changed.

Problem

🟪⬛ A TypeScript change reran all of Trax.Effect
Trax.Effect listed Trax.Api.StateMachine upstream, because its machine tests read the corpus
and run the TypeScript engine under node. Touching one line of the TypeScript engine rebuilt and
retested every Effect project against Postgres and RabbitMQ, and every Effect run installed the
engine's npm packages.

🟪 The machines had no home of their own
Machines are the one layer that shared a folder with three others, so their decisions were split
across Effect's ADRs and the central ones, they had no AGENTS.md, and nothing stopped an Effect
project from referencing a machine project or a machine project from reaching the mediator.

🟪 There was nowhere for a third language to go
The kit sat inside the TypeScript engine's folder, so the next engine would have had to read
another language's directory to find the cases it must answer.

Example

One commit that changes only Trax.StateMachine/ts/src/index.ts, run through
.github/scripts/ci-changes.sh pull_request:

Before (the same change in Trax.Api.StateMachine/src/index.ts) After
Trax.Effect job runs does not run
C# machine tests run (inside Trax.Effect) do not run (state-machine-dotnet is not selected)
TypeScript engine state-machine job state-machine-ts suite
Tests that drive both engines run (inside Trax.Effect) state-machine-conformance suite

A change only under dotnet/ selects state-machine-dotnet and state-machine-conformance and
not state-machine-ts. A change to conformance/ selects all three. Folders downstream
(Mediator and below) still run for any change in the folder, since detection is per top-level
folder.

How this solves it

🟪 One folder, one directory per language

Trax.StateMachine/
  AGENTS.md  README.md  docs/adr/      bind every language
  dotnet/       Trax.StateMachine.slnx, Directory.Build.props, src/, tests/ (with Tests.Meta)
  ts/           @traxsharp/state-machine (name unchanged)
  conformance/  machines/, ir.schema.json, canonical-wire.json, refusal-codes.json,
                strict-input/, PROTOCOL.md, run.mjs, kit.mjs

dotnet/ is laid out like every other .NET folder, so the Meta guards that find their folder by
its .slnx work unchanged. The runner and kit.mjs use only node's own modules and now sit in
conformance/ with the corpus, so the C# tests read and spawn them without reaching into ts/.

flowchart LR
    Core["Trax.Core"] --> Effect["Trax.Effect"]
    Effect --> SM["Trax.StateMachine"]
    SM --> Med["Trax.Mediator"]
    Med --> Sch["Trax.Scheduler"]
    Sch --> Api["Trax.Api"]
    Sch --> Cli["Trax.Cli"]
    Api --> Samples["Trax.Samples"]
    Cli --> Samples
    classDef trains fill:#22c55e,stroke:#15803d,color:#000
    classDef machines fill:#a855f7,stroke:#7e22ce,color:#fff
    classDef dispatch fill:#f97316,stroke:#c2410c,color:#000
    classDef surface fill:#3b82f6,stroke:#1d4ed8,color:#fff
    classDef tooling fill:#334155,stroke:#0f172a,color:#fff
    class Core,Effect trains
    class SM machines
    class Med,Sch dispatch
    class Api surface
    class Cli,Samples tooling
Loading

🟪 The packages and namespaces are renamed
Trax.Effect.StateMachine, .Persistence and .Testing become Trax.StateMachine,
.Persistence and .Testing, namespaces included. Types, members, tables and the wire are
unchanged. The new ids have no release yet, so package validation is off for them until their
first one, and the old ids' CompatibilitySuppressions.xml go. Every InternalsVisibleTo stays
as it was, renamed. Effect already grants internals to the folders below it, and making these
members public would widen what a consumer sees for nothing.

⬛ Three suites instead of a folder job
Trax.StateMachine has a packages.json row (upstream Trax.Effect, its ADR guard) but no
dotnet key, so no folder job. Its work is three suites, each with its own paths:
state-machine-dotnet (the whole C# solution, with the folder job's format, lock, deprecation,
pack and coverage steps behind a new checks key, and tests filtered to
TestCategory!=CrossEngine), state-machine-ts (the existing npm job, now selected by suite
name), and state-machine-conformance (the C# tests marked CrossEngine, then
node conformance/run.mjs through the TypeScript testee). The release's pack list, the
Dependabot lockfile workflow and the coverage upload now include suites.

🟪⬛ The dependency direction is enforced
Every folder's DependencyDirectionTests gains the Trax.StateMachine family between Effect and
Mediator, with cases pinning that an Effect project referencing a machine project and a machine
project referencing the mediator both fail. Effect's ADRs on request-id replay, the effect runner
and the transition history move to Trax.StateMachine/docs/adr/ as 0001 to 0003, with the tests
that are their exemplars.

Warning

Renamed package ids. A consumer swaps Trax.Effect.StateMachine* for Trax.StateMachine*
and the usings with it (Trax.Docs/migration-guides/trax-statemachine-packages.md). This PR
is a refactor: and publishes nothing; the new ids first ship with the next release, and the
old ones are deprecated on nuget.org after that. The book's Directory.Packages.props still
pins 1.61.0, which the new ids will never have; in the repository a project reference replaces
it, and the pin moves to the first release that carries them.

Architectural hinges

  • 🟪 Trax.StateMachine/AGENTS.md: the folder's layout by language, its decisions, and which
    CI suite runs for which path. Read this first.
  • ⬛ .github/ci/packages.json (the Trax.StateMachine row and the three state-machine-*
    suites): where the "a TypeScript change does not rebuild C#" rule is decided, by paths.
  • ⬛ .github/workflows/ci.yml (suite job): the checks, pack, coverage and
    conformance steps a suite can now take on, so a whole solution can stand in for a folder job.
  • 🟪 Trax.StateMachine/dotnet/tests/Trax.StateMachine.Tests/Helpers/FixturePaths.cs
    (FixturePaths): how the C# tests find conformance/machines and the TypeScript engine; it
    throws rather than skipping when they are missing.
  • ⬛ Trax.StateMachine/dotnet/tests/Trax.StateMachine.Tests.Meta/Tests/DependencyDirectionTests.cs:
    the chain with the new family, identical in every folder's Meta project.

…/dotnet

The engine, its persistence and testing packages and their three test
projects leave Trax.Effect for a folder of their own. Everything .NET sits
under dotnet/ so the folder can hold the other languages beside it; the
guards that enumerate each folder's src/ and tests/ also look there.
Names are unchanged in this commit.
…conformance/

Trax.Api.StateMachine becomes Trax.StateMachine/ts; @traxsharp/state-machine
keeps its name. The conformance kit, the shared machines/ corpus and the
runner (run.mjs, kit.mjs, node builtins only) move to
Trax.StateMachine/conformance, beside the engines rather than inside one.
The C# tests that run or read the TypeScript engine are marked CrossEngine,
so CI can run them apart from the C#-only suite.
…achine

Trax.Effect.StateMachine, .Persistence and .Testing become
Trax.StateMachine, .Persistence and .Testing, with their namespaces and
test projects. The new ids have no release on nuget.org yet, so package
validation is off for them until their first one, and the old ids'
CompatibilitySuppressions go. Shipped migrations keep the old names in
their comments.
…ateMachine

Trax.StateMachine gets its own Tests.Meta with the folder-agnostic guards
and the three public API baselines, which leave Effect's. Every folder's
DependencyDirectionTests gains the Trax.StateMachine family between Effect
and Mediator, so an Effect project that references a machine project, or a
machine project that references the mediator, fails the build; both are
pinned as cases.
Trax.StateMachine has no folder job. Its C# solution, its TypeScript
engine and the cross-engine checks are three suites with their own paths:
state-machine-dotnet runs for dotnet/ and the kit, state-machine-ts for ts/
and the kit, and state-machine-conformance, the C# CrossEngine tests plus
the kit through the TypeScript engine, for anything in the folder. A change
to one language no longer rebuilds the other, and none of them reruns
Trax.Effect.

A suite marked checks makes the folder job's checks itself, and the
release, the Dependabot lockfile workflow and the coverage upload include
suites.
…he move

effect/0013, 0017 and 0022 move to Trax.StateMachine/docs/adr as 0001 to
0003, with the tests that are their exemplars. Trax.StateMachine gets its
AGENTS.md, by language, merged from the TypeScript engine's and Effect's
machine rows; the root AGENTS.md gains the folder, the chain and the
suites. The central ADRs that bind every folder, and the machine ones, are
tagged statemachine. The READMEs, the packages reference, the repository
layout page and the site's package list gain the folder, and a migration
guide covers the renamed ids.
@vercel

vercel Bot commented Oct 20, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated
trax-website Ignored Ignored Oct 20, 2026 5:55pm UTC

@codecov

codecov Bot commented Oct 20, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.66667% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
Trax.Cli/src/Trax.Cli/Commands/MachineCommand.cs 66.66% 0 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

@Theauxm
Theauxm merged commit dc72fa2 into main Oct 20, 2026
49 checks passed
@Theauxm
Theauxm deleted the refactor/ws46-state-machine-folder branch October 20, 2026 18:09
@github-actions

Copy link
Copy Markdown
Contributor

This PR is included in version 1.68.0

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.

1 participant