From c6755e24b86f1a20c7ae6988580044af54ed4b07 Mon Sep 17 00:00:00 2001 From: Mura Li <2606021+typeless@users.noreply.github.com> Date: Thu, 13 Aug 2026 17:25:36 +0800 Subject: [PATCH] State three laws the record-damage cycle left only in the code The #383 cycle settled three questions and left every answer implicit in the code, where it reads as a choice someone made rather than a rule someone must keep. A later change can quietly restore any of the three to a judgement call. DESIGN.md now states all three. An announcement filter announces by default. Enumerating error codes selects the wording of a particular announcement, or excuses one proven benign; it is never the condition for being announced at all. The announced set therefore grows every time the enum does while the benign set stays closed, so a rejection added later announces unless someone deliberately excuses it, instead of falling silent by omission. load_old_index (src/cli/context.cpp) is the standing example, with IndexVersionMismatch the one excused code. The site keeps its one-line comment and DESIGN.md carries the class law; the law ranges over filters whose default outcome would otherwise be silence, not over any dispatch switch whose every arm acts. ErrorCode is an in-process vocabulary. No value is serialized, mapped back from persisted bytes, or emitted as a stable token in machine-read output -- the on-disk record carries its own magic, version and layout instead. Enumerators may therefore be added, renamed or deleted freely, and have been: IndexTruncated and InvalidFormat are both gone from the enum. That freedom lasts exactly as long as the in-process boundary holds, which is the reason to write it down rather than leave it for whoever first wants to print a code to rediscover, or to violate. Atomicity entitles the reader to a conclusion, and that conclusion is what makes announcing damage honest rather than presumptuous. Because an interrupted write leaves the previous record in place instead of a prefix of the new one, a record that fails validation was damaged after putup wrote it, by something other than putup's own writer -- so the reader may announce damage rather than suspect a half-finished write of its own. The entitlement is wholeness, not freshness: which whole record survives a crash turns on directory-entry durability, which is the filesystem's affair. It covers the files written through pup::platform::atomic_write, not putup's outputs generally, since build outputs are written by user commands under no such regime. A whole record from outside the readable version window is not damage either. Doc-only, and nothing further is owed: the code fix shipped earlier in the cycle -- src/index/reader.cpp already returns IndexDamaged for the too-small and bad-magic rows -- and REQ-READ-ANNOUNCE-DAMAGE (spec/requirements/record-read.ears.md) already pins the observable half with four discharges. `make spec-check` passes at 105 requirements and 0 gaps. No suite was run and none is needed; nothing under src/ or test/ changed. Each claim was checked against the line it describes rather than against the cycle's own notes, and a second model reviewed the prose and swept independently for any site that persists or prints an ErrorCode as a machine-read token, finding none. One leg is not verified here: that a Win32 replacement leaves the previous record whole rests on external documentation, because no crash-injection test for atomic_write exists on either platform. The prose claims wholeness rather than strict MoveFileExW atomicity, so it is not hedged -- but nothing in this tree exercises it. The stale "Write process" list under ### IndexWriter is deliberately left untouched. It misplaces both destination and layer: serialization builds a buffer in memory (src/index/writer.cpp) and the temporary file is created inside atomic_write, so the checksum and footer are produced during serialization rather than after any file exists. That defect is independent of this change and gets its own issue. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_014wx1bWwRf23eFT82y9D641 --- DESIGN.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/DESIGN.md b/DESIGN.md index 7b2c60c5..e50389d2 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -205,6 +205,25 @@ Error codes are categorized: | Graph | CyclicDependency, UnknownMacro | | Exec | CommandFailed, MissingInput | +When a consumer filters these codes to decide what a build announces, announcing is the default +arm's job: enumeration selects the wording of a particular announcement, or excuses an exception +proven benign, but it is never the condition for being announced at all. The announced set grows +every time the enum does while the benign set stays closed, so a rejection added later announces +unless it is deliberately excused, instead of falling silent by omission. `load_old_index` +(`src/cli/context.cpp`) is the standing example — `IndexVersionMismatch` is the one excused code, +and the two damage codes appear as arms only to word their warnings — and REQ-READ-ANNOUNCE-DAMAGE +(`spec/requirements/record-read.ears.md`) pins the observable half. The law ranges over +announcement filters as a class, meaning those whose default outcome would otherwise be silence; +a dispatch switch whose every arm acts is not one. + +`ErrorCode` is an in-process vocabulary: no value is serialized, mapped back from persisted bytes, +or emitted as a stable token in machine-read output — the on-disk index carries its own magic, +version, and layout instead. Enumerators may therefore be added, renamed, or deleted freely, and +have been: `IndexTruncated` and `InvalidFormat` are both gone from the enum. That freedom lasts +exactly as long as the in-process boundary holds, and the first code to cross a process or +persistence boundary joins a compatibility regime like the index format's. Changing the enum under +an announcement filter is safe by default for the reason above — the default arm announces. + --- ## Memory Management @@ -1144,6 +1163,16 @@ Write process: 3. Write footer with checksum 4. Atomic rename to final path +What this buys the reader: the record at `.pup/index` was whole when putup last wrote it, because +an interrupted write leaves the previous record in place rather than a prefix of the new one. A +record that fails validation was therefore damaged after it was written, by something other than +putup's own writer, and the reader is entitled to announce damage instead of suspecting a +half-finished write of its own. The entitlement is wholeness, not freshness — which whole record +survives a crash turns on directory-entry durability, which is the filesystem's affair — and it +covers the files written through `pup::platform::atomic_write`, not putup's outputs generally, +since build outputs are written by user commands under no such regime. A whole record from outside +the readable version window is not damage either. + --- ## Execution Module