From 8da5b5db32fc2d120544b14df88f8254a25a54d2 Mon Sep 17 00:00:00 2001 From: Eric J Date: Mon, 17 Aug 2026 12:14:14 -0700 Subject: [PATCH] Add CLAUDE.md, and point at the blog's search Two things, both about knowledge that had nowhere to live. The repo had no CLAUDE.md, so everything learned by running the game sat only in a session's memory. Most of it is invisible from the code and expensive to rediscover: Factorio writes nothing to stderr, omission in mod-list.json means enabled, read-data is layout-dependent, script.on_init takes exactly one handler, serde_json is one ULP out on long decimals, preserve_order must stay off, runtime-api.json publishes no defines values, and 2.1 deleted LuaEntity.fluidbox along with the whole LuaFluidBox class. Each of those is recorded with what was measured rather than as a rule to follow, because the reason is the part that stops someone undoing it. A reader who does not know why preserve_order is off will turn it on. The section leads with the reason it exists: three real defects here once survived 60 unit tests and were caught by the first run against the actual game, because the fake encoded the same wrong beliefs the code did. Second, the README said the blog "carries the 10 most recent posts only". That is true of the Atom feed and wrong about the blog. There is a search endpoint, https://factorio.com/blog/search/, and it covers the whole archive - a search for "mirroring" returns FFF #80 from 2015. That matters because the feed alone cannot answer "what shipped in version X", which is the actual question after a game update. Measured 2026-08-17: search returns HTML, about 10 results, no total. Word choice matters more than expected - "mirroring" does NOT return FFF #442, the post that introduced entity mirroring, while "flip" and "fluid" both do. So the note says to try several wordings before concluding a post does not exist. Docs only. fmt, clippy and test all pass: 113 unit tests plus 6 install-gated. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01P9FADuTnjE7SFEQWnpNhfc --- CLAUDE.md | 188 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 26 +++++--- 2 files changed, 205 insertions(+), 9 deletions(-) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..9532e98 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,188 @@ +# CLAUDE.md + +Guidance for Claude Code (claude.ai/code) working in this repository. + +## What this is + +`factorio-oracle` runs a real Factorio install headless and reports what it did. +It owns discovery, mod scaffolding, launching and reading results back. It owns +**none** of the analysis. + +That split is the whole design. A probe compares the game against a consumer's +own reimplementation, so the comparison has to run in the consumer's language. +The interface is therefore JSON in and JSON out, not a probe framework. Four +repos rely on it: FactorioTools, factorio-blueprint-editor, FactorioMapWebUI and +FactorioWikiDamageThresholds. + +**The migration rule is new probes only.** Nothing already working in those +repos gets rewritten to use this. Adoption happens when someone writes a probe +they did not have before. + +## Prerequisites + +- Rust **1.97.1**, pinned in `rust-toolchain.toml`. The pin exists so a shared + tool four repos depend on does not change behaviour because a contributor has + a different rustup default. +- A real Factorio install for the integration tests. Without one they skip + rather than fail, so a green run on a machine with no game proves less than it + looks. Check which happened before trusting it. + +## Common commands + +```bash +# Exactly what CI runs, in order. Run all three before claiming anything passes. +cargo fmt --all -- --check && cargo clippy --all-targets -- -D warnings && cargo test --all-targets + +# Find installs +cargo run -- installs list + +# Run a probe and read the result +cargo run -- run --probe examples/pumpjack-terminals/probe.json --work-dir /tmp/pj +cat /tmp/pj/write/script-output/oracle-dump.json + +# Reproduce FactorioTools' committed fixture +cargo run -- run --probe dump-data.json --work-dir /tmp/w > /tmp/run.json +cargo run -- trim --run /tmp/run.json --spec trim-spec.json --out fixture.json [--check] +``` + +Test counts to expect: **113 unit tests**, plus **6 install-gated integration +tests** (3 in `tests/acceptance.rs`, 3 in `tests/real_game.rs`). + +## Layout + +| Module | Job | +| --- | --- | +| `install.rs` | Finding installs and working out where their pieces live | +| `version.rs` | Reading a version out of `factorio --version` | +| `probe.rs` | The JSON document a consumer hands in | +| `scaffold.rs` | Writing the throwaway mod's files and the isolated config | +| `lua.rs` | The only place this tool writes Lua on a consumer's behalf | +| `args.rs` | The argument vector, which differs per mode | +| `spawn.rs` | The one boundary that touches processes, behind a trait | +| `outcome.rs` | Deciding whether a run succeeded, per mode | +| `run.rs` | Wiring the pure builders to disk and a spawner | +| `numbers.rs` | Preserving the bits the game produced | +| `trim/` | Cutting a full `data.raw` dump down to a consumer's slice | + +Five run modes, and **the success predicate differs per mode**: `dump-data` +(no mod at all, the mod dir exists only to be empty), `create`, `interactive` +(a human at the keyboard), `preview` (**exits 0** on success), and `read-only` +(files on disk, no binary). + +## Measured facts + +**Read this section before changing anything that talks to the game.** Every +item cost a failed run to learn, and none is visible from the code. Three real +defects once survived 60 unit tests here and were caught by the first run +against the actual game, because the fake encoded the same wrong beliefs the +code did. **A fake can only be wrong in the ways its author already considered.** + +- **Factorio writes nothing to stderr.** Everything, including an + `error("DUMPED-OK")` sentinel, goes to stdout. Measured three ways on 2.1.14, + and again on Windows: stderr was zero bytes every time. The original code + checked stderr, so the sentinel was never seen on any real run. +- **Omission in `mod-list.json` means enabled.** Factorio rewrites the file at + startup and adds back every bundled mod the file does not mention, with + `enabled: true`. A file naming only `base` comes back naming five, all loaded. + An explicit `enabled: false` **is** honoured, so naming a mod is the only way + to get a smaller game than the install ships with. An empty mod directory + keeps out *user* mods and nothing else. +- **`read-data` is layout-dependent, so never write it as a relative token.** + `__PATH__executable__/../data` is right for a macOS bundle and wrong + everywhere else: Windows and Linux put the binary at `bin/x64/`, so it + resolves to `bin/data` and the game exits 1 with "There is no package core + in". Use the absolute `data_dir` that `resolve_layout` already computes. + Windows accepts either slash style, so `Path::display()` needs no rewriting. +- **`--write-data` is not a CLI flag on any platform.** It is a `config.ini` + `[path]` key reached with `-c`. `--dump-data` honours it, and the dump lands + in that directory's own `script-output`, which is what makes a stale dump from + an earlier capture impossible to pick up. +- **`script.on_init` takes exactly one handler.** A prelude registering it is + silently replaced by the consumer's own - no error, the handler simply never + runs. 17 of 18 probes in factorio-blueprint-editor register an `on_init`, so a + prelude must not. `helpers.write_file` works at `control.lua` toplevel with no + event at all, which has no collision surface and does not wait a tick. +- **`serde_json` mis-parses long decimal literals by one ULP**, in both + directions, on values like + `0.394500000000000028421709430404007434844970703125`. The crate uses + `arbitrary_precision` and re-parses through `std`'s `f64::from_str`, which is + correctly rounded and agrees with CPython bit for bit. **Do not remove that.** +- **`preserve_order` must stay off** so `serde_json::Map` is a `BTreeMap` and + output sorts to match Python's `sort_keys=True`. Byte-for-byte determinism is + a hard requirement, because `--check` is a diff. +- **`runtime-api.json` publishes no `defines` values.** Across all 1,554 entries + the only keys are `name`, `order` and `description`. Reading `order` as the + value is right today only because Factorio declares directions clockwise from + `north = 0` with no gaps. Read the real values with a probe instead + (`defines_from: "probe"`). +- **`--dump-data` is byte-identical across architectures.** Same build, macOS + arm64 Steam and Windows x64 standalone both produce sha256 `acc944e6...` from + byte-identical inputs. So the acceptance test is not platform-specific. +- **`--create` needs no map-gen settings file**, and `--map-gen-seed` overrides + a seed inside the settings file. The CLI takes one `seed` and writes both + channels, which makes the precedence irrelevant. +- **`loadedMods` cannot come from the active-mods prelude**, because + `script.active_mods` never reports `core` and the fixtures list it. Grep the + game's stdout for `Loading mod `. + +### Writing Lua for a probe + +- **Factorio 2.1 deleted `LuaEntity.fluidbox` and the whole `LuaFluidBox` + class**, flattening it onto `LuaEntity` as `get_fluid_box_*`. So + `entity.fluidbox.get_pipe_connections(i)` became + `entity.get_fluid_box_pipe_connections(i)`. + `entity.prototype.fluidbox_prototypes[i].pipe_connections` still works in + both, so prototype-level reads need no branching. +- **Feature detection needs a `pcall`.** Reading an unknown key on `LuaEntity` + *raises* in 2.0 rather than returning nil, so the obvious guard throws on the + older version: + + ```lua + -- throws on 2.0 + if entity.get_fluid_box_pipe_connections then ... end + -- works on both + local has_new = pcall(function() return entity.get_fluid_box_pipe_connections end) + ``` + +- **Run a probe against two versions when you can.** The older one reproduces + the known-good answer, which is what earns trust in the new one. See + `examples/pumpjack-terminals`. + +### Running the game from a shell + +- **`factorio.exe` is a GUI-subsystem binary on Windows.** Launched from + PowerShell it returns immediately, does not wait, and captures no stdout - the + run looks like a 0.03 second success that produced nothing. Use + `Start-Process -Wait -NoNewWindow -PassThru -RedirectStandardOutput`. Rust's + `Command::spawn` plus `wait_with_output` waits correctly, so the tool itself + is fine; this bites shell invocations. +- **Nothing here has a timeout by default in the consumer repos**, which is one + of the things this tool exists to fix. A hung game hangs the capture forever. + +## Sources + +Two kinds, and the difference decides how each may be used. `README.md` has the +full version. + +**Authoritative** sources say what the game accepts, ship with the install, read +offline byte-identically, and are the only things a capture may depend on: +`--dump-data`, `data/*/migrations/*.json`, `doc-html/runtime-api.json`. + +**Reference** sources say *why* something changed, which is what tells you which +captured value now needs review. Nothing automated reads them and nothing +should: `data/changelog.txt`, and the Friday Facts blog. + +Search the blog at `https://factorio.com/blog/search/`. It covers the +whole archive. Word choice matters: `mirroring` does not return FFF #442, the +post that introduced entity mirroring, while `flip` and `fluid` both do. + +## Conventions + +- **Hyphens only.** No em dashes or en dashes, in files or commit messages. +- **Comments carry the reasoning, not the mechanics.** The measured facts above + live beside the code that depends on them, with the measurement and its date. + That is deliberate: a reader who does not know why `preserve_order` is off + will turn it on. +- **Prefer measuring the game over reasoning about it.** If a claim about + Factorio is not backed by a run, say so. +- Do not push or commit unless asked. Branch first if on `main`. diff --git a/README.md b/README.md index 22f55d8..f408060 100644 --- a/README.md +++ b/README.md @@ -58,17 +58,25 @@ Fresh Paint", is the post that explains it, and the same release added `use_mirroring` and `migrate_horizontal_mirroring` to that prototype. Reading it is how you learn the change is about entity mirroring rather than a typo fix. +**Search is the way in**: `https://factorio.com/blog/search/`. It covers +the whole archive, not just recent posts - a search for `mirroring` returns +FFF #80, from 2015. + Practical notes, measured 2026-08-17: -- The feed is Atom, at , served as - `application/atom+xml`. -- It carries the **10 most recent posts only**. It is a what-changed-lately - feed, not an archive, so it cannot answer "what shipped in version X" on its - own. -- Each entry embeds the full post body, so recent posts need no second fetch. -- Older posts live at `https://factorio.com/blog/post/fff-`. -- It is the only source here that needs the network. That is why it can never - gate a capture: captures must stay reproducible offline and byte for byte. +- Search returns HTML, not JSON, with about 10 results and no total shown. +- **Word choice matters more than you would expect.** Searching `mirroring` + does *not* return FFF #442, the post that introduced entity mirroring, while + `flip` and `fluid` both do. Try two or three wordings before concluding a post + does not exist. A bare number works too: `search/442`. +- Direct post URLs are `https://factorio.com/blog/post/fff-`. +- There is an Atom feed at , served as + `application/atom+xml`, with each entry embedding the full post body. It + carries the **10 most recent posts only**, so it answers "what changed lately" + rather than "what shipped in version X". Use the search for that. +- The blog is the only source here that needs the network. That is why it can + never gate a capture: captures must stay reproducible offline and byte for + byte. ## Examples