A declined package names the plan, not the grant
An operator message is an instrument. This one answered confidently about a knob it had never read, and two separate passes followed it to a remedy that does nothing.
The sentence, and what it cost
RegistryPackages.NotOffered is what the Store's provisioning banner and the Admin/Maintenance
RefreshModules task print when a registry declines a package. Until #1802 it read:
the registry R does not offer a package 'P' to this instance — check the instance's catalog grant
On #1584 — the one case anybody actually chased — the grant was already correct, and both readers did what the sentence said:
| when | the prescribed remedy | what it would have done |
|---|---|---|
| 2026-09-10 03:07Z | "grant the memex instance the personal plan on the registry (Plugins/*@personal)" |
nothing |
| 2026-09-13 08:17Z | "grant the control instance's registry key the plan its installs need (Plugins/*@enterprise covers all eleven)" |
nothing |
Measured on the registry (memex.meshweaver.cloud) 2026-09-13 17:26Z: Admin/_PluginGrant/memex was
at v4 with entries Plugins/* and Crm/*, neither carrying a tier — the grant already reached
every Plugins package — while rbuergi/MeshWeaverInstance/memex was at v1, unchanged since
2026-08-06, with no plan field at all. PlanTierRanks.CoversInstance reads a blank plan as
BaselinePlan = "free", deliberately. The refusal was the instance's plan.
And a grant edit cannot fix a plan refusal even in principle. Since MeshWeaver#2804 an entry's tier is a CAP that only lowers:
// PlanTierRanks.Narrower
return capRank < planRank ? capId : plan;
Narrower("free", "enterprise") is "free". Applying either remedy above would have produced no
change at all, and would have read as "the fix didn't work" — which is the second cost, after the
wasted pass: a correct diagnosis arriving later has to overcome an experiment that appeared to
falsify it.
This is the same family as a message that reported an ABSENCE in the vocabulary of a DISAGREEMENT and sent a session chasing a bake epidemic that did not exist. The defect is never the wording on its own; it is a sentence that states a cause it did not measure.
Two causes, and a registry answer that could not tell them apart
Before MeshWeaver#4097 a plan-tier refusal was indistinguishable from "no such package" on the wire. That is not a bug — it is the enumeration defence: a package outside an instance's granted sources must stay byte-identical to absence, or a registered instance could map out everything a registry carries by asking for names.
#4097 opened exactly the crack that is safe to open. RegistryPackageSource.ListCatalog returns a
RegistryListing carrying both halves:
public sealed record RegistryListing
{
public IReadOnlyList<PackageManifest> Packages { get; init; } = [];
public IReadOnlyList<PlanTierRefusal> Refused { get; init; } = [];
}
PluginGrant.TierRefusal answers only when some grant entry already reaches the package and the
plan is what refuses it — i.e. only for a package the registry itself declared in this instance's
default set. Nothing new is disclosed.
So there are now two shapes, and they want two different sentences:
| the registry's answer | what may be said |
|---|---|
a typed PlanTierRefusal naming this package |
the cause: the plan, the tier required, and where the plan is written |
| no refusal for this package (pre-#4097 registry, or a refusal outside the default set) | the candidates: grant and plan — and no prescription |
The second row is why the old wording had to change too, not merely gain a branch. A refusal for a package outside the default set is withheld by the same rule that withholds an ungranted one, so "check the grant" is a guess in that case as well; it now names both knobs and says the registry reported no reason, so a reader knows they are holding a candidate list rather than a diagnosis.
Why the corrected sentence cannot drift
The fix is structural, not a re-wording. RegistryPackages.Install now reads ListCatalog instead
of ListPackages — the whole answer rather than its first half — and the refusal branch carries
core's own sentence verbatim:
public static string NotOffered(
string packageId, string registryUrl, IReadOnlyList<PlanTierRefusal>? refused) =>
(refused ?? [])
.FirstOrDefault(r => string.Equals(r.PackageId, packageId, StringComparison.OrdinalIgnoreCase))
is { } refusal
? $"{refusal.Describe()} Raise the PLAN on the registry {Normalize(registryUrl)} — …"
: NotOffered(packageId, registryUrl);
PlanTierRefusal.Describe() is the one method every consumer surface says a plan refusal with —
/health required_modules, the package card, the self-updater. There is no second copy of those
words in this repository to go stale, and the verdict comes from the registry's own
PluginGrant.TierRefusal rather than from anything this repo re-decides. A message written beside
the code that decides drifts from it; a message derived from the decision cannot.
What this repo adds is only the remedy — Settings → Instance Grants → the plan form, on the
registry — because the registry knows the refusal and not the operator's route to it. That is pinned
by its own assertion (NotOffered_WithAPlanRefusal_NamesThePlan_NotTheGrant), together with a
negative that fails if the guessing sentence ever comes back into the branch that has a verdict.
What inherits the fix without changing
Both consumers read the thrown exception's Message:
- the Store's provisioning banner, through
SystemInstall; ModuleRefresh.RefreshOne, whose per-registryCatchfoldsexception.Messageinto its report line — and which must stay per-registry and inside theConcat, because a catch outside it lets the first registry's miss terminate the sequence and leaves every later registry untried.
The limit, stated
A registry that predates #4097 sends no refused member at all, and a plan refusal for a package
outside the instance's default set is never reported by design. Both land on the honest fallback.
The sentence is therefore not a guarantee that a plan problem will be named — only that it will
never again be mis-named as a grant problem.
Related
- Systemorph/MeshWeaver.Plugins#1802 — this defect · #1584 — the issue it misled on, twice.
- Systemorph/MeshWeaver#4097 — the typed refusal · #2804 — the cap-never-raises rule.
- The executed registry-install fixture — how this lane is exercised.