Skip to content

Give up exactly what was asked, and put the Dev Drive where it was freed - #97

Merged
ALERTua merged 2 commits into
mainfrom
bugfix/83
Aug 26, 2026
Merged

Give up exactly what was asked, and put the Dev Drive where it was freed#97
ALERTua merged 2 commits into
mainfrom
bugfix/83

Conversation

@ALERTua

@ALERTua ALERTua commented Aug 26, 2026

Copy link
Copy Markdown
Owner

Closes #83.

Note. Two independent reviews ran on this change. Both are reported below.

What was wrong

Shrink mode computed the resize target from Get-PartitionSupportedSize's SizeMax, then created the partition with -UseMaximumSize. Both are wrong, and #83 as filed described only the mildest of the consequences.

Two behaviours, neither documented, both measured on scratch disks:

SizeMax is the partition's size plus the contiguous unallocated run behind it. Microsoft documents only "the minimum and maximum size of the partition". Measured twice in one layout: a 20 GB partition with a 10 GB gap behind it reported SizeMax 30 GB; a 5 GB partition with a 24.98 GB tail behind it reported 29.98 GB. A later run confirmed the contiguous part: a 40 GB partition with a 4 GB gap behind it, and a 70 GB tail beyond a further partition, reported 44 GB.

-UseMaximumSize creates the largest possible partition on the disk, not one in the space just freed. Measured on a disk laid out A(15 GB) | gap 15 GB | C(5 GB) | tail 25 GB: the partition landed in the tail at 24.98 GB and the freed 15 GB was left untouched.

Together those gave three defects:

Old behaviour
The drive could be created in a different free region entirely, leaving the freed space unused
The volume gave up less than the plan named — SizeMax − asked instead of size − asked
Worse where more space adjoined the volume than the user asked to free, the resize grew it

What it does now

  • The volume gives up exactly the amount asked for.
  • The Dev Drive fills the whole contiguous free run immediately behind it — the amount freed plus whatever already adjoined. Unallocated space elsewhere is never touched.
  • While the amount is being chosen, one line names the adjoining space when there is any:
  Maximum shrinkable: 700 GB
  Unallocated right behind D: 50 GB - it joins the new Dev Drive

Enter Shrink amount in GB (min: 50, max: 700)
  • The plan says the resulting size and why it exceeds what is freed:
* Shrink Drive D (ALERT) by 200 GB to free up space
* Create 250 GB Dev Drive on Disk 1 (CT4000P3PSSD8) using ReFS
  50 GB of unallocated space already sits next to drive D and will be taken
  as well, so the Dev Drive comes out 250 GB rather than the 200 GB being freed.
  • -UseMaximumSize survives only in the .vhdx branch, where the disk was created moments earlier and holds exactly one free region. A syntax-tree assertion enforces that: any New-Partition without an explicit -Size must name $vhdxDiskNumber.

The arithmetic moved into Resolve-ShrinkPlan, a pure function, so the numbers are testable without a disk. It guards before subtracting and answers zero sizes on every rejection, so no caller can act on a number it never got.

Confirmed on real hardware

A USB disk laid out shrinkme 40 GB | 4 GB free | 5 GB partition | 70.23 GB free, freeing 6 GB:

result:  shrinkme 34 GB | new 10 GB | 5 GB partition | 70.23 GB free

PASS  the volume gave up exactly the 6 GB asked for
PASS  the new partition sits immediately behind the shrunk volume
PASS  the new partition is the size that was predicted
PASS  the drive absorbed the space that was already unallocated
PASS  the far unallocated region was left alone
PASS  the far region is the bigger one, so taking the maximum would have shown

The far region being seven times the resulting drive is the point: the old code would have put a 70 GB partition there. The disk was restored to a single empty NTFS volume afterwards. dev_drive.ps1 was never executed — the probe lifts its functions out of the syntax tree, which is how this repository exercises them.

Checks

Parse OK; Invoke-ScriptAnalyzer -Path . -Recurse no findings; 695 tests pass (665 before). The pre-commit hook ran the same three.

Both new syntax-tree assertions were proved to have teeth by reintroducing their defects on scratch copies:

placement:  real script -> PASSES    broken copy -> FAILS: a call outside the vhdx branch asks for the maximum

Review

An independent review raised 18 points and all were applied. The three that mattered most:

  • A rejection name was being printed at the user, attributed to Windows. "Windows reports MaxBelowCurrent" — a token this script invented, which Windows never said and which names nothing a person can act on. There is now one wording for both call sites, through Format-ShrinkRefusal, and a test forbids the token appearing in user-facing text.
  • The subtraction deciding what gets written to disk was unguarded, and it runs after the volume has already been shrunk. A negative difference would have reached New-Partition as a parameter-binding error rather than as an explanation. It now has a guard that says what happened, and a test pins that the guard precedes the creation.
  • Nothing pinned the argument wiring. The decision function could have been tested to perfection and handed the wrong arguments — a literal minimum, or the current size passed as the maximum. Four assertions now cover the wiring, that the offset and size come from the read-back rather than the prediction, and that the warning is printed before the confirmation question.

Also applied: a comment that claimed [uint64] subtraction wraps around (measured: it yields a negative Double); a rejection token colliding with an unrelated one in Resolve-DevDriveSizeInput; [decimal]$null silently turning "unknown" into zero; inconsistent unit naming across the returned fields; ([uint64][math]::Round($GB * 1GB)) written three times, now ConvertTo-ByteCount; and two test-block boundaries that my own edits had quietly loosened.

Nothing from that review was declined.

Separately, and after it: a first attempt also lowered the minimum shrink the question accepts by whatever adjoins the volume, added a viability check counting that space toward the 50 GB Dev Drive minimum, and skipped the resize when nothing had to be given up. That was removed at the owner's request as more flow than the problem warranted. The minimum stays a flat 50 GB, so the drive is always at least that whatever adjoins it, and the note at the input point is the whole of what the user gains.

🤖 Generated with Claude Code


Second review, and the measurement it asked for

A second independent review read the finished change. It raised 13 points; 12 were applied, and one of its open questions turned out to be a defect that would have shipped.

It asked whether an explicit -Offset survives a shrink amount that is not a round number of gigabytes. The end-to-end run above used 6 GB, which happens to land on a megabyte boundary. Measured on a scratch virtual disk with five awkward amounts:

6.37 GB    remainder 125952 against 1 MB   ->  refused: The specified offset is not valid
3.14159    remainder 12800                 ->  refused
0.77       remainder 545280                ->  refused
1.005      remainder 923136                ->  refused
2.0009     remainder 82432                 ->  refused

New-Partition accepts only an offset that is a whole number of megabytes, and Resize-Partition aligns to the cluster instead - measured 20 to 389 bytes off its target every time. So the unaligned offset is the ordinary case, not a rare one, and the run would have died after the volume was already shrunk, the moment a user typed 50.5 instead of 50.

Resolve-AlignedPlacement now nudges the start forward to the next megabyte and takes the same amount off the size. Re-running the same probe against the shipped function:

rounds where the offset was NOT 1 MB aligned:      5 of 5
rounds where New-Partition did not simply accept:  0 of 5

The other finding worth naming: the note about the space behind the drive was printed before the 50 GB viability check, so a volume that was about to be refused was first told that space would join its Dev Drive, and the refusal never mentioned it. The note now comes after the refusal and before the question.

Also applied: a default arm in the refusal wording that would have let a later rejection inherit another one's message; two size parameters defaulting to a number nobody measured; the plan note's three figures not adding up, now derived from the two that are shown; "at least this much" where alignment can leave the drive marginally smaller, now "about this much, and likely more"; ConvertTo-ByteCount missed in free-space mode; and a guard so a drive that comes out below the 50 GB minimum stops the run instead of being reported and created anyway.

The finding that mattered most for the suite: reverting a single line left every one of the 695 tests green while the plan understated the drive and the note read "comes out 200 GB rather than the 200 GB being freed". The wiring the change turns on is now pinned.

Declined, one: separately checking that the size question rejects numbers with many decimal places. The note now computes the extra space as the difference between the two figures it prints, so the arithmetic on screen holds however many decimals arrive.

718 tests pass.

ALERTua and others added 2 commits August 26, 2026 14:31
…reed

Shrink mode computed the resize target from Get-PartitionSupportedSize's
SizeMax and then created the partition with -UseMaximumSize. Both were wrong,
and measurement showed why.

SizeMax is the partition's size plus the contiguous unallocated run behind it,
not the partition's size. Measured: a 40 GB partition with a 4 GB gap behind
it reported SizeMax 44 GB, while a 70 GB tail beyond a further partition was
not counted. So a target of "SizeMax minus the amount asked for" made the
volume give up less than the plan named - and, where more space adjoined the
volume than the user asked to free, grew it instead of shrinking it.

-UseMaximumSize creates the largest possible partition on the disk, not one in
the space just freed. Measured on a disk laid out A(15) | gap 15 | C(5) |
tail 25: the partition landed in the tail and the freed 15 GB was left unused.

- compute the target from the volume's current size, so it gives up exactly
  the amount asked for
- place the new partition explicitly, at the offset the shrunk volume actually
  ends at and sized to the free run behind it, so it can only land there
- keep -UseMaximumSize in the .vhdx branch alone, where the disk was created
  moments earlier and holds one free region
- name the adjoining space while the amount is being chosen, and say in the
  plan how large the drive will come out and why that exceeds what is freed
- refuse with wording a person can act on, never with a rejection name
- guard the subtraction that decides what is written, because by then the
  volume has already been shrunk

Confirmed end to end on a USB disk laid out shrinkme(40) | 4 free | 5 | 70.23
free: freeing 6 GB left the volume at 34 GB, produced a 10 GB partition
immediately behind it, and left the 70.23 GB region untouched.

Closes #83

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…re refusing it

Two findings from a second review and one measurement it asked for.

New-Partition refuses an offset that is not a whole number of megabytes, and
Resize-Partition aligns to the cluster rather than to that boundary - measured
20 to 389 bytes off its target across five runs. So the offset handed to
New-Partition is usually unaligned, and all five awkward shrink amounts
(6.37, 3.14159, 0.77, 1.005 and 2.0009 GB) were refused with "The specified
offset is not valid" - after the volume had already been shrunk. The earlier
end-to-end confirmation passed only because 6 GB happened to be aligned.
Resolve-AlignedPlacement nudges the start to the next megabyte and gives back
from the size exactly what the nudge took; the same probe then accepted all
five.

The note naming the space behind the drive was printed before the 50 GB
viability check, so a volume about to be refused was first told that space
would join its Dev Drive. It now comes after the refusal and before the
question.

- guard the drive against coming out below the Dev Drive minimum, and say when
  the space behind the volume did not come to what the plan named
- give Format-ShrinkRefusal a real default arm, so a rejection added later
  cannot inherit another one's wording, and make its sizes mandatory rather
  than defaulting to a number nobody measured
- derive the extra space in the plan note from the two figures shown, so the
  three always add up on screen
- say "about this much, and likely more" where the adjoining space could not
  be measured, since alignment can leave the drive marginally smaller
- use ConvertTo-ByteCount in free-space mode too, which the first pass missed
- pin the wiring the change turns on: reverting one line had left every test
  green while the plan understated the drive

Closes #83

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ALERTua
ALERTua merged commit 8e7d7e0 into main Aug 26, 2026
3 checks passed
@ALERTua
ALERTua deleted the bugfix/83 branch August 26, 2026 12:17
ALERTua added a commit that referenced this pull request Aug 26, 2026
Record the two shrink facts measured in #97
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Shrink mode absorbs pre-existing unallocated space without saying so

1 participant