Skip to content

Port the last C#-only jobs to Rust, and add Rust to the devcontainer (port stage 13a) - #176

Merged
wormeyman merged 8 commits into
mainfrom
port/stage-13a
Oct 21, 2026
Merged

wormeyman merged 8 commits into
mainfrom
port/stage-13a

Conversation

@wormeyman

@wormeyman wormeyman commented Oct 21, 2026 •

Copy link
Copy Markdown
Collaborator

Stage 13a of #138. The live site has run the Rust planner since #174. Before stage 13b deletes the C#, this PR moves the last jobs only the C# does over to Rust. It does that while the C# still exists, so each port is checked against real C# behavior. No C# is deleted here, and no test data moves.

Merging deploys the site, as every merge to main does. The only site change is that ApiError loses its unused response field, along with the dead branch that displayed it. Nothing has set that field since 12b.

What's ported

Job the C# did Rust replacement How it was checked against the C#
PlannerDefaultsTest writes src/vue/src/lib/plannerDefaults.verified.json rust/contract/tests/planner_defaults.rs. It fails when the file is stale; UPDATE_PLANNER_DEFAULTS=1 rewrites it. An exhaustive destructure makes a new option that isn't emitted fail to compile. Generates the committed file byte for byte (2,201 bytes, with the BOM and no trailing newline)
FactorioOracleTest checks hardcoded game facts against factorio-oracle.json rust/planner/tests/factorio_oracle.rs: all 7 checks, plus an entity-size check in rust/blueprint. Rust has no reflection, so entity_names::vanilla::ALL and item_names::vanilla::ALL list the names, and a test fails if a constant is missing from them. It also adds one check the C# never had: the planner's own pumpjack, pole and beacon sizes against the collision boxes. Each check fails when the value it guards is broken (9 deliberate breaks)
CLI oil-field normalize cargo run --release -p snapshots --bin normalize-lists (--keep-invalid, --dir), plus a test that the lists are normalized: no repeats and no blueprint in both lists Run against the real C# CLI on two hand-built list sets (duplicates, recompressed copies, cross-list blueprints, comments, CRLF, a BOM, invalid lines): same lines kept, in the same order
Verify writes the 2,748 *.verified.txt snapshots and the scoreboard UPDATE_SNAPSHOTS=1 cargo test --release -p snapshots -- --include-ignored creates, rewrites and deletes stale snapshots. The pinned 2,748 count is replaced by one derived from the cases. Rust regenerated all 2,748 files plus the scoreboard byte for byte, with an empty diff. A blueprint moved from big-list to small-list made 26 new files, 46 renamed ones and 1 deletion, and the C# Verify suite accepted every file Rust wrote (4,329 passed)
swagger-gen writes FactorioToolsApi.ts from WebApp/swagger.json The file is hand-maintained: 757 lines down to 446, with no @ts-nocheck. FactorioToolsApi.test.ts checks it against all 358 plan and normalize responses and 27 error envelopes in rust/contract/fixtures/contract.jsonl: key shape, value types and exact enum sets. The fixture loads with a Vite ?raw import and isn't in the production bundle. Renaming a field, adding a key, or removing or adding an enum member each fails the test or vue-tsc

Two side findings:

  • The C# normalize on .NET 10 rewrites all 1,208 committed blueprint lines to identical content, because they were compressed by an older zlib. The Rust tool leaves a line exactly as written when it's already normalized, so its diffs show only real changes.
  • No Rust code could write snapshots before this PR. Without UPDATE_SNAPSHOTS, nobody could add a test blueprint or make a deliberate output change once 13b removes the C#.

Devcontainer

  • Rust is now in the image. rustup-init 1.29.1 is pinned by sha256 for arm64 and amd64, and the toolchain is pre-installed (RUST_VERSION=1.98.1 with rustfmt, clippy, rust-src and wasm32).
  • Volumes: rust/target and the cargo registry and git caches each get their own volume.
  • Editor and setup: rust-analyzer is added, and the container builds the planner when it's created.
  • devcontainer.yml:
    • It checks that RUST_VERSION, rustc, cargo and the wasm32 target all match rust/rust-toolchain.toml.
    • It runs cargo test --all-targets inside the container.
    • It builds the planner in the container, which replaces the 12b step that built it on the runner.
  • What stays until 13b: .NET, Lua and PowerShell are still in the image, so the C# keeps working until then.
  • Built and checked locally. The image was built on Docker Desktop and every workflow step ran inside it, twice, with the pinned @devcontainers/cli@0.89.0: the toolchain check, cargo test (175 passed), the front end, dotnet test (4,302 passed) and the Lua transpile. A full run takes about 8 minutes.

Other changes

  • swagger-gen is gone: the swagger-gen script, its swagger-typescript-api devDependency (1,019 lockfile lines) and the "Check the committed API client is up to date" CI step are removed.
  • CLAUDE.md documents the new commands, including "Adding a test blueprint" and "Changing planner output on purpose".

Checks

  • Rust: fmt and clippy are clean; cargo test --all-targets gives 190 passed and 7 ignored; the release gates (-p snapshots -p contract --include-ignored) give 37 passed; the wasm32 workspace build succeeds; and update mode over the whole tree leaves git status clean.
  • Front end: npm ci, build-wasm, build, npm test (55 tests) and eslint all pass.
  • Security review (guidance mode) of the whole diff found nothing to fix:
    • Both workflows run on pull_request only, with contents: read and no untrusted ${{ }} in run: steps.
    • The rustup-init download is pinned by checksum.
    • No new crates appear in Cargo.lock, and one npm devDependency is gone.
    • Update mode deletes only *.verified.txt files under the test directory, and only after checking how many snapshots the cases should produce.

Next: stage 13b

13b moves the test data out of test/, then deletes .NET, Lua, the submodules, the C# recorders and their CI jobs. It also strips .NET from the devcontainer and updates the docs.

🤖 Generated with Claude Code

https://claude.ai/code/session_01PTSQFMEdmByebgkoa9RiqH

Summary by CodeRabbit

  • New Features

    • Added Rust-based blueprint list normalization, including duplicate removal, ordering, comment handling, and optional preservation of invalid entries.
    • Added snapshot update workflows that can create or refresh expected results.
    • Added Rust and WebAssembly development support to the development container.
  • Bug Fixes

    • Simplified error responses by removing the unstructured fallback error message.
  • Tests

    • Expanded validation for planner defaults, blueprint geometry, API contracts, and Factorio compatibility.

wormeyman and others added 8 commits October 21, 2026 03:39
Adds rust/contract/tests/planner_defaults.rs, which builds the same payload
as the C# PlannerDefaultsTest and writes it in Verify's strict JSON layout
(BOM, two-space indent, no final newline), with doubles through
blueprint::json_out::format_f64. The bytes match the committed
src/vue/src/lib/plannerDefaults.verified.json exactly.

The test compares and fails on any difference. UPDATE_PLANNER_DEFAULTS=1
rewrites the file instead. An exhaustive destructure of OilFieldOptions
stands in for EmitsEveryOilFieldOptionsProperty: a new field that is not
emitted fails to compile.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PTSQFMEdmByebgkoa9RiqH
rust/planner/tests/factorio_oracle.rs checks the planner's hardcoded
Factorio facts against test/FactorioTools.Test/OilField/factorio-oracle.json,
as FactorioOracleTest.cs does: vanilla entity and module names, the
pumpjack terminal offsets, direction values, raw pole and beacon
geometry, and MAX_UNDERGROUND against max_underground_distance. It also
checks the planner's own footprints (PUMPJACK_WIDTH and the options
presets) against the collision boxes.

EntityNameToSize lives in the blueprint crate as a private function, so
its check is a unit test in rust/blueprint/src/write.rs.

Rust has no reflection, so entity_names::vanilla and item_names::vanilla
gain ALL arrays, and a data.rs unit test counts the constants in each
module's source so a constant missing from ALL fails. MAX_UNDERGROUND is
now pub, matching the rest of the planner crate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PTSQFMEdmByebgkoa9RiqH
…(port stage 13a)

FactorioToolsApi.ts was generated from the WebApp's swagger.json, which goes
away with the .NET code in 13b. It is now a hand-maintained file: the unused
HTTP client plumbing is gone, along with @ts-nocheck and the generated banner,
and it is linted and formatted like the rest of src/.

FactorioToolsApi.test.ts checks it against the plan and normalize responses
in rust/contract/fixtures/contract.jsonl: typed samples the compiler checks
with `satisfies Required<...>`, the key shape and value types of every
recorded response, every strategy and quality name the contract writes, and
the error envelope shape.

Also drops the swagger-gen script and the swagger-typescript-api dependency,
the CI step that regenerated the client, the client's .gitattributes line,
and ApiError.response, which nothing has set since the site moved to the
in-browser planner.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PTSQFMEdmByebgkoa9RiqH
The node:fs read needed a `/// <reference types="node" />`, which puts
Node's globals into the type-check of the whole app. vite/client already
types `*?raw` imports as strings, and vitest resolves the file outside
src/vue, so the test now imports the jsonl that way. The production build
does not include it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PTSQFMEdmByebgkoa9RiqH
`cargo run --release -p snapshots --bin normalize-lists` rewrites big-list.txt
and small-list.txt the way the C# CLI's `oil-field normalize` does: big less
small, then small less the new big, blueprints cleaned to their pumpjacks,
repeats dropped, comments kept in the small list only.

Two differences on purpose:
- A blueprint's identity is its normalized JSON, the text SerializeBlueprint
  compresses. A line whose own data decodes strictly to exactly that text is
  kept as written, and lines are compared by that text. Neither flate2 nor
  .NET 10 reproduces the committed compressed bytes, so a literal port (or the
  C# itself today) would rewrite all 1,208 blueprint lines with no change.
- No console prompt for a line that does not normalize: it fails naming the
  file and line, and --keep-invalid keeps such lines as the C#'s "y" does.

tests/normalize.rs checks the committed lists are already normalized: no
blueprint twice (by JSON, so a recompressed copy counts), none in both lists,
and the tool's logic changes neither file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PTSQFMEdmByebgkoa9RiqH
… stage 13a)

The snapshot tests compared every *.verified.txt with the Rust output, but
only .NET Verify could write them. With UPDATE_SNAPSHOTS=1 the same tests
write instead: they create missing files, rewrite changed ones (a UTF-8 byte
order mark, then the text with LF line ends, as Verify writes) and delete
*.verified.txt files no case produces any more. Without it nothing changes,
except that failures now name the command.

The pinned count of 2,748 snapshot files is replaced by a count derived from
the two blueprint lists, so a new test blueprint needs no edit there, and
update mode checks the case set before deleting by it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PTSQFMEdmByebgkoa9RiqH
The site builds the Rust planner with `npm run build-wasm`, and the
devcontainer had no Rust, so the Devcontainer job built the planner on the
runner and copied it in. Now the image carries the toolchain and the job
builds and tests Rust inside the container.

- Dockerfile: rustup-init 1.29.1, pinned and checksummed like Node, with the
  rust-toolchain.toml pin (1.98.1, rustfmt, clippy, wasm32) pre-installed
  for the ubuntu user, plus rust-src for rust-analyzer. gcc and libc6-dev
  for linking. Self-update is off so the rustup pin holds.
- devcontainer.json: named volumes for rust/target and the cargo registry
  and git caches, chowned in postCreate; postCreate ends with build-wasm;
  rust-analyzer added.
- devcontainer.yml: rust/rust-toolchain.toml in the paths filter; the
  toolchain step checks RUST_VERSION against the toolchain file, rustc and
  cargo in rust/, and the wasm32 target; the runner-side planner build is
  gone, replaced by build-wasm in the front-end step; new cargo test step.
- CLAUDE.md Devcontainer section: what the container covers now, with
  measured timings.

.NET, Lua and PowerShell stay until stage 13b removes the C#.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PTSQFMEdmByebgkoa9RiqH
Covers the plannerDefaults generator, the hand-maintained API types and
their test, adding a test blueprint with normalize-lists and
UPDATE_SNAPSHOTS, changing planner output on purpose, and the Rust port
of the Factorio oracle test. The .NET and Lua docs stay until 13b
removes those builds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PTSQFMEdmByebgkoa9RiqH
@coderabbitai

coderabbitai Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 8ac06ee6-ab1a-4fae-9159-d29dfa441c05
📥 Commits

Reviewing files that changed from the base of the PR and between 0532d92 and 7d05e83.

⛔ Files ignored due to path filters: 2
  • rust/Cargo.lock is excluded by !**/*.lock
  • src/vue/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing: 25
  • .devcontainer/Dockerfile
  • .devcontainer/devcontainer.json
  • .gitattributes
  • .github/workflows/ci.yml
  • .github/workflows/devcontainer.yml
  • CLAUDE.md
  • rust/blueprint/src/write.rs
  • rust/contract/tests/planner_defaults.rs
  • rust/planner/src/data.rs
  • rust/planner/src/steps/plan_underground_pipes.rs
  • rust/planner/tests/factorio_oracle.rs
  • rust/snapshots/Cargo.toml
  • rust/snapshots/src/lib.rs
  • rust/snapshots/src/normalize.rs
  • rust/snapshots/tests/normalize.rs
  • rust/snapshots/tests/score.rs
  • rust/snapshots/tests/snapshots.rs
  • rust/snapshots/tools/normalize-lists.rs
  • src/vue/.prettierignore
  • src/vue/eslint.config.ts
  • src/vue/package.json
  • src/vue/src/components/ResponseErrorView.vue
  • src/vue/src/lib/FactorioToolsApi.test.ts
  • src/vue/src/lib/FactorioToolsApi.ts
  • src/vue/src/lib/OilFieldPlanner.ts
💤 Files with no reviewable changes: 4
  • src/vue/src/components/ResponseErrorView.vue
  • src/vue/.prettierignore
  • .github/workflows/ci.yml
  • src/vue/src/lib/OilFieldPlanner.ts

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.

📝 Walkthrough

The pull request adds Rust development-container and CI support, Rust planner and snapshot tests, blueprint-list normalization tools, and a hand-maintained Vue API contract validated against Rust fixtures.

Changes: Rust planner integration

Layer / File(s) Summary
Rust development environment and CI
.devcontainer/Dockerfile, .devcontainer/devcontainer.json, .github/workflows/devcontainer.yml, CLAUDE.md
The devcontainer installs pinned Rust tooling and configures Cargo volumes, Rust Analyzer, and the WASM build. CI validates the Rust toolchain, runs Rust tests in the container, and builds the frontend WASM target there.
Planner defaults and Factorio oracle checks
rust/contract/tests/planner_defaults.rs, rust/planner/src/data.rs, rust/planner/src/steps/plan_underground_pipes.rs, rust/planner/tests/factorio_oracle.rs, rust/blueprint/src/write.rs, CLAUDE.md
Rust tests verify generated planner defaults, vanilla names, geometry, direction encoding, underground distance, and planner footprints against the Factorio oracle. MAX_UNDERGROUND is public for validation.
Blueprint normalization and snapshot updates
rust/snapshots/Cargo.toml, rust/snapshots/src/*, rust/snapshots/tests/*, rust/snapshots/tools/normalize-lists.rs, CLAUDE.md
The snapshots crate adds blueprint-list normalization and a CLI. Snapshot update mode writes created or changed snapshots and removes unchecked snapshots. Tests validate list normalization and snapshot coverage.
Vue API contract and build integration
src/vue/src/lib/FactorioToolsApi.ts, src/vue/src/lib/FactorioToolsApi.test.ts, src/vue/src/lib/OilFieldPlanner.ts, src/vue/src/components/ResponseErrorView.vue, src/vue/package.json, src/vue/eslint.config.ts, src/vue/.prettierignore, .github/workflows/ci.yml, .gitattributes, CLAUDE.md
The Vue API contract is hand-maintained and checked against Rust fixtures. Swagger generation and its CI comparison are removed. ApiError no longer has a response field, and the error view no longer renders the raw response body.

Priority: ➖ Normal

Estimated code review effort: 4 (Complex) | ~60 minutes

Change: Refactor

Sequence Diagram(s)

sequenceDiagram
  participant CLI as normalize-lists
  participant Normalizer as normalize_lists
  participant Files as Blueprint list files
  CLI->>Files: Read big-list.txt and small-list.txt
  CLI->>Normalizer: Pass list contents and keep_invalid
  Normalizer-->>CLI: Return normalized list contents
  CLI->>Files: Rewrite changed lists
Loading
sequenceDiagram
  participant Tests as Snapshot tests
  participant Planner
  participant Snapshots as Snapshot library
  participant Files as Snapshot files
  Tests->>Planner: Generate planner results
  Planner-->>Snapshots: Return successful results
  Snapshots->>Files: Compare snapshots or write in update mode
  Snapshots-->>Tests: Report comparison or write outcomes
Loading

Possibly related issues

  • FactoryGameFan/FactorioTools#175 — The CI changes affect which checks can be required after the Rust port, but this pull request does not implement branch protection.

Suggested reviewers: joelverhagen

Merge Risk: 🔵 Low · up to 7d05e

A write failure could leave the blueprint lists temporarily inconsistent, but the files can be restored. The change is mergeable with awareness of that narrow failure case.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 78.95% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 76 functions across 14 files. (7 skipped:… Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title accurately summarizes the main changes: porting the remaining C#-only jobs to Rust and adding Rust support to the devcontainer.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

Docstring coverage is 78.95% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 76 functions across 14 files. (7 skipped: 7 unsupported.)

Resolution

Write docstrings for the functions missing them to satisfy the coverage threshold.

✨ Finishing Touches: 💡 1
📝 Generate docstrings: 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@wormeyman
wormeyman merged commit ee83ffc into main Oct 21, 2026
9 checks passed
@wormeyman
wormeyman deleted the port/stage-13a branch October 21, 2026 13:45
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