docs(installer): record Phase 3 — the patch tier as built
PLAN.md gains a "Phase 3 as built" section covering the decisions the plan had left open, and §2.2, §2.2.1 and §7.0 are brought in line with what shipped: * The engine is fully native. §2.2.1 wrote rung 1 as "apply verbatim with git apply", but §1 chose the release tarball so there would be no git on the shard host, and rung 2 needs a native applier anyway. Rung 1 keeps its stronger verdict and shares rung 2's write path. On the real files this is not academic — the shipped patches are CRLF and two of their three targets are LF, so git apply refuses patches the installer places correctly. * §7.0 documents `patch_tier` in the overlay manifest. Which patches form one unit, which companion follows which, whether a core rebuild is needed and what declining costs are not derivable from a diff, so the release declares them and adding a patch regenerates metadata rather than an installer. * §2.2 gains the pre-image cache and the widened patch cache, and §2.2.1 gains the second, per-feature level of the all-or-nothing rule. INSTALL.md's illustrated tier output is replaced with the real thing, the status banner now says `install` is complete, §3's path tables list patches/originals/, and Appendix A2 names the line-ending trap that makes git apply refuse a patch whose region is visibly untouched. Refs: RunicGateway/installer#6, RunicGateway/servuo-plugins#10 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,15 +1,15 @@
|
||||
# Runic Gateway Installer — plan
|
||||
|
||||
Status: **Phases 1 and 2 built, on `edge`.** Phase 0's prerequisites all landed, the installer repo
|
||||
publishes the bundle manifest, and [`INSTALL.md`](INSTALL.md) specified the operator-facing run
|
||||
Status: **Phases 1, 2 and 3 built, on `edge`.** Phase 0's prerequisites all landed, the installer
|
||||
repo publishes the bundle manifest, and [`INSTALL.md`](INSTALL.md) specified the operator-facing run
|
||||
before the binary existed. The crate now implements the installer core (bundle resolution, ServUO
|
||||
detection and validation, the overlay sync, `install.json` — [Phase 1 as
|
||||
built](#phase-1--installer-core)) and the sidecar half (binary, config, service, token handoff —
|
||||
[Phase 2 as built](#phase-2--uo-link-install-and-service)). Both are on the `edge` branch, not
|
||||
`main`, so no half-capable binary is released. **Phase 3 (the patch tier) is next, and the
|
||||
`edge → main` cutover follows it** rather than Phase 2: `INSTALL.md` §4 describes the tier as part
|
||||
of the run, and a first release whose every patch-tier answer is "not implemented" is the same
|
||||
half-capable binary that kept Phase 1 off `main`.
|
||||
built](#phase-1--installer-core)), the sidecar half (binary, config, service, token handoff —
|
||||
[Phase 2 as built](#phase-2--uo-link-install-and-service)), and the patch tier (the rung ladder, the
|
||||
unsupported-version path, the cached patch set — [Phase 3 as built](#phase-3--patch-tier-opt-in)).
|
||||
All three are on the `edge` branch, not `main`, so no half-capable binary is released. **The
|
||||
`edge → main` cutover is next**, and it now cuts a binary that does everything `INSTALL.md`
|
||||
describes except `doctor`/`update`/`uninstall`, each of which says which phase it arrives in.
|
||||
This document is the design of record; it supersedes the informal overview it grew out of, which
|
||||
described a ServUO integration that does not match how `servuo-plugins` actually ships (see
|
||||
[Corrections](#corrections-to-the-original-overview)).
|
||||
@@ -102,10 +102,19 @@ most real shards are hand-modified. Therefore:
|
||||
audit forwarding**.
|
||||
- The `EventSink.cs` patch must warn loudly that a **core solution rebuild** is required, not just a
|
||||
shard restart.
|
||||
- Record applied patches in `install.json`, **and cache the applied `.patch` files** next to it
|
||||
- Record applied patches in `install.json`, **and cache the `.patch` files** next to it
|
||||
(`/etc/runicgateway/patches/`, `%ProgramData%\RunicGateway\patches\`). Re-runs stay idempotent,
|
||||
and uninstall can print the exact hunks offline long after the release tarball is gone (§5,
|
||||
Phase 4).
|
||||
Phase 4). As built this caches every patch the tier *evaluated*, not only those that applied,
|
||||
because the refusal message names that path as the file to apply by hand.
|
||||
- **Cache the pre-image of every file the tier edits**, under `patches/originals/`, mirroring its
|
||||
path in the ServUO tree. It is written before the first edit and never overwritten, so a revert
|
||||
can be verified byte-for-byte rather than reconstructed from a printed diff — which matters most
|
||||
after a `region-match` apply, where the surrounding file was already the operator's. It stays out
|
||||
of the ServUO tree, since uninstall has promised never to clean up in there.
|
||||
- **A `.patch` does not carry everything the tier needs.** Which patches form one unit, which
|
||||
companion `.cs` follows which, whether a core rebuild is required and what declining costs are
|
||||
declared by the overlay release and read from its manifest — see §7.0.
|
||||
|
||||
#### 2.2.1 A whole-file hash mismatch is not a verdict — check the region
|
||||
|
||||
@@ -137,9 +146,13 @@ rules:
|
||||
- **Line numbers are advisory.** The hunk header's offsets are used only to prefer the nearest
|
||||
candidate when reporting; the match itself is by content, since insertions above the region shift
|
||||
every number below it.
|
||||
- **All-or-nothing per patch file.** If one hunk of a patch reaches rung 3, none of that patch's
|
||||
hunks are applied. A half-patched `EventSink.cs` compiles against a companion `.cs` that expects
|
||||
the whole thing, and a partial apply is harder for an operator to unpick than an untouched file.
|
||||
- **All-or-nothing per patch file, and again per feature.** If one hunk of a patch reaches rung 3,
|
||||
none of that patch's hunks are applied — a half-patched `EventSink.cs` compiles against a
|
||||
companion `.cs` that expects the whole thing, and a partial apply is harder for an operator to
|
||||
unpick than an untouched file. The same rule then applies across the patches of one feature: the
|
||||
two vendor-sale patches are a unit (the event, the call site, and the subscriber that needs both),
|
||||
so a patch that *could* have been placed is held back when a sibling cannot be — and the run says
|
||||
that rather than reporting it as applied.
|
||||
- **Rung 0 is checked first and is also all-or-nothing.** A file where some hunks are already
|
||||
present and others are not is a hand-merge in progress, not an idempotent re-run — that is
|
||||
rung 3.
|
||||
@@ -631,6 +644,74 @@ pick the first). Fixtures are cheap: the three stock 57.4 files, each with a han
|
||||
patched region (must reach rung 2), an edit inside it (must reach rung 3), and an already-patched
|
||||
copy (must reach rung 0).
|
||||
|
||||
**As built** ([installer#6](https://gitea.whitlocktech.com/RunicGateway/installer/pulls/6), with the
|
||||
metadata half in [servuo-plugins#10](https://gitea.whitlocktech.com/RunicGateway/servuo-plugins/pulls/10))
|
||||
— `src/diff.rs` (the parser), `src/patch.rs` (the ladder and the applier) and `src/tier.rs` (consent,
|
||||
writing, reporting, recording), wired into the same `install` run between the overlay sync and the
|
||||
sidecar. The decisions that were not already settled above:
|
||||
|
||||
- **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. One engine now serves both: 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. That
|
||||
leaves one set of CRLF and whitespace behaviours to reason about instead of two, and a bug report
|
||||
never has to say which engine ran. It is also not academic: the shipped `.patch` files are CRLF in
|
||||
a Windows checkout while two of their three targets are LF, so `git apply` **refuses** patches
|
||||
this places correctly.
|
||||
- **What a `.patch` cannot say 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 all things a diff does not carry. `servuo-plugins/patches/tier.json`
|
||||
declares them and the release workflow folds them into `manifest.json` as `patch_tier` (§7.0), so
|
||||
adding a patch regenerates release metadata rather than requiring an installer release — the same
|
||||
rule §7.1 applies to the bundle. Overlay `v0.1.1` is in the current bundle and declares nothing,
|
||||
so the installer carries a built-in description of exactly that release; a declared tier always
|
||||
wins. A checked-in fixture of the release workflow's **own jq output** asserts the two descriptions
|
||||
are identical, so the two repos cannot drift apart quietly — the failure mode otherwise is a tier
|
||||
that is silently never offered.
|
||||
- **All-or-nothing gained a second level.** §2.2.1 makes it per *patch file*; the tier is also
|
||||
all-or-nothing per **feature**, because the two vendor-sale patches are one unit — `EventSink.cs`
|
||||
grows the event, `PlayerVendorGumps.cs` raises it, and the companion subscribes to it. Applying
|
||||
either alone yields a tree that does not compile or silently never emits. A patch that could have
|
||||
been placed but was held back by a sibling says so in as many words; reporting it as applied is
|
||||
the exact misreading this tier exists to prevent.
|
||||
- **The pre-image of every patched file is cached**, under `<state>/patches/originals/`, mirroring
|
||||
its path in the ServUO tree. The tier is the only part of the installer that edits a file the
|
||||
operator owns, and this is what turns "here are the hunks we added" into a revert anyone can
|
||||
verify — which matters most for a `region-match` apply, where the surrounding file was already
|
||||
theirs. It lives in the state directory rather than beside the file it copies, because an
|
||||
installer-owned file inside the ServUO tree is one `uninstall` has promised never to clean up. It
|
||||
is written before the first edit and never overwritten, so it stays pre-tier however many times
|
||||
`install` runs.
|
||||
- **Every patch the tier *evaluated* is cached, not only the ones that applied** — a refinement of
|
||||
§2.2's "cache the applied `.patch` files". The refusal message names that path as the file to
|
||||
apply by hand (as §4 of `INSTALL.md` already illustrated), so caching only successes would point
|
||||
an operator at a file the run had decided not to write.
|
||||
- **Rung 0 reuses the previous record whole rather than re-deriving it.** The rung is the
|
||||
support-relevant fact — how did this land? — and a later run re-deriving it answers
|
||||
`already-present` for something that first landed as `region-match`. That flip rewrites
|
||||
`install.json` on the second run of an identical install, which is the same class of bug as the
|
||||
`Bridge.cfg` comparison in Phase 1: a record describing the run instead of the state. A tree
|
||||
patched by hand per `INSTALL.md` Appendix A2 has no prior record, so there `already-present` is
|
||||
correctly what gets minted.
|
||||
- **Declining never erases what an earlier run applied**, and no longer claims a loss that is not
|
||||
real. `--no-patches` and an unselected prompt both carry the previous `patches` section through
|
||||
untouched, as `--verify` does — and the "Without it:" line now names only the features the record
|
||||
does not already show as applied.
|
||||
- **Withholding `--patches-unsupported-servuo` skips the tier loudly rather than failing the run.**
|
||||
By that point the overlay is deployed and the sidecar is about to be installed; turning a completed
|
||||
base install into exit 1 over a tier documented as optional would cost the operator more than the
|
||||
tier is worth. Saying nothing would be the real failure, so it is reported where it happens.
|
||||
|
||||
Verified on this machine against the ServUO 57.4 tree at `C:\Users\colby\Desktop\ServUO`, across
|
||||
four scratch roots built from its real files: a hand-patched tree (rung 0 on both vendor-sale
|
||||
patches), a reverse-applied stock one (**rung 1 on the real `EventSink.cs`**, whose blob hash
|
||||
reproduces the patch's declared `index d30788f` pre-image), a feature resolving at mixed rungs, a
|
||||
tree with edits inside two patched regions (rung 3 — nothing written, the placeable sibling held
|
||||
back, no companions copied, and all three patches cached anyway), and a non-57.4 tree both with and
|
||||
without the extra consent flag. Three consecutive runs left `install.json` byte-identical, the
|
||||
patched files unchanged, and the cached pre-image still pre-patch.
|
||||
|
||||
### Phase 4 — diagnostics and updates
|
||||
|
||||
`runicgateway doctor` — the command that makes the whole thing supportable:
|
||||
@@ -745,14 +826,30 @@ Shipped inside every `runicgateway-overlay-<ver>.tar.gz`, generated by that repo
|
||||
"repo": "RunicGateway/servuo-plugins",
|
||||
"protocol": 3,
|
||||
"servuo": { "min_version": "57.4", "patches_verified_against": "57.4" },
|
||||
"patch_tier": {
|
||||
"features": [{
|
||||
"name": "vendor-sale",
|
||||
"summary": "vendor.sale events — player-vendor purchases with buyer, owner, item, price and commission",
|
||||
"lost": "no vendor.sale events",
|
||||
"rebuild": "core",
|
||||
"patches": [
|
||||
{ "name": "playervendor-sale-eventsink", "file": "patches/playervendor-sale-eventsink.patch", "target": "Server/EventSink.cs" },
|
||||
{ "name": "playervendor-sale-gump", "file": "patches/playervendor-sale-gump.patch", "target": "Scripts/Gumps/PlayerVendorGumps.cs" }
|
||||
],
|
||||
"companions": [
|
||||
{ "file": "patches/BridgeVendorSale.cs", "install_to": "Scripts/Custom/Bridge/BridgeVendorSale.cs" }
|
||||
]
|
||||
}]
|
||||
},
|
||||
"files": { "overlay/Config/Bridge.cfg": "32718424…", "patches/…": "…" }
|
||||
}
|
||||
```
|
||||
|
||||
`version` and `commit` come from the release engine; `protocol` and the `servuo` block are read from
|
||||
`servuo-plugins/overlay.toml`; `files` is a SHA256 per shipped file.
|
||||
`servuo-plugins/overlay.toml`; `patch_tier` is folded in from `servuo-plugins/patches/tier.json`;
|
||||
`files` is a SHA256 per shipped file.
|
||||
|
||||
Two of these carry weight beyond documentation:
|
||||
Three of these carry weight beyond documentation:
|
||||
|
||||
- **`protocol` is a hand-maintained declaration, and has to be.** The plugin announces no version on
|
||||
the wire and none is queryable before ServUO boots, so nothing in CI can derive it — which makes
|
||||
@@ -763,6 +860,18 @@ Two of these carry weight beyond documentation:
|
||||
overlay moved on"** (§5, Phase 4). The installer copies these hashes into `install.json` at deploy
|
||||
time; a later mismatch against *both* the manifest and `install.json` means upstream changed, a
|
||||
mismatch against `install.json` alone means local edits.
|
||||
- **`patch_tier` is everything a `.patch` cannot say about itself**, and is the reason the tier is
|
||||
data rather than code. Which patches form one all-or-nothing unit, which companion `.cs` may only
|
||||
be copied once that unit lands, whether the change needs a **core** solution rebuild or just the
|
||||
dynamic script build, and what the operator loses by declining are none of them derivable from a
|
||||
diff. Declaring them here means adding a patch regenerates release metadata rather than requiring
|
||||
an installer release — the rule §7.1 already applies to the bundle. The maintainer-facing source
|
||||
is `servuo-plugins/patches/tier.json`; the release workflow folds it in and removes the staged
|
||||
copy, so the tarball carries exactly one statement of the table, and gates that every `.patch` is
|
||||
described by exactly one feature, that every named patch and companion exists, and that each
|
||||
declared `target` is the file its diff actually edits. Installers older than this key ignore it;
|
||||
an installer newer than the overlay it is deploying falls back to a built-in description of the
|
||||
release that predates it (see [Phase 3 as built](#phase-3--patch-tier-opt-in)).
|
||||
|
||||
`min_version` and `patches_verified_against` are separate on purpose. The base overlay only *adds*
|
||||
files and is expected to work broadly; the patch tier diffs stock ServUO files and is verified
|
||||
|
||||
Reference in New Issue
Block a user