feat(installer): implement Phase 3 — the patch tier #6

Merged
whitlocktech merged 3 commits from feat/phase3-patch-tier into edge 2026-08-05 01:08:07 +00:00
Member

What & why

Two features need edits to stock ServUO sources, because the events they depend on do not exist. This implements the rung ladder of PLAN.md §2.2.1, the unsupported-version path of §2.2.2, and the record and cache Phase 4 will read.

Three modules, split by how they fail — a wrong answer in one corrupts a stock ServUO file, a wrong answer in the other means an operator was never asked or believes something was patched that was not:

Module Job
src/diff.rs unified-diff parsing — hunks, index blobs, \ No newline at end of file, CRLF
src/patch.rs the rung ladder, the byte-preserving applier, the declared tier + built-in fallback
src/tier.rs consent, writing, reporting, recording

Three decisions the plan had left open

1. The engine is fully native; git is never invoked. §2.2.1 wrote rung 1 as "apply verbatim with git apply", but §1 chose the release tarball precisely so there would be no git on the shard host — and rung 2 needs a native applier regardless. Rung 1 keeps its distinct, stronger verdict (the whole file reproduced the diff's index pre-image, computed as a git blob SHA1 in process) while the write goes through rung 2's code path.

This is not academic. The shipped .patch files are CRLF in a Windows checkout, and two of their three targets are LF — so git apply refuses patches this places correctly.

2. Per-patch metadata is declared by the release (RunicGateway/servuo-plugins#10), with a built-in fallback for overlay v0.1.1, which is in the current bundle and declares nothing. A checked-in fixture of the release workflow's own jq output asserts the declared tier and the fallback are identical, so the repos cannot drift quietly — the failure mode otherwise is a tier that is silently never offered.

3. Pre-images are cached in the state directory, at <state>/patches/originals/, mirroring the ServUO tree. This is the only part of the installer that edits a file the operator owns, and it is what turns "here are the hunks we added" into a revert anyone can verify — most valuable after a region-match apply, where the surrounding file was already theirs. Kept out of the ServUO tree, which uninstall has promised never to clean up.

Everything else follows §2.2.1

Exact matching with only line-ending and trailing-whitespace normalization; exactly one occurrence or it fails; all-or-nothing per patch file and again per feature; and a byte-preserving splice, so nothing outside a hunk can be reformatted and inserted lines take the target's own line ending.

How it was tested

112 unit + 9 integration tests. The integration suite runs the engine against the three real shipped patches, because the properties that matter belong to those files: the header-less one (rung 1 structurally unavailable), the four-hunk one (descending-offset splice, overlap check, all-or-nothing together), and both line-ending spellings of each.

Live, against the ServUO 57.4 tree on this machine, across four scratch roots built from its real files:

Tree Result
hand-patched rung 0 on both vendor-sale patches
reverse-applied stock rung 1 on the real EventSink.cs — its blob hash reproduces the patch's declared index d30788f pre-image
mixed one feature resolving at rung 1 and rung 0 together
regions edited rung 3 — nothing written, the placeable sibling held back, no companions copied, all three patches cached anyway
non-57.4 skipped loudly without --patches-unsupported-servuo; applied and labelled unsupported_servuo: true with it

Three consecutive runs left install.json byte-identical, the patched files unchanged, and the cached pre-image still pre-patch.

Three reporting defects the live runs caught, each fixed with a test

  • a --verify run and a held-back patch both said applied at line … for something never written;
  • the core-rebuild warning fired when nothing had been written, and listed a Scripts file as core;
  • a declined tier announced the loss of features install.json showed as applied.

Refused patches are now cached too, since the refusal message names that path as the file to apply by hand — previously it pointed at a file the run had decided not to write.

Paired with RunicGateway/servuo-plugins#10 and RunicGateway/docs#92.

Checklist

  • I have read CONTRIBUTING.md.
  • The change builds and existing tests/checks pass locally.
  • I have added or updated tests/docs where it makes sense.
  • My commits are reasonably scoped with clear messages.

AI-assisted contributions (required)

  • AI tools were used. Tool(s): Claude Code (Opus). I have reviewed and understand every change, and take responsibility for it. AI-authored commits are marked with a Co-Authored-By trailer.

License

  • I agree that my contribution is licensed under this project's license (GNU GPL v3.0 or later), and I have the right to contribute it.
## What & why Two features need edits to **stock ServUO sources**, because the events they depend on do not exist. This implements the rung ladder of PLAN.md §2.2.1, the unsupported-version path of §2.2.2, and the record and cache Phase 4 will read. Three modules, split by how they fail — a wrong answer in one corrupts a stock ServUO file, a wrong answer in the other means an operator was never asked or believes something was patched that was not: | Module | Job | |---|---| | `src/diff.rs` | unified-diff parsing — hunks, `index` blobs, `\ No newline at end of file`, CRLF | | `src/patch.rs` | the rung ladder, the byte-preserving applier, the declared tier + built-in fallback | | `src/tier.rs` | consent, writing, reporting, recording | ### Three decisions the plan had left open **1. The engine is fully native; `git` is never invoked.** §2.2.1 wrote rung 1 as "apply verbatim with `git apply`", but §1 chose the release tarball *precisely* so there would be no git on the shard host — and rung 2 needs a native applier regardless. Rung 1 keeps its distinct, stronger verdict (the whole file reproduced the diff's `index` pre-image, computed as a git blob SHA1 in process) while the write goes through rung 2's code path. This is not academic. The shipped `.patch` files are CRLF in a Windows checkout, and **two of their three targets are LF** — so `git apply` refuses patches this places correctly. **2. Per-patch metadata is declared by the release** (RunicGateway/servuo-plugins#10), with a built-in fallback for overlay `v0.1.1`, which is in the current bundle and declares nothing. A checked-in fixture of the release workflow's *own jq output* asserts the declared tier and the fallback are identical, so the repos cannot drift quietly — the failure mode otherwise is a tier that is silently never offered. **3. Pre-images are cached in the state directory**, at `<state>/patches/originals/`, mirroring the ServUO tree. This is the only part of the installer that edits a file the operator owns, and it is what turns "here are the hunks we added" into a revert anyone can **verify** — most valuable after a `region-match` apply, where the surrounding file was already theirs. Kept out of the ServUO tree, which `uninstall` has promised never to clean up. ### Everything else follows §2.2.1 Exact matching with only line-ending and trailing-whitespace normalization; exactly one occurrence or it fails; all-or-nothing per patch file **and again per feature**; and a byte-preserving splice, so nothing outside a hunk can be reformatted and inserted lines take the target's own line ending. ## How it was tested **112 unit + 9 integration tests.** The integration suite runs the engine against the three *real* shipped patches, because the properties that matter belong to those files: the header-less one (rung 1 structurally unavailable), the four-hunk one (descending-offset splice, overlap check, all-or-nothing together), and both line-ending spellings of each. **Live, against the ServUO 57.4 tree on this machine**, across four scratch roots built from its real files: | Tree | Result | |---|---| | hand-patched | rung 0 on both vendor-sale patches | | reverse-applied stock | **rung 1 on the real `EventSink.cs`** — its blob hash reproduces the patch's declared `index d30788f` pre-image | | mixed | one feature resolving at rung 1 and rung 0 together | | regions edited | rung 3 — nothing written, the *placeable* sibling held back, no companions copied, all three patches cached anyway | | non-57.4 | skipped loudly without `--patches-unsupported-servuo`; applied and labelled `unsupported_servuo: true` with it | Three consecutive runs left `install.json` byte-identical, the patched files unchanged, and the cached pre-image still pre-patch. ### Three reporting defects the live runs caught, each fixed with a test - a `--verify` run and a **held-back** patch both said `applied at line …` for something never written; - the core-rebuild warning fired when nothing had been written, and listed a **Scripts** file as core; - a declined tier announced the loss of features `install.json` showed as **applied**. Refused patches are now cached too, since the refusal message names that path as the file to apply by hand — previously it pointed at a file the run had decided not to write. Paired with RunicGateway/servuo-plugins#10 and RunicGateway/docs#92. ## Checklist - [x] I have read [CONTRIBUTING.md](CONTRIBUTING.md). - [x] The change builds and existing tests/checks pass locally. - [x] I have added or updated tests/docs where it makes sense. - [x] My commits are reasonably scoped with clear messages. ## AI-assisted contributions (required) - [x] AI tools were used. Tool(s): `Claude Code (Opus)`. I have reviewed and understand every change, and take responsibility for it. AI-authored commits are marked with a `Co-Authored-By` trailer. ## License - [x] I agree that my contribution is licensed under this project's license (**GNU GPL v3.0 or later**), and I have the right to contribute it.
wtclaude added 1 commit 2026-08-05 00:59:04 +00:00
feat(installer): implement Phase 3 — the patch tier
Some checks failed
PR Checks / rust-gates (pull_request) Failing after 46s
52d330167b
Two features need edits to stock ServUO sources, because the events they
depend on do not exist. This adds the rung ladder of PLAN.md §2.2.1, the
unsupported-version path of §2.2.2, and the record and cache Phase 4 will read.

Three decisions were not settled by the plan:

* The engine is fully native, with no `git`. §2.2.1 wrote rung 1 as "apply
  verbatim with git apply", but §1 chose the release tarball specifically so
  there would be no git on the shard host, and rung 2 needs a native applier
  regardless. Rung 1 keeps its distinct, stronger verdict — the whole file
  reproduced the diff's `index` pre-image, computed as a git blob SHA1 in
  process — while the write goes through the same code path as rung 2. On the
  real trees here that is not academic: the shipped .patch files are CRLF in a
  Windows checkout and two of their three targets are LF, so `git apply`
  refuses patches this applies correctly.

* Per-patch metadata is declared by the release, with a built-in fallback.
  Which patches form one all-or-nothing unit, which companion .cs follows
  which, whether a CORE rebuild is needed and what declining costs are not
  derivable from a diff. servuo-plugins now declares them; overlay v0.1.1 is in
  the current bundle and declares nothing, so a built-in copy stands in for it.
  A checked-in fixture of the release workflow's own jq output asserts the two
  descriptions are identical, so the repos cannot drift quietly.

* Pre-images are cached in the state directory. The tier edits files the
  operator owns, and `/etc/runicgateway/patches/originals/` is what turns "here
  are the hunks we added" into a revert anyone can verify — kept out of the
  ServUO tree, which uninstall has promised never to clean up.

Everything else follows §2.2.1: exact matching with only line-ending and
trailing-whitespace normalization, exactly one occurrence or it fails,
all-or-nothing per patch file and again per feature, and a byte-preserving
splice so nothing outside a hunk can be reformatted.

Verified against the ServUO 57.4 tree on this machine across four scratch
roots: a hand-patched tree (rung 0), a reverse-applied stock one (rung 1 on the
real EventSink.cs, its blob matching the patch's declared pre-image), a
mixed-rung feature, a tree with edits inside two patched regions (rung 3 —
nothing written, nothing held back applied, no companions copied), and a
non-57.4 tree both with and without the extra consent flag. Three consecutive
runs left install.json byte-identical and the cached pre-image still pre-patch.

Three reporting defects the live runs caught are fixed with tests: a dry run
and a held-back patch both claimed to be "applied", the core-rebuild warning
fired when nothing had been written and named a Scripts file as core, and a
declined tier announced the loss of features install.json showed as applied.
Refused patches are now cached too, since the refusal message names that path.

Refs: docs/installer/PLAN.md §2.2, §5 Phase 3

Co-Authored-By: Claude <noreply@anthropic.com>
wtclaude added 1 commit 2026-08-05 01:01:42 +00:00
fix(installer): satisfy clippy's unnecessary_sort_by on the CI toolchain
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 58s
02c5ad9839
The two descending sorts in the applier used an explicit comparator. CI runs
clippy 1.97, where `unnecessary_sort_by` flags that and `-D warnings` turns it
into a build failure; the local toolchain here is 1.94, which does not have the
lint. `sort_by_key` with `Reverse` says the same thing.

Co-Authored-By: Claude <noreply@anthropic.com>
wtclaude added 1 commit 2026-08-05 01:02:57 +00:00
docs(installer): drop phase references that are now this build's behaviour
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 2m8s
265911a58f
Five comments described the patch tier as work a later phase would do. It is
this phase, so they read as stale the moment the code landed.

Co-Authored-By: Claude <noreply@anthropic.com>
whitlocktech merged commit 0043264482 into edge 2026-08-05 01:08:07 +00:00
whitlocktech deleted branch feat/phase3-patch-tier 2026-08-05 01:08:08 +00:00
Sign in to join this conversation.
No description provided.