Reference the Freeze Doctor contract as a package instead of copying it (BL-16719) - #8219
Reference the Freeze Doctor contract as a package instead of copying it (BL-16719)#8219JohnThomson wants to merge 3 commits into
Conversation
…it (BL-16719) Stage 1 of the migration, on its own branch so PR #8218 stays reviewable and green. Nothing is published: the package is restored from a local folder that build/pack-contract-locally.ps1 fills from a clone of the Doctor repo, so this branch can be built and reviewed before anything irreversible happens. What goes away: - The three copied contract files, 750 lines of them. They were hand-maintained duplicates of files in BloomBooks/bloom-freeze-doctor describing a wire format the two programs must agree about exactly, and they had already drifted. - build/check-freeze-doctor-contract.sh and its workflow. That check existed only to catch drift between the copies; with one definition there is nothing to drift. - The RobustFile pre-commit exemption. It was needed because the copies could not call RobustFile and still match the originals. The files have left this repo, so the shared quality gate is back to full strength with nothing excused from it. That was the exemption John had to approve; it is now moot. What arrives: one PackageReference with no transitive dependencies of its own, two usings, and temporary local-feed scaffolding (root NuGet.Config, the pack script, localpackages/) that all disappears when the package is published. The layout-pinning test stays, with its purpose rewritten, because that purpose changed. It no longer guards against two copies diverging - it pins the layout Bloom expects against the layout the referenced version actually has, so a package upgrade that moved an offset fails the build instead of silently making Bloom publish its health where the Doctor no longer looks. Full suite: 3153 passed, 13 skipped, 0 failed - the same as with the copies.
…719) The three files Bloom and the Freeze Doctor share are not a "contract" in any sense a reader would recognize from the word; they are the wire format of the shared-memory channel between the two processes - a protocol. So the package, its namespace, its folder in the Doctor's repo, and everything here that names it, all say Protocol now. On this side that means: - BloomExe.csproj references BloomBooks.FreezeDoctor.Protocol. - FreezeDoctorSupport.cs imports that namespace. - The local restore folder, NuGet.Config source key, and .gitignore comment name the new package. - build/pack-contract-locally.ps1 becomes build/pack-freeze-doctor-protocol.ps1 and packs the renamed project. - DoctorChannelContractTests.cs becomes FreezeDoctorProtocolTests.cs. Its purpose already changed when the protocol became a package - it no longer guards two hand-maintained copies against drift, because there is only one definition now; it pins the layout Bloom expects against the layout the referenced package version actually has, so a version bump that moved an offset fails the build instead of publishing plausible nonsense. No behavior changes: the assembly is byte-for-byte the same code under a different name. Full C# suite green (3153 passed, 13 skipped). The matching rename in the Doctor's repo is BloomBooks/bloom-freeze-doctor commit 8f569d9. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Your trial has ended. Reactivate Greptile to resume code reviews.
| One package means one definition of the wire format instead of two. | ||
| Source: https://github.com/BloomBooks/bloom-freeze-doctor | ||
| --> | ||
| <PackageReference Include="BloomBooks.FreezeDoctor.Protocol" Version="0.1.0-alpha" /> |
There was a problem hiding this comment.
[Devin — Investigate, current commit] A fresh checkout cannot build until someone runs the pack script.
This PackageReference can only be restored from the localpackages folder that the new root NuGet.Config adds, and that folder is empty on a fresh clone — only .gitkeep is tracked. Nothing is published to nuget.org yet, so restore fails on any machine that has not run build/pack-freeze-doctor-protocol.ps1 against a clone of the Doctor repo. That includes CI and TeamCity.
Devin's own read is that this is intentional and documented rather than a bug, and adds that a reviewer should confirm the package is published and the temporary scaffolding removed before this actually merges. Agreed on both counts — which is why this thread is being left open.
Leaving open deliberately: this is a merge precondition, not a settled point. Before #8219 merges, three things have to go: the root NuGet.Config, build/pack-freeze-doctor-protocol.ps1, and localpackages/ — replaced by the package being restorable from nuget.org. Resolving this thread now would delete the only durable record of that.
Worth noting for anyone reading the checks: GitHub Actions does not build Bloom on PRs, so this will not appear as a red check here. It would surface in TeamCity.
The shared page now records PayloadBytes - how far the writer actually wrote - so that a Doctor newer than the Bloom it is reading can tell a field this Bloom never wrote from a real zero. See BloomBooks/bloom-freeze-doctor commit aaec915 for the reasoning; Bloom's side of it is just the reference bump, because Bloom keeps writing the same fields either way. The version had to move even though nothing is published: NuGet caches by id AND version, so re-packing 0.1.0-alpha with a changed layout would let a machine that had already restored it keep the old assembly, with nothing looking wrong anywhere. The tests are the substantive part. Bloom's pinned test asserted SchemaVersion, Size, ActivityMaxBytes and the section name - but not a single field offset, so a package upgrade that MOVED a field would have sailed through: Bloom would compile, run, and publish its health to offsets the Doctor no longer reads, producing reports that look plausible and are wrong. That was the whole failure mode the fixture existed to prevent, and it did not cover it. It does now, working from the layout's own published Fields table: - LayoutIsWhatBloomWasBuiltAgainst pins every field's name, offset and size by value, so a moved field fails Bloom's build. - AddingAFieldToTheProtocolDoesNotBreakBloom pins the invariant rather than the numbers: PayloadBytes must be exactly the end of the last field, must be at least the generation-1 baseline, and the baseline itself must stay 328. This is what lets the test above be strict without being a nuisance - the protocol is allowed to grow, and growth costs one line in the pinned list and changes nothing else on Bloom's side. - WhatBloomPublishesSaysHowMuchOfItIsReal checks the extent actually reaches the shared page, rather than trusting the constant to be right. Note the first test's comment says explicitly NOT to update the numbers to make it pass. A moved field means every already-installed Doctor is reading Bloom's page wrongly, so the numbers are the symptom. FreezeDoctor tests: 11 passed. Full suite: 3155 passed, 13 skipped, 0 failed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Your trial has ended. Reactivate Greptile to resume code reviews.
Read this for shape, not to merge yet
Stacked on #8218 — review that one first; this branch's base is that branch, so its diff shows only the migration.
Nothing is published. The package is restored from a local folder, so this branch is buildable and reviewable before anything irreversible happens. Companion PR: BloomBooks/bloom-freeze-doctor#1.
Why
Three files describing the wire format between Bloom and the Freeze Doctor — the shared-memory layout, the session file, the named events — were 750 lines of hand-maintained duplicates across two repos. They had already drifted, and a fix in one had to be carried across the other by hand.
Drift there fails silently and expensively: Bloom writes one set of offsets, the Doctor reads another, and the reports look plausible and are wrong.
What this removes
193 insertions, 1095 deletions.Three things go away, and the third is the one worth noting:RobustFilepre-commit exemption. It was only needed because the copies couldn't callRobustFileand still match the originals. The files have left this repo, so that shared quality gate is back to full strength with nothing excused from it — the check passes on its own merits. This is the exemption that needed approving in the last preflight; it's now moot.What arrives
One
PackageReferenceonBloomBooks.FreezeDoctor.Protocol— with no transitive dependencies at all, just a 26KB assembly, which is most of what makes this reasonable to add to Bloom — twousinglines, and temporary local-feed scaffolding (NuGet.Configat the root,build/pack-freeze-doctor-protocol.ps1,localpackages/) that all disappears when the package is published.To build it
It packs the protocol from a clone of the Doctor's repo beside this one (or
BLOOM_FREEZE_DOCTOR_REPO) intolocalpackages/. That project only exists on the Doctor repo'scontract-packagebranch until it merges.Why "Protocol" and not "Contract"
These files are not a contract in any sense a reader would recognize from the word — no interface, no assertions, nothing enforced. They are the wire format of the shared-memory channel between two processes: byte offsets, a schema version, event names. "Protocol" says that, so the package, its namespace, its folder in the Doctor's repo, and the test fixture here all say it.
The
BloomBooks.prefix follows the house style already in production:BloomBooks.Velopackis a Bloom-only package published the same way (nuget.org, then unlisted — all 11 versions currently unlisted, 0 search hits), which also settles the earlier open question about aSIL.prefix. There isn't one to seek.The pinned test, with its purpose rewritten — and a hole in it closed
FreezeDoctorProtocolTests(wasDoctorChannelContractTests) used to guard against two copies diverging. That reason is gone, but it still earns its place for a different one: it pins the layout Bloom expects against the layout the referenced version actually has. A package upgrade that moved an offset would otherwise be silent — Bloom would compile, run, and publish its health where the Doctor no longer looks.It wasn't actually doing that. It pinned
SchemaVersion,Size,ActivityMaxBytesand the section name — not a single field offset. So the one failure it existed to prevent was the one it did not cover, and it couldn't have: the offsets areinternalto the package. It now works from aFieldstable the layout publishes about itself, and asserts every field's name, offset and size by value. Its comment says explicitly not to update the numbers to make it pass — a moved field means every Doctor already installed is misreading Bloom, so the numbers are the symptom.Two tests join it. One pins the invariant rather than the numbers, which is what lets the first be strict without becoming a nuisance:
PayloadBytesmust be exactly the end of the last field, and at least the frozen generation-1 baseline. The other checks Bloom's extent actually reaches the shared page rather than trusting the constant.PayloadBytes, and why the version moved to 0.2.0-alphaThe protocol now records how far the writer wrote. This is Bloom's side of bloom-freeze-doctor@aaec915, and for Bloom it is only a reference bump — Bloom writes the same fields either way.
Worth knowing why it exists, since it changes how this format is allowed to evolve: adding a field is the likeliest change, and until now there was no way to make one.
SchemaVersionis part of the section's name, so bumping it makes an old Doctor stop finding the channel altogether rather than merely ignoring a field it doesn't know. With an extent recorded, fields can be appended within a generation, and a newer Doctor reading an older Bloom can tell a field that Bloom never wrote from a real zero — the page is zero-filled, so without it a report would confidently say "0 blocked workers" where the truth is "this Bloom is too old to say". That direction is the common one in the field, not a curiosity: the Doctor updates itself through Velopack while Bloom versions linger, and the 6.4 backport guarantees more than one vintage writing this page.Several offsets moved to make room for it in the header. That was free only because nothing is published yet, which is a reason to settle the package id and get it published rather than to keep the local-folder arrangement going.
The version bump is not cosmetic: NuGet caches by id and version, so re-packing
0.1.0-alphawith a changed layout would let a machine that had already restored it keep the old assembly, with nothing looking wrong. Bump the version for any layout change, even while restoring from a local folder.Decided, so not up for review here
The feed: publish to nuget.org, then unlist the version — absent from search and browse, still restorable when a consumer pins an exact version, and no feed credentials anywhere. A private feed was considered and rejected: it hides nothing (the Doctor's repo is public MIT, so the package is a compiled copy of readable source) and would cost credentials on every dev machine, in TeamCity, and in CI. Reasoning lives in the protocol
.csproj.The id, likewise:
BloomBooks.FreezeDoctor.Protocol. An id on nuget.org is claimed permanently, which is exactly why it is being reviewed before anything is pushed rather than after.Still open
BloomBooks.Velopack's fork uses a repo-levelBLOOMBOOKS_NUGETsecret; whether that is an org-level key we can reuse couldn't be checked from here. Note the gotcha for whoever holds it: pushing a new package id needs a key scoped to "Push new packages and package versions", not just new versions of existing ones.Verified
Bloom builds against the package with the copies deleted, and the full suite is 3155 passed, 13 skipped, 0 failed — the two extra over the pre-package count are the new layout tests.
The Doctor-side sentinel test (the writer never touches a byte past
PayloadBytes) was verified by mutation rather than by passing: a stray write past the extent makes it fail with the intended message, and the mutation was reverted.🤖 Generated with Claude Code
Devin review
This change is