Bind the plan to the conversion and the directory, not just the files - #47
Merged
Conversation
A plan that records only file hashes answers "are these the same bytes?" but not "is this the same conversion?". Three ways it could still mean something other than what was approved: The conversion could change underneath it. The plan now records a semantics version alongside the schema version, and -Apply refuses a plan made under different conversion behaviour rather than carrying it out. That version is deliberately separate from the assembly version: tying plan validity to a release number would invalidate every plan on a release that changed nothing about conversion, which teaches people to work around the check instead of reading it. The plan also writes down the target, BOM policy, backup policy, detection mode and the guarantees the converting build provides, so the file is the whole approval and -Apply needs no ambient option state at all. The tree could change underneath it. Paths were absolute, so a plan copied alongside its tree still named the original tree - and every hash matched, because those were the files it was made from. Entries are now relative to a recorded root, applying a copy converts the tree that was approved, a missing root is refused cleanly, and an entry resolving outside the root is refused rather than followed. A plan is a file people can edit and pass around. The file could change between verification and write. The preflight proves every file matches when the run starts; a large tree leaves time after that. The converter's existing recheck compares length and timestamp against what that run itself saw on opening the file, so it cannot speak to a decision made earlier. A caller that committed to specific bytes can now supply the hash it approved, and the source is re-read and compared at the last point before installation. This narrows the window rather than closing it - a source rewritten between that check and File.Replace is still not caught, which would need every source held open against writers for the whole run - and the code and README both say so. Only -Apply pays for the extra read; an ordinary conversion has nothing to compare against and keeps the cheaper check. Also pins the reporting invariant: the displayed categories sum exactly to the selected population, with the two indented lines breaking down the one above them. The summary now describes the running build's guarantees rather than echoing the plan's own booleans, which an edited plan could otherwise use to claim something untrue about the conversion about to happen. 11 new tests, 383 passing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Phase D.2 — the reviewer's three additions before the plan mechanism is final
#46 bound the plan to file hashes. That answers "are these the same bytes?" but not "is this the same conversion?". Three ways a valid-looking plan could still mean something other than what was approved.
1. The conversion could change underneath it
The plan now carries a semantics version next to the schema version, and
-Applyrefuses a plan made under different conversion behaviour rather than carrying it out.That version is deliberately separate from the assembly version. Tying plan validity to a release number would invalidate every plan on a release that changed nothing about conversion — which teaches people to work around the check instead of reading it. It moves only when the meaning of a plan moves.
The plan also now records the target, BOM policy, backup policy, detection mode (
Detected/Explicit) and the guarantees the converting build provides, soplan.jsonis the whole approval:{ "PlanVersion": 2, "SemanticsVersion": 1, "ECVersion": "3.7.0.0", "Semantics": { "StrictDecoding": true, "StrictEncoding": true, "OutputVerification": true, "AtomicInstall": true, "AmbiguityRefusal": true }, "BaseDirectory": "...", "TargetEncoding": "utf-8", "TargetHasBom": false, "BackupEnabled": true, "ExplicitSourceEncoding": null, "DetectionMode": "Detected" }SemanticsVersionis what's enforced; theSemanticsblock is the record for the reader.2. The tree could change underneath it
Paths were absolute. A plan copied alongside its tree still named the original tree — and every hash matched, because those were the files it was made from. The relocation hazard was real and silent.
Entries are now
RelativePathagainst a recorded root:..\..\Windows\System32— is refused. A plan is an ordinary file people can edit and pass around.3. The file could change between verification and write
The preflight proves every file matches when the run starts; a large tree leaves time after that. The converter's existing recheck compares length and last-write-time against what that run itself saw on opening the file, so it cannot speak to a decision made earlier.
ConversionOptions.ExpectedSourceSha256lets a caller that committed to specific bytes insist they are still those bytes, checked at the last point before installation.-Applysets it from the plan.This narrows the window; it does not close it. A source rewritten between that check and
File.Replaceis still not caught — closing that needs every source held open against writers for the whole run, which would fail conversions of files legitimately open elsewhere. The XML doc and the README both say so rather than implying a guarantee that isn't there.Only
-Applypays for the extra read. An ordinary conversion has nothing to compare against and keeps the cheaper check; the hash is shared with the metadata record when both are wanted, so a backed-up-Applyreads once, not twice.Reporting invariant
Pinned as a test: the displayed categories sum exactly to the selected population, with the two indented lines breaking down the one above them and excluded from that sum.
The guarantees line now describes the running build, not the plan's own booleans — an edited plan could otherwise state something untrue about the conversion that is actually about to happen.
Tests
11 new, 383 passing. Plan schema is version 2; plans written by the previous build are refused with a message saying to re-run
-Plan, which is the intended behaviour for a schema that changed shape.🤖 Generated with Claude Code