PLAN: Differencing phase 4 — read-side policy¶
Phase 4 of PLAN-differencing.md.
Goal¶
Stop instar reading a differencing image as though it had no parent. Every operation that composes data from a differencing VHD or VHDX refuses it, by name, with an exit code that says so — replacing today's silent wrong answer on VHD and today's undiagnosed generic failure on VHDX.
This phase refuses. It does not compose: composition is phases 11 to 16, and phase 14 replaces each refusal added here with a real read. The refusal is an interim state inside this plan, settled by open question 1, and it is worth its own phase because it is the only part of the read-side answer that has to be true before the emitters in phases 5 and 6 ship.
Closes #547 and #548.
Planning effort¶
High, as the master plan requires for this phase. The judgement is not in any individual refusal — it is in choosing where the refusal lives, and the survey below moved that answer twice.
Review effort¶
High, concentrated on one question: is there a read path that still reaches sector composition on a differencing image? A refusal that covers seven callers out of eight is not a partial fix, it is the original defect with a smaller footprint. The management session should enumerate the entry points from the source rather than from this plan's list, because this plan's list is exactly the thing that would be wrong.
Scope¶
In scope:
- A typed refusal reason, carried from the guest to the host and rendered as a message that names the format and the reason.
- Refusal at every read entry point that can reach sector
composition on a differencing VHD (
disk_type == 4) or a differencing VHDX (HasParentset), forconvert,compare,bench,check,measureanddd. - Making the VHDX path diagnosable rather than merely non-zero, which is what #548 is about.
instar inforeporting a parent it can now see, which phase 3 taught the crates to read.- Integration tests over the phase 2 fixtures asserting the refusal per operation.
Out of scope, and deliberately:
- Composition. No op reads through to a parent in this phase.
mapandresize, which already refuse — see the survey.- The emitters. Nothing here writes a differencing image.
- Host-side chain discovery.
discover_backing_chain(src/vmm/src/main.rs:2416) is phase 11's business; this phase does not call it, extend it, or configure it. - Changing what
crates/vhdandcrates/vhdxparse. Phase 3 settled that surface and this phase consumes it unchanged, with the single exception recorded in decision 3.
What the survey found¶
The master plan's phase 4 material was written 2026-09-05, before phase 3 executed. Most of it holds. Four things do not, and two of them change the shape of the work.
Confirmed unchanged:
VhdState::initstill acceptsDISK_TYPE_DIFFERENCINGalongsideDISK_TYPE_DYNAMIC(src/crates/vhd/src/lib.rs:1346). The silent misread is live.VhdxState::initstill rejectshas_parentby returning bareNone(src/crates/vhdx/src/lib.rs:1647). Phase 3 added parent locator parsing without disturbing it.discover_backing_chainis atsrc/vmm/src/main.rs:2416, andbacking_path_allowlist/max_chain_depthatsrc/vmm/src/config.rs:65and:67, exactly as claimed.- Issues #547 and #548 are open.
Stale claim 1 — map is done, and is the template. The
master plan's success criteria list map among the ops phase 4
must fix. map has refused a differencing VHD since commit
eb6e23f (2026-06-03), well before this plan was written:
src/operations/map/src/main.rs:462 returns
MapResult::ERROR_HAS_BACKING, which the host renders at
src/vmm/src/main.rs:15053 as a sentence naming the reason and
pointing at the deferral. That is precisely the shape this phase
generalises, so map moves from work to precedent.
resize likewise already refuses, at
src/operations/resize/src/main.rs:629, with
ResizeResult::ERROR_UNSUPPORTED_SUBFORMAT.
Stale claim 2 — dd is not an operation. The master plan
lists dd as one of eight ops. There is no src/operations/dd.
run_dd (src/vmm/src/main.rs:13958) builds a convert execution
and calls execute_convert, so dd shares convert's guest binary
and inherits whatever convert does. It needs a test, not a fix.
Structural finding 3 — there is one read entry point, not
eight. This is the finding that reshaped the phase. convert,
compare, bench and check never call VhdState::init or
VhdxState::init; greping the operations for either name returns
nothing outside map and measure. They reach a VHD or VHDX
source through the generic chain-state initialiser in the qcow2
crate, which owns vhd_states and vhdx_states arrays
(src/crates/qcow2/src/lib.rs:9385 and :9387) and dispatches on
ImageFormat at :9478 and :9494 under the vhd-input and
vhdx-input features. Only three real VhdState::init call sites
exist in the tree: that initialiser, measure
(src/operations/measure/src/main.rs:388) and map (:438).
So the refusal has two homes — the shared initialiser and
measure — rather than six. A per-op refusal would have been five
copies of one check, and would have been the wrong answer for the
same reason it is wrong to fix a caller five times instead of
fixing the callee once.
Structural finding 4 — info is categorically different.
info does not link the vhd crate at all (src/operations/info/Cargo.toml)
and parses the footer itself via parse_vhd_footer
(src/operations/info/src/main.rs:434). It therefore cannot
inherit any refusal added above, and it should not: info reports
metadata, it never composes sector data, so it has no wrong answer
to give. Its defect is an omission — it reports no parent for an
image that has one — which phase 3 made fixable. See decision 4.
Corrected in review: the second sentence was read as licence to
keep parsing the VHD side locally, and that was wrong. Not linking
the crate meant not inheriting its validation either, which is how
info came to decode a parent name out of an unvalidated
data_offset (see "Found in review", item 2). info now links
crates/vhd for 704 bytes. The first sentence still stands: info
does not inherit the refusal, only the parsing.
Mechanism finding 5 — the per-op result struct is not a
usable channel, and the right precedent is issue #375. Both
init functions return Option<Self> and the chain initialiser
returns bool, so none of them can say why today; that is the
whole of #548. The obvious fix is a u32 in the op's result
struct, the way MapResult::ERROR_HAS_BACKING (:2924) is
rendered by map_error_message
(src/vmm/src/main.rs:15048). That does not generalise, and
this plan's first draft was wrong to assume it did. Three of the
five operations have nowhere to put such a code:
converthas no result struct at all. It reports throughsend_complete("convert", 0, false)and its module header states that no result message is needed. The host turns that into the bare string atsrc/vmm/src/main.rs:13241.CompareResult(src/shared/src/lib.rs:2349) carries a magic and flags, but no error constants.CheckResult(:2069) likewise has none.
Adding result structs to three operations to carry one boolean fact would be a protocol change out of all proportion to the phase.
The tree already solves this exact problem, for exactly this
reason, in issue #375: when the guest IDT catches a CPU fault, a
run loop that ends without a result must explain why instead of
printing "guest did not return a result". The mechanism is a
single capture in the message decoder — last_cpu_exception at
src/vmm/src/main.rs:770, set at :833 when a Payload::Error
arrives whose operation field marks it — and a single formatter,
no_result_error at :789, that prefers the captured reason and
falls back to the generic text. send_error(op, device, sector,
status) is already on the call table
(src/core/src/main.rs:430), so every guest binary can raise it
today with no protocol change.
Phase 4 adds a sibling to that pair. This is op-agnostic, needs one host-side capture point rather than five, and works for the three operations that have no result struct.
Naming hazard 6. src/vmm/src/main.rs:18659 and the
MapRenderer doc comment above it refer to "Phase 4". That is
PLAN-map.md's phase 4, not this one. An agent grepping for
phase 4 in the vmm will find map's streaming-renderer work and
should ignore it.
Corrections made at source. As part of the planning commit,
the master plan's phase 4 wording is corrected for map and dd,
and the phase 3 row's empty Merged column is filled in
(42e879f, #558). One phase 3 Definition-of-done item was also
false as merged — it asserted git diff --name-only
develop...HEAD -- src/operations/ is empty, but the review round
that plumbed metadata_length into parse_metadata changed
src/operations/check/src/main.rs, a call-site-only edit with no
behaviour change. That bullet is annotated rather than deleted, so
the record shows what happened.
Found during implementation, and left alone¶
Three things surfaced while implementing that are recorded rather than fixed, so a later reader sees them as decisions:
-
resizeaccepts a differencing VHDX.src/crates/resize/src/vhdx.rs:55guards onopts.has_parent, but the caller hard-codeshas_parent: false(src/operations/resize/src/main.rs:839), so the guard has never fired -- dead since94d73b7in May 2026.instar resizeon a differencing VHDX therefore succeeds. This is a write path and predates the plan;resizenever calledVhdxState::init, so phase 4 neither caused it nor fixes it. It also corrects this plan's survey, which recorded "resizealready refuses": that is true for VHD only. It is documented as a known limitation indocs/resize.mdanddocs/quirks.md, and filed by the operator as issue #565 after the phase landed for review. Reproduced there against thevhdx-diff-childfixture: a 16 MiB child grows to 100 MiB while its parent stays 16 MiB, exit 0. Differencing VHD is refused (error 6: subformat does not support resize) and qemu-img 10.0.13 refuses differencing VHDX outright, so instar-on-VHDX is the only accepting combination. -
infoprints an unresolvable "actual path" for a VHDX parent. The VHDX locator path is Windows-shaped (.\vhdx-diff-parent.vhdx), and the host renders it as a POSIX path atsrc/vmm/src/main.rs:1499, producing a filename containing a backslash that cannot exist. The output is honest about where instar would look, and qemu-img prints the same field unconditionally, but the underlying issue is path normalisation, which belongs to phase 11. Suppressing the display alone would paper over it. Phase 4e documents it as a known limitation. -
map's VHDX arm had to change after all. The step plan said to leavemapalone on the strength of its VHD refusal atsrc/operations/map/src/main.rs:462. That was wrong: map's VHDX safety came entirely from theVhdxState::initrejection that decision 3 removes, as its own comment recorded. Left untouched,mapwould have begun emitting a differencing VHDX's parent blocks as holes. The refusal was added using map's existingERROR_HAS_BACKING, so the precedent is preserved rather than migrated.
Found in review¶
The automated reviewer on PR #563 raised twelve items. Two were regressions this phase introduced, and are the reason this section exists rather than a changelog line.
create -b <differencing VHDX>started succeeding. RemovingVhdxState::init's blankethas_parentrejection -- the change that lets the read entry points refuse with a reason instead of failing anonymously -- also removed the only guard oncreate'sread_backing_virtual_size, which reads a user-supplied image. An overlay whose base every read path refuses is a chain that can never be read back. Decision 3 anticipated this class of fallout and the definition of done enumeratedinit's call sites, but classifiedcreateas "writes rather than reads" and stopped there, which was wrong about this call site.
Fixed by refusing both formats in read_backing_virtual_size,
with a new CreateResult::ERROR_BACKING_DIFFERENCING rather than
the generic ERROR_BACKING_PARSE_FAILED: the backing header parses
perfectly well, and telling a user a valid image is "truncated,
corrupted, or an unrecognised format" is precisely the undiagnosed
failure #548 was filed over. The VHD arm never had a guard at all
and gains one here, so the two formats now agree.
infodecoded a parent name without validating the dynamic header.parse_vhd_parent_nametookdata_offsetfrom the footer and decoded 512 bytes at header offset 64 as UTF-16BE. Bothdisk_typeanddata_offsetare image-controlled, so an image could pointdata_offsetanywhere in itself and have arbitrary content printed asbacking file:-- untrusted bytes promoted into a structured, user-facing field, with none of the validationcrates/vhdapplies to the same structure.
Structural finding 4 and step 4c's brief are the root cause: they
left info parsing the VHD side locally rather than linking
crates/vhd, on the grounds that the VHD side was "small enough
to parse locally". That reasoning was already weak once the same
step added a vhdx dependency, and it produced six re-declared
constants with no mechanism to catch drift -- info is a
no_main guest binary that cannot run cargo test, so nothing
could have failed if the crate's values moved. info now links crates/vhd, uses its
constants, and gates on VhdDynamicHeader::parse -- the crate's
own cxsparse cookie check. Measured cost: info.bin went from
148,512 to 149,216 bytes -- 704 bytes, against a 768 KB ceiling
the binary uses 18% of. The size argument decision 4 rested on did
not survive being measured.
Verified by negative control: with the cookie check compiled out,
an image carrying UTF-16BE text at a bogus data_offset reports
backing file: PWNED-SECRET.vhd; with it in, nothing. The test
builds that image rather than shipping it as a fixture.
Two further defects were found while writing the tests for those, and neither was in the review:
-
backing-filename-formatclaimed a differencing VHD's parent was a qcow2. The field defaults to"qcow2"when no backing format is recorded, which is right for a qcow2 v2 image with no header extension. A differencing image records no such extension either, so reporting a parent at all -- new in this phase -- put a false claim in a machine-read field. SPEC(VHD) and SPEC(VHDX) both require a parent to be the same format as its child, so the format is known without a header extension;vpc/vhdxare now reported. -
A malformed differencing VHD fails generically, and that is correct. The refusal in
init_chain_statesreads aVhdState, andVhdState::initcannot build one without a validcxsparseheader -- so an image with a bogusdata_offsetfails as malformed before its disk type is consulted. This was an assumption in a test I wrote, not in the code, and it is recorded here because the test now asserts the real behaviour and says why. The property #547 was filed over still holds: no content is written and the exit code is non-zero.
The remaining ten items were documentation and test-coverage gaps:
plan phase numbers in the user-facing refusal string and in six
documents (AGENTS.md keeps phase numbers inside docs/plans/, and the
string reaches a user of an installed .deb who has neither the plan nor
its numbering); refuse_differencing inserted between detect_and_scan's
doc comment and detect_and_scan, orphaning an unsafe fn's safety
contract; check_vhd computing corruption findings past a refusal that
suppresses them; the six adversarial parent-locator fixtures and
vhd-diff-child-mixed untested; per-operation docs unwritten and
docs/check.md still claiming differencing VHDs are validated;
check --output json emitting no JSON on a refusal; subTest missing
from every fixture loop; and a quirks.md heading still describing the
pre-fix behaviour the body now contradicts.
Decisions¶
-
Refuse at the shared chain initialiser and at
measure, not inVhdState::init. Making the crate'sinitrejectdisk_type == 4would close the hole in one line for every caller at once, and it is the obvious move. It is wrong here for three reasons: phase 3's parsing surface exists to be read from a successfully initialised differencing image, andmapalready depends oninitsucceeding so it can readstate.disk_type; phases 11 to 16 needinitto succeed in order to compose; and a crate-levelNoneproduces exactly the undiagnosed failure that #548 exists to complain about. The callee is not wrong — the callers are missing a policy check. -
One guest-side refusal signal, captured once on the host, following issue #375 — not a per-op result code. The guest raises
send_errorwith a reserved operation marker and a status naming the format; the host decoder captures it besidelast_cpu_exception, and the failure paths render it in place of their generic text. Survey finding 5 records why the per-op-result-code shape, which this plan proposed in its first draft, cannot work:converthas no result struct andCompareResultandCheckResulthave no error constants, so three of the five operations have nowhere to put a code. The message still names the operation, because the formatter takes the op name as an argument exactly asno_result_errordoes.mapkeeps its existingERROR_HAS_BACKINGand is not migrated: it works, it is already covered by a test, and changing it would put a working refusal at risk for tidiness. -
Make VHDX symmetric with VHD:
VhdxState::initstops rejectinghas_parent, and the entry points refuse instead. This is the decision most likely to be argued with, because it deliberately removes a working safety net. Today a differencing VHDX fails closed with a useless message; the alternative of keeping the rejection and threading a reason out ofinit(changingOption<Self>toResult<Self, Reason>across twelve call sites) preserves fail-closed but leaves VHD and VHDX structurally different for phases 11 to 16 to reconcile later. Symmetry is worth more: after this change both formats initialise, both expose a parent flag (has_parentis alreadypubon the metadata atsrc/crates/vhdx/src/lib.rs:935), and one uniform check at each entry point covers both.
The hazard is real and the mitigation is structural: the commit that removes the rejection must be the same commit that adds every entry-point refusal. Split across two commits, the tree passes through a state where a differencing VHDX is silently misread — converting a safe failure into the exact defect this phase exists to close. Step 4b is therefore indivisible, and the review checks that first.
-
inforeports; it does not refuse.infocomposes nothing, so it has no wrong answer to give, and refusing would remove the only way to inspect an image the rest of the tool declines to read — which is precisely when a user needsinfomost. It gains a parent line instead. Note this changesinfooutput for differencing images, so phase 3's parity script (tools/verify-info-output-parity.sh) will report differences on exactly the differencing fixtures and must be run with that expectation stated, not as a pass/fail gate. -
ddgets a test, not a fix. It shares convert's guest binary. The test exists to catch a future divergence, since nothing in the tree records that dd and convert must stay linked. -
The refusal message points at the deferral. Following map's text, each message says composition is deferred and names the plan, so a user who hits it learns it is a known boundary rather than a corrupt image.
Step plan¶
| Step | Effort | Model | Isolation | Brief for sub-agent |
|---|---|---|---|---|
| 4a | medium | sonnet | none | Build the refusal channel, following issue #375's last_cpu_exception pair exactly. (i) In src/shared/src/lib.rs add a small module of stable constants: a reserved send_error operation marker (a short string such as differencing) and one u32 status per format (VHD, VHDX), documented as append-only the way BenchResult's error codes are at :4232. src/shared is no_std. (ii) In src/vmm/src/main.rs add a last_differencing_refusal: Option<u32> field beside last_cpu_exception (:770, initialised :781), capture it in add_byte beside the existing capture (:829-835) when the Payload::Error's operation equals the marker, and add a formatter beside no_result_error (:789) that takes the op name and renders a sentence naming the operation, saying the source is a differencing image whose parent instar cannot yet compose, and saying composition is deferred to PLAN-differencing phases 11-16. Follow the wording of map_error_message (:15053). (iii) Unit-test the formatter both ways, mirroring the two tests at :863 and :872. Nothing raises the error yet — this step adds no behaviour. |
| 4b | high | opus | worktree | Indivisible — one commit. (i) Remove the has_parent rejection at src/crates/vhdx/src/lib.rs:1647 and expose the flag on VhdxState so callers can test it (has_parent is already pub on the metadata at :935). (ii) In the generic chain-state initialiser src/crates/qcow2/src/lib.rs:9450-9520, refuse a differencing source in both the ImageFormat::Vhd arm (:9478, test state.disk_type == vhd::DISK_TYPE_DIFFERENCING) and the ImageFormat::Vhdx arm (:9494, test the new flag), raising the refusal from inside the function rather than threading it out: init_chain_states already takes call_table: &CallTable, and send_error is a field on it (src/shared/src/lib.rs:914), so the arm can raise 4a's signal itself and return false unchanged. This needs no signature change and no caller change — the five real callers (convert:300 and :1020, compare:211, bench:1395, rebase:1201) stay as they are, and the crate already calls the table elsewhere (call_table.debug_print). Note send_error takes NUL-terminated *const u8, while 4a's OPERATION is a &'static str for the host's comparison, so add a NUL-terminated companion constant beside it in src/shared/src/lib.rs with a test asserting the two agree, rather than writing a bare literal at the call site. (iii) Do the same at measure's two direct call sites (src/operations/measure/src/main.rs:388 and :400). (iv) At each refusal, call the call table's send_error with 4a's marker and the format's status before returning failure, so the host renders the specific message; map's refusal at src/operations/map/src/main.rs:459-470 shows the return shape, but use the 4a channel rather than a result code — convert, compare and check have nowhere to put one. (v) Wire the host's generic failure sites to the 4a formatter, at minimum src/vmm/src/main.rs:13241 (convert) and :12213 (compare). Constraints: both crates are no_std, panic-free, no allocator; the arms are behind the vhd-input and vhdx-input features, so check both feature combinations build. Do not touch VhdState::init. |
| 4c | medium | sonnet | none | Teach info to report the parent. info parses the footer itself (parse_vhd_footer, src/operations/info/src/main.rs:434) and does not link the vhd crate — decide with the management session whether to add the dependency or extend the local parser, and state the choice. Report the parent name for a differencing VHD and the parent locator's linkage for a differencing VHDX, in both human and --output json forms, following how qcow2's backing file is already reported. Refuse nothing. |
| 4d | high | opus | worktree | Integration tests over the phase 2 fixtures. For each of convert, compare, bench, check, measure and dd, assert a non-zero exit and the expected message on vhd-differencing, vhd-diff-child-aligned and vhdx-diff-child; assert map still refuses (regression guard on the precedent) and that info now reports a parent. Assert dd and convert produce the same refusal, which is the only thing recording that they share a binary. Use the existing integration harness rather than a new one. The composed goldens (vhd-diff-aligned-composed.raw, vhdx-diff-composed.raw) are phase 11-16 material — do not use them here. |
| 4e | medium | sonnet | none | Documentation and closeout. Update docs/format-coverage.md (divergence notes), docs/quirks.md and CHANGELOG.md to state that differencing VHD and VHDX are refused on read with composition deferred. Close #547 and #548 with a comment naming the commit and the message a user now sees. Do not touch docs/create.md or the emitter docs — phases 5, 6 and 10 own those. |
Steps 4a and 4c are independent of each other. 4b depends on 4a. 4d depends on 4b and 4c. 4e last.
Risks and mitigations¶
- A read path is missed, and one op still composes silently.
The likeliest failure of this phase, and the reason the entry
points were enumerated from the source rather than from the
master plan's op list. Mitigation: the management session
re-derives the list of
VhdState::init/VhdxState::initcallers andImageFormat::Vhd|Vhdxdispatch sites from the tree at review time and compares it against what 4b changed; 4d covers every op by name including dd. - The 4b window. Removing the VHDX rejection before the refusals land makes the tree briefly worse than it is today. Mitigation: 4b is one commit, stated in the brief and checked first at review.
- Feature-gate blindness. The chain initialiser's arms are
behind
vhd-inputandvhdx-input. A refusal added inside a gate that some op does not enable protects nothing. Mitigation: 4b's brief requires building both feature combinations; the review greps which ops enable which features. infoparity churn. Decision 4 deliberately changesinfooutput for differencing images, which phase 3's parity script will report. Mitigation: stated in decision 4 and in the DoD; the run is read for which images changed, and any change outside the differencing fixtures is a defect.- Guest binary size. Every guest binary has a 768KB cap and
make check-binary-sizesenforces it. The additions are small, butcheckandconvertare the largest binaries. Mitigation: in the DoD.
Definition of done¶
Checked by step 4e (documentation and closeout) against the commits
listed at the top of this plan's "Found during implementation" section
and against the tree at HEAD. Items are ticked where 4e could verify
them directly (by reading the code/tests or diffing commits); where 4e
relied on an earlier step's own record without re-running it, that is
said so explicitly rather than presented as independently checked.
-
instar convert -O raw,instar dd,instar compare,instar bench,instar checkandinstar measureeach exit non-zero onvhd-differencing,vhd-diff-child-alignedandvhdx-diff-child, with a message naming the operation and the parent reference. Verified by the 4d tests (tests/test_differencing.py), not by hand. The message names the operation and the format (VHD/VHDX) and says "whose parent instar cannot yet compose"; it does not quote the parent's literal path (that would require resolving it, which phase 4 deliberately does not do — see decision 3's VHDX path-resolution note). - No operation writes output composed from a differencing source.
Specifically,
instar convert -O rawonvhd-differencingproduces no output file, where before commit10ab838it produced a wrong one and exited 0. Verified byTestDifferencingConvertLeavesNoOutput. - No operation gained a result struct or a new protocol message:
the refusal travels on the existing
send_errorchannel, andgit diff 42e879f..HEADtouches neithercrates/guest-protocolnor any*Resultstruct definition insrc/shared/src/lib.rsbeyond the newDifferencingRefusalconstants module. Verified by diffing the range directly. -
grep -rn 'VhdState::init\|VhdxState::init' --include=*.rs src/and theImageFormat::Vhd/ImageFormat::Vhdxdispatch arms insrc/crates/qcow2/src/lib.rstogether enumerate every read entry point, and each one either refuses a differencing source or iscreate(which writes) or a fuzz target. Checked by reading the code in step 4e: the real (non-comment, non-fuzz) call sites aremap(both arms),measure(both arms),init_chain_statesincrates/qcow2(both arms, servingconvert/compare/bench/rebase), andcreate'sVhdxStateuse for output construction — every one of the first three refuses a differencing source, andcreatewrites rather than reads. -
src/crates/vhdx/src/lib.rsno longer rejectshas_parentininit, and the commit that removed it is the same commit that added every entry-point refusal — verified withgit show --stat 10ab838, which touchescrates/vhdx,crates/qcow2,operations/check,operations/map,operations/measure,sharedandvmmtogether. -
instar inforeports a parent forvhd-diff-child-alignedandvhdx-diff-childin both human and JSON output, andtools/verify-info-output-parity.shreports differences on the differencing fixtures and on no others.
This bullet's own wording is imprecise, corrected here rather
than silently fixed. vhd-differencing — the example this bullet
originally named — is a dynamic VHD patched to disk type 4 with an
all-zero parent name, so info correctly reports no backing
file for it (test_info_reports_the_disk_type_4_fixture_without_a_parent
pins exactly this). The fixtures that actually have a resolvable
parent for info to report are vhd-diff-child-aligned (parent
vhd-diff-parent.vhd) and vhdx-diff-child (parent
.\vhdx-diff-parent.vhdx), which is what the corrected wording
above says. The parity-script claim itself is independently
confirmed (from a tools/verify-info-output-parity.sh run against
base 42e879f and this phase's tree, captured during step 4d and
read by step 4e rather than re-run): Compared: 208, Failed: 9,
with the nine named exactly as the differencing children and the
locator-audit fixtures (vhd-diff-child-aligned,
vhd-diff-child-mixed, vhdx-diff-child,
vhd-diff-locator-etc-passwd, vhd-diff-locator-dotdot,
vhd-diff-locator-unc, vhd-diff-locator-url,
vhd-diff-locator-overlong, vhd-diff-locator-conflicting) and no
others, matching commit c561aad's own record exactly.
- [x] instar map still refuses vhd-differencing with its existing
message, unchanged. Verified by reading
src/operations/map/src/main.rs:459-470 (unchanged VHD arm) and by
TestDifferencingMapStillRefuses.
- [ ] Issues #547 and #548 are closed, each with a comment quoting the
message a user now sees. Deliberately not done by step 4e. Per
this step's brief, closing issues and posting GitHub comments is
left to the operator: the closing comments are drafted to the
session scratchpad (not committed to the repository) for the
operator to review and post by hand.
- [x] VhdState::init is byte-for-byte unchanged. Verified with
git diff 42e879f..HEAD -- src/crates/vhd/src/lib.rs, which is
empty.
- [x] make instar builds, make check-binary-sizes passes, and
make test-rust passes. Confirmed from this session's own
build/test run logs from step 4d (not re-run by 4e, which is
docs-only): the release build succeeded (all binaries listed,
including check.bin, map.bin and measure.bin),
check-binary-sizes passed as part of the same pre-commit run
noted below, and make test-rust reported 0 failures across every
crate's unit-test binary.
- [x] pre-commit run --all-files passes. Confirmed twice: once
from step 4d's own run log (every hook passed), and again by step
4e itself against this documentation change (see the commit this
plan update lands in).
- [x] make test-integration passes. Run by the management session
against the tree with every step of this phase committed:
Ran: 3508 tests ... Passed: 2637, Skipped: 871, Failed: 0, exit 0.
An earlier run during this phase did record one failure, in
test_commit.py, where a qemu-img commit subprocess timed out at
60 seconds -- the signature of already-tracked issue #528, a
load-sensitive flake unrelated to differencing. It did not
reproduce in the clean run above, which is the evidence for calling
it load and not a regression.
Back brief¶
Before executing any step of this plan, please back brief the operator as to your understanding of the plan and how the work you intend to do aligns with that plan.
In particular, back brief before starting step 4b: it removes
a working safety net and must land as a single commit, and the
choice of how to thread the refusal reason out of the chain
initialiser's bool return should be agreed before the editing
starts rather than discovered in review.