feat(installer): implement Phase 3 — the patch tier #6
Reference in New Issue
Block a user
No description provided.
Delete Branch "feat/phase3-patch-tier"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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:
src/diff.rsindexblobs,\ No newline at end of file, CRLFsrc/patch.rssrc/tier.rsThree decisions the plan had left open
1. The engine is fully native;
gitis never invoked. §2.2.1 wrote rung 1 as "apply verbatim withgit 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'sindexpre-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
.patchfiles are CRLF in a Windows checkout, and two of their three targets are LF — sogit applyrefuses 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 aregion-matchapply, where the surrounding file was already theirs. Kept out of the ServUO tree, whichuninstallhas 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:
EventSink.cs— its blob hash reproduces the patch's declaredindex d30788fpre-image--patches-unsupported-servuo; applied and labelledunsupported_servuo: truewith itThree consecutive runs left
install.jsonbyte-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
--verifyrun and a held-back patch both saidapplied at line …for something never written;install.jsonshowed 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
AI-assisted contributions (required)
Claude Code (Opus). I have reviewed and understand every change, and take responsibility for it. AI-authored commits are marked with aCo-Authored-Bytrailer.License