Skip to content

Changelog ​

한국어

Unreleased ​

  • S-44-23: AGENTS.md points to kit docs/plans/local-links.md: while polyspec versions are 0.0.x, development checkouts link installed polyspec packages to their source, and links are never committed.
  • S-44-22: the shared tools are kit 0.0.53, whose release proof checks that a release lists the asset of each sibling wheel.
  • S-44-21: the shared tools are kit 0.0.52, whose kit-check requires the .php-version of fsm and whose release workflow runs kit-check before any release step.
  • S-44-20: .php-version declares PHP 8.5, the minor of config/toolchain.json, and every setup-php step of the workflows reads it.
  • S-44-19: the shared tools are kit 0.0.51: make toolchain-lock starts with the node of the machine, and config/knip-entries.json declares the go, make and php that the scripts and tests run.
  • S-44-15: make lint-ts passes and make check runs it: config/knip-entries.json declares packages/db, packages/gui, packages/js and docs as knip workspaces.
  • S-44-17: the shared tools are kit 0.0.45, whose knip ignores the binaries of the pinned runtimes and reads the nested npm projects that config/knip-entries.json declares as workspaces.
  • S-44-15: orm is v0.0.15 in every kind (npm and fsm-db tarballs, go module, python wheel with rapidyaml and ruamel.yaml, cargo tag, Composer zip with purrphp/yaml), whose generated TypeScript models import only what they use and whose generated Rust models carry no allow attribute; the Go imports read github.com/polyspec/orm/packages/go/..., and config/knip-entries.json declares the generated models.ts, whose MANIFEST_TEXT and MANIFEST_HASH orm exports by design.
  • S-44-15: config/knip-entries.json declares the files that make or a test runs or reads by path outside kit's layout entries (the fsm-gen and fsm-gui commands, the GUI client entry, the golden npm files, the generated declarations and their consumer, the configuration of the documentation site) and the two GUI programs run by hand, so knip reports no unused file; isGenerated of write-generated.mjs is no export.
  • S-44-8-1: config/release-assets.json pins the release zips of orm and ordered-json, and make release-assets-fetch of kit downloads them into var/composer-artifacts, the Composer artifact repository of packages/php and packages/php-ext; the composer-artifacts recipe and config/composer-artifacts.txt are removed.
  • S-44-16: the shared tools are kit 0.0.44: make install-tools installs the Rust toolchain of rust-toolchain.toml, so CI runs no rustup step of its own, ruff keeps its cache in var/ruff, and knip takes the entries of config/knip-entries.json.
  • S-44-14: the PHP of fsm passes phpcs: the generated ConformanceTest.php runs one method testConformance over the data provider cases(), one data set per case named <feature>__<id>, which parity runs by its symbol (kit K137), and the generated throws take one line per argument.
  • S-44-13: rustls is 0.23.46, which fixes RUSTSEC-2026-0346 to 0349, and playwright is 1.64.1.
  • S-44-12: the PHP files of fsm put a blank line after <?php (PSR-12), so the fsm-gen header of a generated PHP file is its third line.
  • S-44-11: the generated rust of a machine without edges passes clippy: target returns the error alone instead of a match of one wildcard arm.
  • S-44-9: every test of a kind is the test of a parity binding: the persist tests are four feature units bound in all six kinds (two transitions of one row at the same time, and the deadlock retry), the npm type check is the root tool test tests/npm-types, and the rust conformance target is tests/conformance/main.rs with its module vectors.rs.
  • S-44-10: the shared tools are kit 0.0.43: parity counts the module files of a cargo target tests/<name>/main.rs as files of its binding, the oxlint configuration allows the registrations of node:test, knip takes the scripts, benchmarks and tests of the layout as entries, and API Extractor no longer asks every export for a release tag.
  • S-44-5: the folders follow kit's layout: the packages are packages/compiler, db, gui, go, js (the npm kind), php, php-ext, python and rust; the fsm project of the repository (fsm.config.json, fsm.lock.json, machines/, schema/ and conformance/vectors.json) is contracts/, and the tables of the db store are packages/db/schema/fsm.dbs; the root scripts and tool tests are TypeScript (.mts). The Go module path is github.com/polyspec/fsm/packages/go (it was github.com/polyspec/fsm-go).
  • S-44-8: the dependencies are at their latest releases: orm v0.0.14 and ordered-json v0.0.8 in every kind, whose generators read the set files dbspec-set.json; Composer takes the release zips of orm and ordered-json from an artifact repository pinned by sha256 in config/composer-artifacts.txt, and the Composer package copies of their metadata are gone; python takes the release wheels; hyper v0.0.13, mysql2 3.24.6, mermaid 12.2.0 and PHPUnit 13.4.2. The npm installs pass --allow-remote=root, the package tests run kit's run-tests from the checkout root, and CI sets up no Go of its own.
  • S-44-7: the shared tools are kit 0.0.42: mise.lock pins the tools for linux-arm64 and macos-x64 too, and the bootstrap writes var/tools/go.mod, so the Go of mise is a module of its own.
  • S-44-6: the shared tools are kit 0.0.40, whose mise installs the toolchain of mise.toml at the digests of mise.lock; the python package runs the uv and the Python of the shims of mise. The test of the Python of the checkout parses again; its quoting broke it in S-44-4.
  • S-44-4: the shared tools are the npm package @polyspec/kit 0.0.38, pinned by its release asset and integrity; the Makefile includes the copied scripts/kit/kit-bootstrap.mk, the vendored scripts/kit/, tests/kit/, .kit/kit.lock.json and kit.json are gone, and the stale TypeScript exception of packages/fsm-db is removed. make layout-check is recorded red as S-44-5.
  • S-44-3: config/parity.json follows kit's parity schema: no PYTHONPATH, support files for the generated go vectors and php-ext stub and header, and tools roots for the compiler, DB, GUI and root tool tests.
  • S-44-2: the toolchains are at kit's pins: Node.js 26.11.1, PHP 8.5, Python 3.14, Go 1.27.2 (no toolchain line), Rust 1.99.0 and TypeScript 7.0.2, with orm's TypeScript 6.0.3 in packages/fsm-db declared as an exception; uv and alint are declared in config/toolchain.json and installed into var/tools, and the CI workflow installs no uv of its own.
  • S-44-1: the compiler imports no file of kit: src/io/files.mjs and src/io/git.mjs write files atomically and run git on node built-ins, and the import test refuses a path into scripts/kit (plan C-5).
  • S-43: AGENTS.md(.ko) Rules: an existing outside tool is used as it is, its own symbolic links accepted; the repository creates no symbolic link and no relative path, and a path is named in a tracked file or found by a stated discovery rule.
  • S-34-4: the python lock is checked by uv itself (uv lock --check and the export of uv.lock against pylock.toml); fsm's own lock script is removed.
  • S-42-2: fsm's own vector parity script (scripts/check-parity.mjs, vector-parity-check, parity-<kind>) is removed; kit's parity check does the work, and each kind's owner check runs its package tests.
  • S-34-3: the python lock holds registry packages and release archives only: orm and ordered-json come from the archives of their v0.0.7 tags with their sha256, so both uv and pip install the lock with hashes, and the lock check refuses a git source.
  • S-42-1: the vector parity runs on kit's parity check (K33): config/parity.json, one vector file conformance/vectors.json with a case name per case, per-case PHPUnit methods, and scripts/check-parity.mjs keeps only the record, time-limit and feature-coverage rules.
  • S-21: fsm is public at https://github.com/polyspec/fsm. The CI workflow (.github/workflows/ci.yml) runs make install and make check with PostgreSQL and MySQL services on every push to main and passed the full suite (run 38121153369); the Pages workflow publishes the documentation site at https://polyspec.github.io/fsm/. The local repository directory is named fsm.
  • S-21-1: CI and Pages run on ubuntu-26.04, because orm's PHP client requires SQLite 3.46 and the PHP of ubuntu-24.04 links SQLite 3.45.1; CI run 38121153369 passed the full suite there, 47 of 47 targets (tree 4963dbe).
  • S-41-1: the python package venv is made by the kit tool (make python-venv DIR=packages/fsm-python/.venv, kit K44 in kit v0.0.24), not by python -m venv in fsm's Makefile. On Linux CPython's venv makes a lib64 -> lib link that run-tests --venv refuses, which failed the ubuntu CI of S-41 (run 38118092530); the kit tool removes that link. A test of tests/python-tools asserts that make -n install calls the kit tool (RED with -m venv). make install removes the venv before it makes a new one, as before.
  • S-35-3: make check runs vector-parity-check again; a rebase had put the kit name parity-check back into CHECK_TARGETS, so the suite ran kit K33's checker instead of fsm's vector parity, and a make-install test now asserts the target.
  • S-42: one parity tool runs in make check. CHECK_TARGETS named kit's parity-check (S-35-2 renamed the fsm target but not the list), so the vector records were never checked. It names vector-parity-check now, and a test of tests/check-parity reads the Makefile to require that. Kit K33 is not adopted: it runs each binding itself and reads no record, so it cannot state the record, kinds, feature, tool-test and time rules (plan C-4 lists each one).
  • S-34-1: the kit dependency gate covers every transitive python package of the python lock: its latest-release and advisory review reads packages/fsm-python/pylock.toml through pythonLocks (kit K36). RED: kit's python-lock fixture tests (K36) pass in kit-test; in fsm, dependency-policy-check fails with pythonLocks empty and passes with the lock listed and reviewed.
  • S-34-2: the python lock is packages/fsm-python/pylock.toml, the PEP 751 export of uv.lock (uv export --format pylock.toml --frozen --no-emit-project, kit K36). config/dependency-policy.json names it in pythonLocks, and make dependency-review RECORD=1 records its packages and the two git packages (polyspec-orm and polyspec-ordered-json at v0.0.7, by commit) in the review. make python-lock-check fails when the committed body differs from a fresh export (RED with the rule absent). The package venv is filled from this file (S-41).
  • S-41: python comes from make install-tools (pythonBuild 3.14.8 in config/toolchain.json, sha256 per platform), not from uv. install-python and tests/install-python are replaced by tests/python-tools (make python-tools-test). The package venv is a copy of that interpreter (python3 -m venv --copies --without-pip packages/fsm-python/.venv) filled with uv pip install --link-mode copy from packages/fsm-python/pylock.toml, which uv exports from uv.lock; uv venv and uv sync write links that the kit runner refuses. The python tests run run-tests.mjs python --venv, not a PATH entry, which fixes make test-python ("No module named 'polyspec'" on main). RED: python-tools-test fails on the uv-synced venv of the checkout. GREEN: test-python 290 cases, python-persist-test-postgres and python-persist-test-mysql on the running servers, parity-python, toolchain-check, make-install-test and workflows-test pass.
  • S-35-2: the vendored kit is v0.0.23 (make kit-sync KIT_TAG=v0.0.23, kit K36 and K37). config/dependency-policy.json gets the required field pythonLocks ([] until S-34-2 names the lock): the kit gate and review read the PEP 751 pylock.toml files it lists. The review record config/dependency-review.json gets the required vcs list ([]) from make dependency-review RECORD=1. The fsm target parity-check is renamed vector-parity-check (kit v0.0.23 defines parity-check, and the repository's target replaced it with a make warning); a test in tests/make-install fails on any make warning of make -n check. Kit's parity-check is not adopted yet: S-42 evaluates it (plan C-4). The review record (config/dependency-review.json) gets the required vcs list ([]) from make dependency-review RECORD=1, the v0.0.23 schema. run-tests.mjs python --venv now runs the venv interpreter only when its pyvenv.cfg names the install-tools Python. Known: the python package tests (make test-python, the python persist targets) stay broken until S-41, because kit 0.0.18 onward runs the python tool only as var/tools/bin/python3 and ignores PATH, while the fsm-python tests put the venv on PATH ("No module named 'polyspec'", CI run 38115497717). RED: kit-check against the v0.0.23 files with the lock at v0.0.20 gave 29 findings. GREEN: kit-check, owner-validate, documents-check, toolchain-check, dependency-policy-check, hooks-check, name-check, commits-check and kit-test pass.
  • S-35-1: the vendored kit is v0.0.20 (make kit-sync KIT_TAG=v0.0.20). Its kit-test runs the vendored tests through scripts/kit/run-tests.mjs (kit K32, released in 0.0.19), so they report per-case progress and timeouts like every other test; 0.0.20 is taken because the vendored python-real test of 0.0.18 and 0.0.19 read the consumer's toolchain and failed here, which kit K34-1 moved out of the vendored tests.
  • S-29-2: the composer vendor of packages/fsm-php and packages/fsm-php-ext is keyed on composer.json as well as composer.lock, and its stamp is touched after the install, because the autoload of composer.json is not in the lock: an existing checkout kept the autoload of before S-25 and the persist cases failed with an undefined function.
  • S-37-2: the fsm-db CLI and command tests run on PostgreSQL and MySQL (audit 23, 24). Each case gets an empty database: a new SQLite file, or on the servers a new database that createEmptyDatabase (packages/fsm-db/tests/dialect.mjs) creates through the admin DSN FSM_TEST_PG_ADMIN_DSN or FSM_TEST_MYSQL_ADMIN_DSN, and the file drops again. The db-test-postgres and db-test-mysql targets run cli.test.mjs and command.test.mjs too, and require the admin DSNs. The CLI tests check the quoted table name of the missing-table message, because the PostgreSQL server answers in Korean. RED: the tests on the PostgreSQL target ran on SQLite (silent pass), and then failed on the server message relation "machine" does not exist (the server wrote it in Korean). GREEN: 36 cases pass on each server. Plan C-6 has the decision and the CI service lines; the fsm-db devDependencies pg and mysql2 are pinned and reviewed.
  • S-37-1: the persist cases of go, rust, php, php-ext and python run on PostgreSQL and MySQL too (audit 23). Each kind has make <kind>-persist-test-postgres and make <kind>-persist-test-mysql, in TOOL_TESTS and CHECK_TARGETS. The test reads FSM_TEST_DIALECT, takes its DSN from FSM_TEST_PG_DSN or FSM_TEST_MYSQL_DSN, checks that the connection has that driver, clears the persist table and runs the cases one after another. A server target with an empty DSN stops with exit 2 (require_dsn), as the db and npm server targets now do. RED: before the test read the dialect, the PostgreSQL target passed with the SQLite cases (go failed with driver "postgres" is not registered until its example imported orm/pg). GREEN on PostgreSQL 17 and MySQL 8.4 for every kind; the concurrency proof stays in npm (plan C-4).
  • S-26-2: the README facts that S-26-1 found follow the real output. The compiler README gives --go-module for vectors --kind go. The python README names the package that its command writes, ./app/order_fsm (run for real; the example imports it with app/ on the module path), instead of src/app_fsm, which the checkout does not have. The go README says that the persistence excerpt imports the test bed's model package, examples/model, and that a consumer imports its own; the excerpts were checked against the files. RED: the compiler command failed without --go-module, and the python note named a path the command does not write. GREEN: each documented command ran, and the Korean fenced blocks match the English ones.
  • S-26-1-1: the go vectors file imports only the machine packages of its module (<module>/src/<machine>), so a consumer's vectors --kind go file vets in the consumer's module. The generated decide returns the unnamed func type that examples.Decide names, so the file no longer imports the test bed's examples (which needs the test bed's orm model); the persistence example stays the test bed's own. RED: go vet on a consumer module failed with cannot find module providing package github.com/polyspec/fsm-go/examples; GREEN: the consumer test of packages/fsm-compiler/tests/go-backend.test.mjs passes, the golden vectors file and packages/fsm-go/tests/vectors_test.go are regenerated, and make test-go, make parity-go and make gen-check pass. Plan C-2 and C-5 record the decision.
  • S-22: the GUI manual check of the plan ran in the Chromium of the pinned Playwright with the json, mermaid and db stores on commit bc9f1b1 and is recorded with its steps, outcome and recordings in docs/verification/gui.md: the diagram names every state, an added row holds the suggested code 6, a saved state appears in the table and the diagram, a removed state leaves and the next code is 7, a rename keeps its code, and fsm-gen check or fsm-db check passes on each store. The driver is packages/fsm-gui/scripts/manual-check.mjs. Its first runs found S-22-1, S-22-2, S-22-3 and S-38.
  • S-20-2: the docs install prints no npm warning. npm 11 warned for the install scripts of esbuild and fsevents, which docs/package.json did not decide. The esbuild script is approved for its pinned version (allowScripts), and the fsevents script is denied, because the site runs no watcher and the package ships its prebuilt binary; plan C-8 has the trace and the evidence. tests/docs-build runs a fresh npm ci of the lock and fails on any npm warning.
  • S-29-1: gui-browsers downloads the Chromium once per pinned Playwright version. The browser is the file var/deps/playwright-<version>, keyed on the version in packages/fsm-gui/package-lock.json, and gui-browser-test depends on that file, so a later run with the stamp present prints no download and needs no network (make -n gui-browser-test checked in tests/make-install). gui-browsers stays as a phony alias of the stamp.
  • S-34: the python dependencies are locked (audit 16). packages/fsm-python/uv.lock (uv lock) holds every package of the tree with its exact version, the sha256 of each artifact, and the commit of each git package (orm and ordered-json at v0.0.7). make -C packages/fsm-python install runs uv sync --locked from it, so uv verifies each artifact against its hash. The python stamp of make install is keyed on uv.lock instead of pyproject.toml. The kit dependency gate reads no Python lock, so scripts/check-python-lock.mjs (make python-lock-check) checks the pin, commit, tag, current and closure rules of the lock (plan C-1). The review of the latest releases and advisories of the transitive packages is filed as S-34-1.
  • S-37: the persistence and the DB store are proven on SQLite, PostgreSQL and MySQL (audit 23, 24). packages/fsm-npm/tests/persist-dialects.test.mjs runs two concurrent transitions of one row on two connections: the same (expected, event) gives one Changed and one Unchanged, two events from one expected state give one Changed and one E_CONFLICT; on SQLite the two run in sequence, because the SQLite driver is synchronous. Without forUpdate() both transitions report Changed on PostgreSQL and MySQL, which is the RED. make db-test-postgres and make db-test-mysql run the DB store tests and the store contract on the two servers through packages/fsm-db/tests/dialect.mjs; a missing FSM_TEST_PG_DSN or FSM_TEST_MYSQL_DSN fails the target, and no case is skipped (RED: the run without the variable failed). The documents name the tested dialects (spec B-2, B-4; fsm-db README; root README). Plan C-6 has the CI service lines for S-21.
  • S-31-1: the layout contract allows the rust retry test (audit of S-31). contracts/layout.json declared tests/persist_retry.rs as a difference, but check-layout also requires the file in the allowed globs of rust; the S-31 commit missed it because its layout-check was not run. make layout-check and make layout-test pass.
  • S-31: the rust persistence example retries a deadlock (spec B-4). examples/persist.rs used transaction_once, which runs its callback once, while B-4 says that orm runs it again on a deadlock. It uses orm v0.0.7 transaction now (three retries by default), and the callback is a Fn. A machine error is returned as a value, because the callback of transaction fails only with an orm error. persist_runs_again_on_a_deadlock (RED: one attempt, expected two) and persist_does_not_run_again_on_another_error are in packages/fsm-rust/tests/persist_retry.rs (make rust-persist-test). Spec B-4 records why the transaction takes the lock with forUpdate() rather than one conditional update: orm v0.0.7 has no public update with an affected-row count, and the rule to revisit it.
  • S-39: a machine key that a go test file cannot take is reserved (spec B-5). examples, vectors, machines, expectation and lookup pass fsm-gen check but break go test ./... of packages/fsm-go (an import redeclared, or a package name already declared through an import), so they fail E_KEY_RESERVED now. tests builds in the measurement, so it is not reserved; the S-39 row named it, and the measurement corrects that.
  • S-33: fromCode refuses a value that is not an int in every caller (audit 13, 14, 22). In a caller without strict_types, php and php-ext converted "2" and 2.0 to the state of 2 and true to the state of 1; they now throw E_UNKNOWN_CODE, as python and npm do (spec B-6). The GUI edit page shows a refused next code (E_CODE_RANGE) before a save, and the core and the forms return no null. The spec B-6 lists the four error constants that the generated code has.
  • S-40: the command test of fsm-gui builds its project from one machine. packages/fsm-gui/tests/command.test.mjs copied the whole root fsm.lock.json into the project of the order machine, so a machine added to the root lock changed the test project. The test now writes the project lock with the order entry of the root lock only ({ order: rootLock.order }) and asserts that the project lock names order alone. RED: the project lock held the five root machines (in_review, limits, order, standby, ticket). GREEN: make gui-test passes, and the command test passes among its 233 cases.
  • S-26-1: the language READMEs follow S-25 and S-26 (plan C-9). The go README gives fsm-gen vectors --kind go with --go-module and the real output, describes the key tables and adapters of tests/vectors_test.go, and copies the Decide of examples/persist.go again from the file (with the adapter of orderMachine.decide). The php README copies the real autoload block of composer.json (the loader src/functions.php in autoload.files, which requires every machine) and its files table names the loader. Every language's Errors section states the field rules of spec B-6 (which of expected, actual and event each code sets) with the field names of that language. RED: the go command failed without --go-module, the composer example loaded only the Order functions, and no Errors section stated B-6. GREEN: each excerpt is verbatim in its file, the Korean code blocks match, and the snippets and commands were run in temporary consumer projects. The go vectors file imports the test bed's examples, not the consumer's, which checklist item S-26-1-1 records.
  • S-29: a fresh clone can run make check (audit 8). make install installs the toolchains of install-tools and every package dependency once: the npm locks of fsm-npm, fsm-db, fsm-gui and docs, the composer locks of fsm-php and fsm-php-ext, the Python 3.14 venv of fsm-python, and the go modules and cargo crates (stamps in var/deps/). Each is a file target keyed on its lock, and each target that uses a package names it as a prerequisite (gen-check, npm-types, test-<kind>, db-test, gui-test, docs-build). Before, a checkout without installed packages failed in gen-check, npm-types, packages-test and parity-check. make make-install-test (tests/make-install/) checks the prerequisites on a copy of the Makefile.
  • S-26: every vector asserts the whole result (audit 4, 15). An error case names the four fields of fsm.Error (code, expected, actual, event), null where a field does not apply (spec B-6, plan C-4), and every language harness asserts all four. from_code has three more inputs: a negative code, 2^31 and the 64-bit maximum, written as decimal strings so that the file stays exact. An invalid state value (0 and the unknown code) is given to code, key, is_final and apply in go, npm and python, the kinds whose state type holds any integer; rust, php and php-ext have no such value, so their native modules do not hold those cases. The kinds list of a case names the kinds that run it, and check-parity checks it. The contract contracts/features.json is unchanged.
  • S-25: the conformance machines cover the definition (audit 3). machines/ gains standby (no events), in_review (underscore keys at every level), ticket (reserved codes 2 and 4: its states were removed after a first lock, and the second lock keeps the codes reserved) and limits (the code 2147483647), with their lock entries and conformance/vectors. The go harness is generic: tests/vectors_test.go holds the key tables and one adapter per machine, generated by the go backend (vectors --kind go takes --go-module); tests/conformance_test.go runs every case through the adapter of its machine, and examples/persist.go takes the machine as a Decide. The php package loads the functions of every machine through the loader src/functions.php (spec B-6, plan C-2). The compiler tests that compare with the order golden files read a project of the order machine alone (tests/order-project.mjs). Every kind passes the 209 cases of the five machines. The package machine without events of S-23 is standby.
  • S-24: the php-ext backend derives every C name from --php-namespace and --php-ext-name. The machine files included php_polyspec_fsm_test.h and polyspec_fsm_test_arginfo.h, and the Error class symbol was Polyspec_FsmTest_Fsm_Error whatever the options; now they are php_<ext>.h, <ext>_arginfo.h and <namespace with _>_Fsm_Error (spec B-6). A backend test builds the output with a second pair, Acme\Tool and acme_tool, with phpize, ./configure and make and loads it in php.
  • S-23: the rust backend compiles a machine without events. A zero-variant enum takes no representation (rustc E0084), so Event of such a machine is pub enum Event {} without #[repr(i32)] (spec B-6). A backend test builds the generated files of that machine with cargo.
  • S-22-3: the browser test waits for the connected form, not for networkidle. The client sets data-fsm-connected="true" on the form host #fsm-form once connectForm and the subscription are in place, and removes it on disconnect; the layout keeps the row buttons from taking a click until the marker is set. The first-run failure was the first navigation of a newly downloaded Chromium, 2.5 to 12.4 s against the 5 s case limit (four fresh downloads measured, plan C-7), not a click before the bind; the setup now opens the edit page once with its own limit.
  • S-30-1: the READMEs state the lock baseline of S-30 (spec B-3). The root README gives the rule with the real output of fsm-gen lock for a swapped, deleted lock (E_LOCK_REGRESSED), the git HEAD baseline of json and mermaid, the outside-git limit and the db store, which has no commit so E_LOCK_REGRESSED does not arise there. The fsm-db README tables and lock section and the fsm-gui save, rename and delete rules say the same.
  • S-38: the Mermaid diagram names every state (spec B-1 and B-5). With the pinned mermaid 12.1.0 draft : 1 drew only the code, so the GUI and any Mermaid viewer showed 1..n and no key. Each state is now a label line state "draft" as draft followed by draft : 1: the diagram shows the key as the box title and the code as the description. The reader refuses a label that differs from its key with E_MERMAID_UNSUPPORTED. Four other forms were checked and rejected in B-1 (a repeated description, the code inside the label, the key and the code in one description, the former form). The reserved words of B-5 were measured again with the label line; the list is unchanged. packages/fsm-gui/tests/browser/diagram.test.mjs asserts that every state key and code is a drawn label (it failed before). The mermaid store, its canonical texts, the GUI fixtures and every Mermaid example in the README and the spec follow.
  • S-20-1: the documentation site builds without a warning. The build printed Circular chunk: framework -> theme -> framework because the alias of vue named vue/index.mjs, a CommonJS wrapper; the alias now names dist/vue.runtime.esm-bundler.js, the import entry of vue. The vite override and vitepress were ruled out with evidence, recorded in the plan (C-8). docs-build-test fails on any warning line of the build output.
  • S-35: the plan and the contracts match the repository (audit 17, 18, 19, 21). The plan D table no longer holds states and points to the checklist; the C-1 tree lists src/store-lock, the name checker, the docs-build and install-python tests and the docs site files. contracts/layout.json declares npm's tests/types.test.mjs and tests/types/** in differences with their reasons, and check-layout refuses a test file of a kind that no declaration covers (RED tests in tests/check-layout/). S-11-3 has a repeatable test, make install-python-test, which runs the target in a temporary checkout with a stub uv and shows that a recipe without --no-bin writes the link. kit-test is in CHECK_TARGETS.
  • S-32: the compiler imports are stated and tested (plan C-5). packages/fsm-compiler/src imports node built-ins by their node: specifier, its own files and the vendored kit of this clone (scripts/kit/), and no npm package. The plan said zero dependencies, which the kit imports contradicted. tests/imports.test.mjs scans every import specifier of src (static, dynamic and relative) and fails on anything outside that set; src/core imports no node: module (audit 5).
  • S-27: name-check judges each occurrence of the old name, not the line: an allowed phrase is removed from its line and the rest is scanned again. The CHANGELOG entry allows only the bullets up to the rename (S-01 to S-18), and the model entry allows only the helper and column names that orm-gen writes for the status column. The entries that allowed a whole line now allow exact phrases, and the audit probe lines are findings (RED tests in tests/check-name/). The rename leftovers are fixed: the Makefile comment, the helper comments of the compiler and gen-check tests, and status_of for the status column of the python and rust examples (audit 6, 12).
  • S-19: the user documentation (R11, C-9 of the plan). The root README and every package README (English and Korean) explain the library to its users: what fsm is and is not, the model with the order example in JSON, Mermaid and the database, codes and the lock, transitions with apply and why expected makes retries idempotent, every validation code, the project files, the requirements and setup, every fsm-gen command and flag with its real output and exit codes, the generated names and the checks. packages/fsm-compiler/README.md is new (architecture, the store contract, commands, backend options, vectors). The database and GUI READMEs gained their required sections (the GUI keeps its Tests section). Each language README gives the generation command, the generated files, the API with an apply example, the errors, the persistence pattern of its examples/persist.* and the tests. scripts/check-readmes.mjs (make readme-check, tests in tests/check-readmes/) requires the sections of each README and a Korean twin with the same headings; it reported 54 findings before the documents were written. make docs-build builds the site with the new pages.
  • S-30: a committed code never changes meaning, even when the lock is lost (R1; spec B-3 and B-5). load() of every store returns the committed lock as baseline (json and mermaid: fsm.lock.json at git HEAD; db: the stored rows). check, gen, vectors, lock, save and copy refuse with E_LOCK_REGRESSED a lock that loses a committed code, gives a committed reserved code to a state, or moves a committed key to another code. A rename is accepted before its commit. The audit (swap two codes, then empty fsm.lock.json, then lock) printed lock: ok and is refused now. Outside git and for an untracked lock nothing is committed, so only the stored lock is judged. A row changed in SQL on the db store is not detected. lock restores a deleted lock only when the committed codes are kept.
  • S-22-2: the GUI forms are usable. make gui-build copies the crudui stylesheet @polyspec/crudui-generator-core/styles/crudui.css to public/assets/crudui.css and passes it to hyper-build-assets --static, and the layout links /assets/crudui.css, so the row buttons show their icon and size. The new field of a state row is labeled New key (the stored key stays Key), so no two fields of a row share a label. public/favicon.ico (an ICO written by scripts/make-favicon.mjs) answers /favicon.ico with 200 instead of 404, and the layout links it. The browser test checks all three.
  • S-22-1: the GUI forms re-render on a row change. The edit form (packages/fsm-gui/client/main.ts) subscribes to its crudui instance and re-renders it with patchContent(host, renderForm(instance, render)), then calls connection.sync(), the contract of connectForm. Before the change a click on a row's add or remove button changed the instance but not the page. render holds the page's crudui render options (the action, the _csrf token and the errors), embedded in the page state, and the form sits in a host element #fsm-form. packages/fsm-gui/tests/browser/browser.test.mjs (make gui-browser-test) drives the form in a headless Chromium of the pinned Playwright 1.64.0 (exact devDependency; make gui-browsers downloads the browser) and covers add, save, remove and the suggested codes. make gui-test runs the top-level tests only. Before the fix the first browser assertion failed with 5 !== 6.
  • S-36: the fsm-db CLI is tested as a process for every command the README documents. packages/fsm-db/tests/cli.test.mjs asserts the output, the exit code and the rows after: lock --rename order.draft=created prints [fsm-db] lock: ok, keeps the code of the renamed state and writes the new key to the rows; an unknown rename prints the README's E_UNKNOWN_STATE_REF line and writes no row; copy --to mermaid --root <dir> writes the canonical .fsm.mmd file and the lock; copy --from mermaid --root <dir> fills the empty database, and a second copy prints the README's E_STORE_NOT_EMPTY line and writes nothing. RED: the process cases were absent, and a mutated expected output fails the rename case. The README matched the commands, so no README text was wrong; it gained the copy --to mermaid example (en and ko).
  • S-28: the store contract of spec B-2 is proven on the three stores. packages/fsm-db/tests/store-contract-all.test.mjs (the fsm-db package owns it, since it depends on the compiler; make db-test) round-trips every valid fixture of packages/fsm-compiler/tests/fixtures/valid, each and all together, json → db → mermaid → json, and compares the files as bytes. It also gives run gen of every kind, with its required options, and vectors (with the vectors file of every kind) byte-identical output on the json, mermaid and db stores. RED: a db store that drops final on save fails the four equality cases, because the core refuses the round trip with E_DEAD_END or the bytes differ; the detection case proves the same. The real stores pass, so no store defect was found. Spec B-2 (en and ko) names the file.
  • S-20: the documentation site (R11, C-8 of the plan). docs/.vitepress/config.mts builds the repository documents in place with VitePress 1.6.4 (make docs-build, its packages from docs/package-lock.json by docs-deps): the root and package READMEs in English and Korean, the specification, the plan, the verification pages and the changelogs. A README.md is the index page of its directory, a link to a repository file that is not a site page points to the public repository, and a dead link fails the build. make docs-build-test (tests/docs-build) checks the failure of a dead link and the real build. docs/package.json overrides vite to 6.4.4, because vitepress 1.6.4 pins the 5.4.x line, which has moderate and high advisories. The plan writes its placeholders <major> in code spans, which the site compiles as Vue markup. config/documents.json sets siteLinks.
  • S-18-1: the commands fsm-gen, fsm-db and fsm-gui exist. They are the bin entries of packages/fsm-compiler/package.json (new, without dependencies), packages/fsm-db/package.json and packages/fsm-gui/package.json; each file starts with a node shebang and is executable, and npm install -g ./packages/<package> from a clone links the command onto the PATH. S-18 had renamed them in the documents only. A test in each package installs it offline into a temporary npm prefix and runs the command; the GUI usage line names fsm-gui.
  • S-15-1: fsm-db install [--project <dir>] installs the schema of schema/fsm.dbs into the database that fsm.config.json names, through orm utils().schema().install, so a database gets its tables without code. On a new database fsm-db check fails with no such table: machine (exit 1), install prints [fsm-db] install: ok (exit 0), and check then passes. A second install changes nothing and exits 0; on a database that has only some of the tables orm rejects the install with CONFIG, which is printed with its stack (exit 1), with no fallback (observed with orm v0.0.7). packages/fsm-db/README.md and README.ko.md are the user guide of the DB store. The spec (B-2) and the plan (C-6) list the commands; the spec's copy line is corrected to the flags the CLI has (--to or --from with --root; the DSN comes only from the variable that dsnEnv names). The install is tested as processes on SQLite in packages/fsm-db/tests/cli.test.mjs.
  • S-18: the library is renamed from status to fsm everywhere (R12). The packages are packages/fsm-*, the commands are fsm-gen, fsm-db and fsm-gui, the files are fsm.config.json, fsm.lock.json, machines/*.fsm.json, *.fsm.mmd, schema/fsm.schema.json and schema/fsm.dbs (dbspec fsm), the documents are docs/spec/fsm.md and docs/plans/fsm-v1.md, the generated shared module is fsm (fsm.Error, <ns>\Fsm\Error, the module fsm of each backend), and the reserved machine key is fsm; status is an ordinary key again. The manifests carry the names polyspec_fsm, polyspec-fsm, @polyspec/fsm-*, github.com/polyspec/fsm-go, the namespace Polyspec\FsmTest and the extension polyspec_fsm_test. The consumer's status column and the process and test statuses keep status. scripts/check-name.mjs (make name-check) fails on a tracked file or path that names the library status outside its allowlist, and the goldens and generated files were regenerated by fsm-gen. AGENTS.md and the plan list name-check among the tools that enforce the rules, the scope is R1–R12, and a document names the old name only as "old name status".
  • S-11-2: the python package drops its color workaround. packages/status-python/Makefile no longer sets PYTHON_COLORS=0 in make test, because kit v0.0.15 (K22) makes run-tests python set NO_COLOR=1 and PYTHON_COLORS=0 itself. Without the kit setting, unittest colors its verbose lines under FORCE_COLOR=3 and the reporter does not read them: the v0.0.14 runner failed 10 of 10 runs with the line removed, and the v0.0.15 runner passed 10 of 10. make test-python passes 89 of 89 cases with FORCE_COLOR=3 and without it.
  • S-16-1: the GUI test hang is fixed at its cause. The kit v0.0.15 sync (K23) makes the stdout pipe of a test child blocking (scripts/kit/test-load-check.mjs). On the asynchronous pipe, the process.exit of the test child dropped the results still queued in it, so a file reported fewer results than it ran, and the runner could print "the test did not finish". With 600 cases in one file, v0.0.14 lost results in 12 of 30 runs, v0.0.15 in 0 of 30, and v0.0.15 with only the blocking lines reverted in 10 of 30. The sync also brings K21 (the node file group under --cwd) and K22 (run-tests python sets NO_COLOR=1 and PYTHON_COLORS=0). Before the test change, make gui-test passed 20 of 20 runs, 4 at a time (210 results each), and make compiler-test passed 20 of 20 (302 results each); the hang did not reproduce in this checkout. The per-call 20-second deadline in packages/status-gui/tests/mermaid-parse.test.mjs only hid the hang, so it is removed; the runner already gives each case its own timeout. The flat tests (one per reserved word and position, and one per ordinary key) are kept, because each case has its own name and result. After the change, make gui-test passed 20 of 20 runs.
  • S-11-3: make install-python runs uv python install --no-bin 3.14, so uv no longer writes a python3.14 executable into a user bin directory (~/.local/bin by default). The interpreter stays uv's managed install, and only var/tools/bin/python3 links to it. Before the change, with UV_PYTHON_BIN_DIR set to a scratch directory, the target wrote python3.14 there; after it, the directory stays empty and ~/.local/bin is unchanged.
  • S-14-3: gen and vectors --kind skip the git-ignored files of their output directory (packages/status-compiler/src/cli/write-generated.mjs). An ignored file is a build output, not a hand-written file: it neither blocks the generator with E_NOT_GENERATED nor is removed as stale, so make gen in packages/status-php-ext runs after make test without make clean. A tracked file or a file that Git does not ignore still gives E_NOT_GENERATED, and outside a git work tree nothing counts as ignored. The ignored files come from git ls-files --others --ignored --exclude-standard in the output directory. make clean remains the removal of the phpize output. The tests are in packages/status-compiler/tests/write-generated.test.mjs.
  • S-14-1: the php-ext package packages/status-php-ext is the kind php-ext of the canonical layout (docs/plans/status-v1.md, C-2). make gen writes its src/ and tests/Vectors.php with status-gen (--php-namespace 'Polyspec\StatusTest' --php-ext-name polyspec_status_test), copies the php package's tests/ConformanceTest.php and examples/persist.php (gen-check then proves they are the same bytes), and writes examples/model/ with orm-gen from the root schema/persist.dbs. make test builds the extension with phpize, ./configure and make in src/ and runs the 89 vector cases with --php-extension, and make parity-php-ext checks them. composer.lock pins the same 30 packages as the php package (polyspec/orm 0.0.7 and PHPUnit 12.5.38), and config/dependency-policy.json and config/dependency-review.json record the php-ext manifest and lock. contracts/layout.json declares the kind with its generated globs. The toolchain is the php and composer declaration of config/toolchain.json, which is not duplicated.
  • S-14-2: the php-ext backend's C output is corrected after its first build. The object parameters are zval *, because Z_PARAM_OBJECT_OF_CLASS writes a zval *; the Error method table follows its constructor; the format of ZEND_LONG_FMT carries no extra %; the class type of the Step property is ZEND_TYPE_INIT_CLASS with a persistent class-name string, since a literal name does not resolve for a property; and the Code enum values are persistent interned strings, so they are not freed at shutdown. .gitignore lists the phpize output of packages/status-php-ext/src/ with exact anchored patterns; config.m4 and the generated sources stay tracked. The golden files are regenerated.
  • S-14: the php-ext backend (packages/status-compiler/src/backends/php-ext.mjs) generates the src/ files of a php-ext package for every machine: <machine>.c registers the enums State and Event with zend_register_internal_enum, the final class Step and the namespace functions of B-6 with ZEND_NS_NAMED_FE, and apply and target throw the B-4 codes through status_throw; status.c holds the enum Status\Code, the exception Status\Error and the module entry, which initializes every machine in definition order; php_<ext>.h, <ext>_arginfo.h (the arginfo is written directly, without gen_stub), <ext>.stub.php (the declarations for editors) and config.m4 complete the set. status-gen gen --kind php-ext requires --php-namespace and --php-ext-name; vectors --kind php-ext writes the test module, which is byte-identical to the php kind's Vectors.php. The golden files under packages/status-compiler/tests/golden/php-ext/ hold the order example; they were reviewed against B-6 before they were written. write-generated.mjs accepts the dnl header of config.m4.
  • S-17-1: the edit page carries the next code of its machine. The embedded form state holds core nextCode(machine, definitions, lock) as nextCode (src/forms.mjs, nextCodeOf and editState), and client/main.ts gives each state row added in the browser the next code, then the codes after it (client/new-codes.ts, codesOfAddedRows). The save still checks every code, and a blank code is still filled by nextCode. tests/edit-next-code.test.mjs covers the embedded code on the json, mermaid and db stores, including the code after a delete, and tests/delete-state.test.mjs shows that deleting a state in the edit form moves its code to reserved and out of codes on the three stores. make gui-test runs 210 tests.
  • S-17: the GUI edits the machines (packages/status-gui). /machines/{machine}/edit is the crudui form of crud/machine.yml: the states and the events are repeated rows, and the key and the code of a stored state are read-only. A save parses the form[...] fields (src/form-data.mjs; a malformed body is 400), gives each new state the code that core nextCode suggests and calls core run save with the full definition set. A refused save writes nothing and answers 422 with every finding; a finding that names a field is shown on that field. /machines/{machine}/rename calls run lock with the renames (the code is kept), /new creates a machine with one initial and final state, and the detail page deletes a machine by a save without it, so its codes move to the reserved list. Every save, rename and delete needs the _csrf token of its page (403 without it). client/main.ts binds the rows of the edit form with crudui connectForm after every htmx settle. make gui-test runs 194 tests, on the json, mermaid and db stores.
  • S-11-1: the python package packages/status-python is the kind python of the canonical layout (docs/plans/status-v1.md, C-2). Its pyproject.toml pins the orm client to its git tag v0.0.7 and the build backend to setuptools==84.0.0. make install creates a Python 3.14 venv (.venv, gitignored) and installs them, and make test runs offline with that venv first on PATH. make gen writes src/polyspec_status/ and tests/vectors.py with status-gen and examples/model/ with orm-gen. tests/test_conformance.py runs each of the 89 vector cases as test_<feature>__<id>, and examples/persist.py runs the B-4 transaction on SQLite with polyspec orm, which the persist cases assert after each run. config/toolchain.json declares python 3.14, config/dependency-policy.json lists the pyproject, and contracts/layout.json declares the kind without a lock file, as C-2 allows for python. make test-python and make parity-python run the package.
  • S-11: the python backend (packages/status-compiler/src/backends/python.mjs) generates the src/polyspec_status/ files of the python package for every machine: <machine>.py with State and Event as IntEnum classes whose members are upper case (B-6), a frozen Step dataclass with to and changed, and the eight functions, which raise status.Error; the shared status.py (Code, a StrEnum of the E_ codes, and Error) and __init__.py. status-gen gen --kind python --out <dir> writes the set through writeAtomic with the python header # Code generated by status-gen from <machine>; DO NOT EDIT., removes the stale generated files and refuses a file without the header (E_NOT_GENERATED); vectors --kind python --out <file> writes the native module tests/vectors.py as a Python literal. The header check of the writer accepts the # comment syntax as well as //. The golden files under packages/status-compiler/tests/golden/python/ hold the order example; they were reviewed against B-6 before they were written, and the tests compare them byte for byte.
  • S-09-2: make npm-types (packages/status-npm/tests/types.test.mjs) compiles tests/types/consumer.ts in strict mode without emitting, against the generated src/*.d.ts, with the pinned typescript 7.0.2 devDependency of packages/status-npm. The consumer uses every public name of B-6 and holds three @ts-expect-error lines that must fail to compile, so a too-loose declaration leaves a directive unused and fails the test. The test runs in its own record, .results/npm-types.json; make test-npm names the conformance file only, so the npm record holds vector cases alone for parity-npm. contracts/layout.json allows the two test paths.
  • S-13-1: the rust package packages/status-rust is the kind rust of the canonical layout (docs/plans/status-v1.md, C-2). make gen writes its src/ and tests/vectors.rs with status-gen; build.rs generates the orm models of the root schema/persist.dbs into OUT_DIR, so the package has no examples/model/ (the declared difference of rust in contracts/layout.json). tests/conformance.rs holds the check functions, and the 89 vector cases run as test_<feature>__<id>; examples/persist.rs runs the B-4 transaction on SQLite with polyspec orm (v0.0.7, pinned by tag), and the persist cases assert the stored code after each run. rust-toolchain.toml declares Rust 1.98.0 at the root, and config/toolchain.json declares cargoAudit 0.22.2 for the dependency review of Cargo.lock. make test-rust and make parity-rust run the package, and make packages-test runs it with the other kinds.
  • S-13: the rust backend (packages/status-compiler/src/backends/rust.mjs) generates the src/ files of a rust package for every machine: <machine>.rs with the B-6 notation (#[repr(i32)] State and Event, the Step struct, the eight functions, from_code taking i64, apply returning Result<Step, Error>), the shared status.rs (Code and Error) and lib.rs. status-gen vectors --kind rust --out <file> writes tests/vectors.rs, one #[test] per case (#[tokio::test] for a persist case) that calls the check functions of tests/conformance.rs. The golden files under packages/status-compiler/tests/golden/rust/ hold the order example; they were reviewed against B-6 before they were written, and tests compare them byte for byte.
  • S-12-1: the php package packages/status-php is the kind php of the canonical layout (docs/plans/status-v1.md, C-2). make gen writes its src/ and tests/Vectors.php with status-gen (--php-namespace 'Polyspec\StatusTest') and examples/model/ with orm-gen from the root schema/persist.dbs. tests/ConformanceTest.php runs each of the 89 vector cases as the PHPUnit data set test_<feature>__<id>, which check-parity reads as one case name per id; it uses the public API of the generated namespace only. The persist cases run the B-4 transaction on SQLite with polyspec orm v0.0.7 (the release zip, pinned by its sha1 in composer.json, with ordered-json v0.0.7) and assert the stored state after each run. composer.json pins PHPUnit 12.5.38 (the latest stable release at the review) and config.platform.php 8.4.0; config/toolchain.json declares PHP 8.5 and Composer 2.10.3, and config/dependency-review.json records the review. make test-php and make parity-php run the package.
  • S-12: the php backend (packages/status-compiler/src/backends/php.mjs) generates the src/ files of a php package for every machine: <Pascal>/State.php and Event.php (backed enums with the B-6 codes and indexes), Step.php, and functions.php with the functions of B-6 as namespace functions, and the shared Status/Code.php (enum of the error codes) and Status/Error.php (an \Exception whose getCode() is the code string). status-gen gen --kind php --php-namespace <ns> --out <dir> requires the namespace (Polyspec\StatusTest for the test bench), so a missing or malformed one is a usage error. vectors --kind php --php-namespace <ns> --out <file> writes Tests/Vectors.php, whose cases() keys every case by test_<feature>__<id>. A php file has <?php on its first line, so the header is read on the second line, and a file without it is still refused (E_NOT_GENERATED). The golden files under packages/status-compiler/tests/golden/php/ hold the order example (89 cases); they were reviewed against B-6 before they were written, and php -l accepts every generated file.
  • S-10-1: the go package packages/status-go is the kind go of the canonical layout (docs/plans/status-v1.md, C-2), module github.com/polyspec/status-go. make gen writes src/ (order/order.go, status/status.go) and tests/vectors_test.go with status-gen, and examples/model/ with orm-gen (go tool orm-gen, a tool directive of go.mod) from the root schema/persist.dbs; the models are generated inside the output directory, so gen-check runs them in a module copy. tests/conformance_test.go runs every vector case as the subtest test_<feature>__<id> against the generated functions, and examples/persist.go runs the B-4 transaction on SQLite with polyspec orm v0.0.7, which the persist cases assert after each run. contracts/layout.json declares the kind with its generated globs. Its language-forced differences are two: gen --kind go takes --go-module (the generated machines import the shared package by the module path), and the toolchain is the toolchain line of packages/status-go/go.mod (go1.27.2, which fixes the stdlib advisory GO-2026-6607 that go1.27.0 reports), read by config/toolchain.json go. config/toolchain.json also pins govulncheck 1.8.0, which the dependency review runs. make test-go (with GOPROXY=off), make parity-go and make packages-test run the package; the lock is go.sum, reviewed in config/dependency-review.json.
  • S-10: the go backend (packages/status-compiler/src/backends/go.mjs) generates the src/ files of a go package: <machine>/<machine>.go per machine with the B-6 notation (type State int32 with the members StateDraft, type Event int32 with EventNext (positions from 1), the struct Step{To, Changed}, and the functions Initial, Code, Key, FromCode, IsFinal, EventsFrom, Target and Apply, which return (value, error) with a *status.Error and check their arguments in the order of npm), and the shared status/status.go (the Error type and the Code* constants). status-gen gen --kind go --go-module <module path> --out <dir> writes that set; the module path is required because the machines import the shared package by it. status-gen vectors --kind go --out <file> writes the native tests/vectors_test.go: one literal per case, with the keys of the oracle, which the conformance test maps to the constants. The golden files under packages/status-compiler/tests/golden/go/ hold the order example; they were reviewed against B-6 before they were written, and tests compare them byte for byte.
  • S-16: the GUI skeleton (packages/status-gui) is a hyper v0.0.11 application. / lists the machines of the project with the crudui list of crud/machines.yml, and /machines/{machine} shows the states, the events with their edges and the canonical Mermaid text in <pre class="mermaid">, which the client renders with mermaid 12.1.0 on the first load and after every htmx swap. node src/main.mjs --project <dir> selects the store that status.config.json names (json, mermaid or db) and calls only that store and core run; every finding of the project is shown as an error, and an unknown machine answers 404. make gui-test builds the assets and runs 133 tests, which include the parse of the order example with the pinned mermaid package and the parse of every reserved Mermaid state key of B-5 at five positions. The pins are hyper v0.0.11, crudui v0.0.5, template v0.0.5, and exact versions of mermaid, htmx.org and yaml, reviewed in config/dependency-review.json. The criterion of the Mermaid parse test is recorded in docs/spec/status.md, B-5.
  • S-09-1: the npm package packages/status-npm is the kind npm of the canonical layout (docs/plans/status-v1.md, C-2). make gen writes its src/ and tests/vectors.mjs with status-gen and examples/model/ with orm-gen from the root schema/persist.dbs (created here and shared by every kind). tests/conformance.test.mjs runs every vector case as test_<feature>__<id> against the generated functions, and examples/persist.mjs runs the B-4 transaction on SQLite with polyspec orm, which the persist cases assert after each run. contracts/layout.json declares the kind with its generated globs: check-layout requires them, and gen-check compares exactly the files they match. make test-npm, make parity-npm and make packages-test run the package, and the lock is in config/dependency-policy.json with its review record.
  • S-09: the npm backend (packages/status-compiler/src/backends/npm.mjs) generates the src/ files of an npm package for every machine: <machine>.js and <machine>.d.ts with the B-6 notation (frozen State and Event, the eight functions, apply returning {to, changed} or throwing status.Error), the shared status.js and status.d.ts (Error and the Code constants), and index.js and index.d.ts. status-gen gen --kind npm --out <dir> writes the set through writeAtomic, removes the stale files that carry the generated header and refuses a file without it (E_NOT_GENERATED, nothing written). status-gen vectors --kind npm --out <file> writes the native module tests/vectors.mjs. The golden files under packages/status-compiler/tests/golden/npm/ hold the order example; they were reviewed against B-6 before they were written, and tests compare them byte for byte.
  • S-15: the DB store (packages/status-db/src/db-store.mjs) keeps the definitions and the lock in the six tables of schema/status.dbs through the orm client (v0.0.7). dbStore(db) reads and writes through the models that orm-gen generates into src/models/models.ts, and save replaces every row inside one transaction, so a failed write leaves the rows that were there. Every key column is varchar(64), the bound of S-05-3, and the edge table has the two indexes of its foreign keys to state. The store passes the store contract (tests/store-contract.mjs), round-trips json → db → json byte-identically with a 64-character key, refuses a copy into a non-empty database with E_STORE_NOT_EMPTY, and rolls back a failed save. status-db (src/cli/status-db.mjs) gives check, lock and copy --to|--from json|mermaid --root; its DSN is the value of the environment variable that status.config.json names. make db-test runs its tests, which include the check that orm-gen reproduces the committed models.
  • S-06-1: the store contract is async. load() and save() of the JSON and Mermaid stores return Promises, and run and copy (src/core/run.mjs) are async, so the DB store can use the async orm client. status-gen awaits the commands, prints an unexpected rejection and exits 1. validate, normalize, lock and vectors stay synchronous. The store contract tests (tests/store-contract.mjs) await a Promise from every store, and every test that calls a store or a command awaits it.
  • S-08: the Mermaid store (src/store-mermaid/) reads and writes machines/*.status.mmd in the canonical form of docs/spec/status.md, B-1: parse.mjs refuses every line outside that form with E_MERMAID_UNSUPPORTED and its line number, and write.mjs writes the form. save deletes the undefined *.status.mmd files and passes the store contract (tests/store-contract.mjs). The lock read and write moved to src/store-lock/lock-file.mjs, which both file stores share. status-gen accepts {"store":"mermaid"}, and copy --to json|mermaid migrates in both directions into an empty target; every valid fixture round-trips json → mermaid → json byte-identically.
  • S-05-3: core validate bounds the length of machine, state and event keys to 64 characters: a key longer than 64 fails E_KEY_FORMAT at its path, with a message that names the length and the maximum. The DB store keeps keys in varchar(64), so the bound lets all three stores accept the same keys (docs/spec/status.md, B-5).
  • S-07: the root files of the conformance hold status.config.json ({"store":"json"}), machines/order.status.json (the order example of B-1, written by the JSON store), its status.lock.json and contracts/features.json. The core vectors (src/core/vectors.mjs) computes every case of C-4 from the IR and the lock; status-gen vectors --out <dir> writes <machine>.json, so conformance/vectors/order.json holds 89 cases. scripts/gen-check.mjs (make gen-check) regenerates the vector files into var/gen-check/ and requires the tracked files of conformance/vectors to equal them, with make gen-check-test as its test.
  • S-06: core normalize returns the IR of a valid definition (src/core/normalize.mjs); lockMismatch, nextLock, nextCode and canonicalLock implement the code lock of B-3 (src/core/lock.mjs), and nextLock refuses a key whose code the lock holds and the definition changes with E_CODE_CHANGED; run (check, lock, save) and copy run the commands of B-2 over a store (src/core/run.mjs). The JSON store (src/store-json/store.mjs) reads and writes machines/*.status.json and status.lock.json in canonical form and passes the store contract (tests/store-contract.mjs). status-gen (src/cli/status-gen.mjs) gives check, lock and copy; the mermaid store stays E_STORE_NOT_CONFIGURED until S-08.
  • S-05-2: core validate reports the name and code errors of B-5, E_KEY_FORMAT, E_DUPLICATE_KEY, E_KEY_RESERVED (the fixed lists are in packages/status-compiler/src/core/reserved.mjs), E_DUPLICATE_CODE and E_CODE_RANGE. A machine key is compared after its underscores are removed, and a machine with a structural error is not compared with the other machines.
  • S-05-1: core validate reports the graph errors of B-5, E_UNKNOWN_STATE_REF, E_SELF_LOOP, E_NONDETERMINISTIC, E_DUPLICATE_EDGE, E_INITIAL_COUNT, E_FINAL_OUTGOING, E_UNREACHABLE and E_DEAD_END, each with its own failing case. Reachability starts from the single initial state, and a machine with a structural error reports only its E_SCHEMA findings.
  • S-05: core validate (packages/status-compiler/src/core/validate.mjs) reports every structural error of a definition as E_SCHEMA with its JSON path and skips the later rules of that machine. schema/status.schema.json is the editor schema, written with the keywords of kit schema-validate.mjs; the correspondence test checks that it and the core agree on every fixture. make compiler-test runs the tests through run-tests.mjs and records .results/compiler-test.json.
  • The JSON store writes an edge as the object {from, to} instead of a pair, so the editor schema checks it exactly and a GUI form edits it as a row (docs/spec/status.md, B-1).
  • S-04: scripts/check-parity.mjs (make parity-check, make parity-<kind> with --kind) fails unless every kind ran every vector case of conformance/vectors once and passed, from a current record that covers its package, every feature of contracts/features.json has a vector case, every test id names one case or is the parent of one, every tool test of TOOL_TESTS left a current passing record, and no record ran over 10 minutes. There is no exemption.
  • S-03: scripts/check-layout.mjs (make layout-check) checks the language packages against contracts/layout.json: the package directories are exactly the tools and the kinds, every file of a kind matches its canonical layout, every canonical and required file exists, and the php and php-ext tests are the same bytes. make layout-test runs its tests through run-tests.mjs and records .results/layout-test.json.
  • S-02: docs/spec/status.md fixes the definition: the model of three parts (machine, states with codes, events of edges), the three stores under one rule set, the lock of codes, the idempotent transition apply(expected, event, actual), the validation codes and the naming rule of the six targets. The plan points to it, so the definition has one source.
  • S-01: the repository adopts kit v0.0.14 (scripts/kit/, tests/kit/, .kit/kit.lock.json) with config/checklist.json, config/commits.json, config/documents.json, config/owner-checks.json, config/toolchain.json (Node.js only), config/dependency-policy.json and its review record; the hooks run the owner validation (pre-commit), the commit rules (commit-msg) and the push gate (pre-push). AGENTS.md holds the development rules, docs/plans/status-v1.md the plan and docs/plans/execution-checklist.md its items. make check runs the full suite once per tree.