Advisory floors in a dependent — what changed here, and what the pin bump measured

MeshWeaver#3648 / #3651 (2026-09-07), Doc/Architecture/ModuleAdoptionPolicy rule R2: a module's declared minMeshVersion is advisory — logged, shown on the status row, and it decides nothing. Loadability is measured: the type-level link probe against the running platform, and the actual load. Step 4 of the same policy made a missing content bake an advisory too — a cost ("would recompile at boot: …"), not a hold — except on a Modules:RequirePrebuilt instance.

The reason is concrete. NuGetVersionComparer ranks ci < rc < clean, so an rc or clean floor could never be satisfied by any ci build. On 2026-09-07 that made memex-cloud's self-updater decline all 11 candidate releases ("77 plugins required … every one declined") and held every production portal on its morning build for the day — while the link probe would have loaded every one of those modules.

What that did to THIS repo

Nothing visible, until the pin moved. Measured on fix/1513-teardown-twin-and-pin at core 73d94b55f, with the denominator, because a count without one cannot tell "these twelve broke" from "the assembly did not load":

suite at core 73d94b55f (past #3661) after adapting
MeshWeaver.PluginCatalog.Test (assembly, 726) 714 / 726 — 12 failed 727 / 727
SelfUpdateAvailabilityGateTest (class, 6) 4 / 6 — 2 failed 7 / 7, in 3 s instead of 63
MeshWeaver.Hosting.Monolith.Test (assembly, 806) 804 passed, 2 skipped, 0 failed

CI shards that second assembly, so its run reported the two failures as 328 / 330 — a slice, not the assembly. Quote the denominator you actually measured; they are not the same number.

Both projects still built clean at the new pin — -c Release -warnaserror, 0 Warning(s), 0 Error(s). That is the point: this is AGENTS.md's shape 7, a method's behaviour changing behind an unchanged signature. No surface detector can see it by construction, the Cross-repo pair (public surface) gate looks for removed types and members and nothing was removed, and for a dependent that PINS core not even the release-event partial cover fires — the platform-ref job resolves the same pin on a release event as on a pull request.

So the pin bump IS the integration test. For a repo on a pinned platform there is no earlier signal, and there is no Dependent suites check anywhere that would have produced one. A red on the bump is that test firing, not a broken bump — and the answer is never to hold the pin, because the staleness arm of Validate node repos measures both frozen inputs against core main and turns every open pull request in the repo red once either passes 24 h or 120 commits.

The one judgement each red test needs

Every failure falls in exactly one of two buckets, and mixing them is the expensive mistake:

  1. It asserts the behaviour the policy replaced. Rewrite it to the new contract — and keep it asserting something. "Advisory" has two halves: the decision no longer refuses, and the claim is still produced. A test rewritten into a bare "it loads" has lost half its subject and would pass equally against an implementation that had stopped reading the floor at all. Every rewrite here asserts the advisory's presence and its absence on the satisfied side.
  2. It asserts something still valid and broke for another reason. Then it is a regression and the fix is not in the test.

The authority for bucket 1 is the policy page's list of decision points plus core's own reference suites (ModuleFloorAdvisoryTest, ModuleFloorAdvisoryFunnelTest). A failure that does not map onto one of those decision points is the signal for bucket 2. Guessing wrong cements a regression that no gate can see.

A red that is neither, exactly: the fixture stops producing the state

SelfUpdateAvailabilityGateTest's two failures were 30-second timeouts, which is not the shape of an assertion about a floor, and they deserved the suspicion they got. The cause was neither a stale assertion nor a regression:

The subject — a held release is not rolled, and the hold is written where the Updates tab reads it — was untouched, and every assertion stayed. What had to move was the fixture: the verdicts are now built under new ReleaseGatePolicy(RequirePrebuilt: true), the supported strict mode in which a missing bake is still a hold. What a hold is made of is ReleaseAvailabilityTest's subject; what the poller does with one is this class's, and the policy change does not touch it.

🚨 A fixture that quietly stops producing the state under test reads as a hang, not as a cause. That is why the class now carries a positive control on the seam itself (TheSeamsTwoVerdicts_AreActuallyAHoldAndAPass): it goes red in milliseconds, naming the reason, where the two real tests could only wait.

The floor's new answers, per decision point

Every one of these used to refuse, hold or skip; each now proceeds and says what was declared.

decision point old answer new answer
ModuleUpdateDecision.Decide SkipPlatformBelowFloor Land, floor worded into Reason
PluginBundleClient.LandFromBundle landed 0, nothing on disk lands, floor recorded on the entry
ModuleLandingService.LandModule InvalidOperationException lands; only the link probe refuses
ModuleLandingService.ShelveModule Held, no PendingRestart ordinary landing, PendingRestart true
ModuleActivationBoot.ComputeEffectiveModuleEntries skipped, loudly effective, announced on onAdvisory
ModuleActivationStatus.NotYetLoaded / Unresolvable excluded as HELD bucketed by its BYTES, Advisory set
ReleaseAvailability.IsUpdatable ModuleFloorExceedsTarget blocker IsUpdatable, floor on Advisories
ReleaseAvailability (no sealed bake) ContentBakeMissing blocker advisory + BootCompiles, unless RequirePrebuilt

ModuleFloorExceedsTarget still exists as an enum member — binary API for hosts compiled against the previous platform — and nothing produces it. A test asserting that kind is asserting a dead branch.

The floor did not become worthless: it is recorded on the activation entry, carried onto status rows as "declares platform ≥ X; running Y", and still gated at pack time by check-module-floors.py / check-module-platform-floor.py, where a module declaring a floor above the platform it was built against remains an authoring error.

Reading this page as a recipe

When a pin bump reddens a suite here:

  1. Re-measure with the denominator, both pins, same tree. "12 failed" is not a measurement; "12 of 726 failed, 726 of 726 at the old pin" is. A count with no denominator cannot distinguish "these twelve broke" from "the assembly did not load".
  2. Read the core change's own doc page and reference tests before touching an assertion. The policy page enumerates the decision points; if a failure does not map onto one, stop.
  3. Prefer the positive evidence in the failing test's own output over inferring a cause from the failure shape. A timeout says only "the condition never came"; the log line above it said which branch actually ran.
  4. Batch, then push once. A push to a pull request here buys a ~105-job run; a guaranteed-red run is waste, not information.

See also

Reconnecting…
The server was updated. Reloading the page to pick up the latest version.