Issue Taxonomy and the Release Readiness Gate

A queue you cannot route is a queue you cannot finish. Before this taxonomy an open issue carried a type at best, so the two questions that actually drive work — what is still open for this plugin? and what is still open for this feature? — could only be answered by reading every title. And the question that gates a release — is anything blocking? — could not be answered at all.

Every open issue now carries up to four labels (closed issues are out of scope — policy issue-taxonomy-scope, see the last section). Three are routing. One is a gate.


The four axes

axis label applies to purpose
Type bug · enhancement · documentation · chore everything, exactly one what kind of work
Area (core) area:<subsystem> MeshWeaver which part of the framework
Plugin (modules) plugin:<Partition> plugin repos which module owns it
Feature feature:<slug> everything with a specific subject the thing itself, across repos
Severity sev:B · sev:H · sev:M · sev:L bug only how bad

chore means CI, build, dependencies or cleanup with no user-visible behaviour change.

feature: is the axis that crosses repositories. A defect in core and the plugin change that depends on it share a feature slug and nothing else; that shared slug is the only way to see them as one piece of work. It also exposes duplication that titles hide — the first pass found six separately-filed issues that were one RoutingGrain back-pressure defect, one per activation.


Severity, and why only bugs have it

label meaning
sev:B BLOCKING — the release cannot ship while this is open
sev:H BLOCKING — a primary path broken but a workaround exists; an intermittent user-visible failure; or silently WRONG results anywhere
sev:M a secondary path broken, or a clear defect with an easy workaround
sev:L cosmetic, a rare edge case, or developer-only

An enhancement, documentation or chore has no severity. If something is not broken it cannot block a release, and letting a feature request carry a blocking flag is how a release gate rots into a wish-list.

sev:B and sev:H are the classes where "we will do it next sprint" is not an available answer — policy release-blocker-gate. sev:M and sev:L are a priority conversation and never gate a cut.


The gate

label:bug  label:sev:B  state:open   →   must be ZERO
label:bug  label:sev:H  state:open   →   must be ZERO

across the seven repositories that carry the taxonomy: MeshWeaver, MeshWeaver.Plugins, MeshWeaver.Crm, MeshWeaver.SocialMedia, MeshWeaver.Reinsurance, MeshWeaver.Manufacturing, MeshWeaver.Education. Memex and MeshWeaver.Feedback are deliberately not gated: they ship no product code.

🚨 Name the repositories; never write "every repo of the product". A repo that is not on the list is not gated, and — worse — a repo on the list that has never had the sev:B label created answers a label query with an empty array, which folds to "no open issues" and is green forever. That was live: the gate named seven repositories and the label existed in five — which is why NoOpenIssues now refuses a label that does not exist instead of folding it to Green (MeshWeaver.Plugins#2190).

That is the whole release-readiness predicate — policy release-blocker-gate — and it is enforced rather than remembered: the release.cut standard in the Governance package requires Gate.NoOpenIssues(repo, "sev:B") and Gate.NoOpenIssues(repo, "sev:H") for each of the seven gated repositories — 14 gates — so a release proposal cannot reach Ready while one is open. The pattern it uses is the long-running check (get Governance/Skill/long-running-check); see Release Process for what a release then is.

Two rules that keep the gate honest

1. A blocking severity is never assigned by guess — and never REMOVED to clear the gate. If you cannot prove from the evidence that a defect blocks the release, assign the severity you can justify and say why. 🚨 Raising the bar to sev:H creates a pressure the sev:B-only gate never had: the cheapest way to a green gate is now to downgrade an H to M. That is the one move this gate cannot survive, so release.cut's acceptance criterion names it — not relabelled or DOWNGRADED to sev:M to clear the gate. A gate that fills with defensive B's stops being a gate — people start shipping past it, and then the one real B ships too. Under-calling is recoverable because someone raises it; over-calling destroys the signal.

2. Re-run the query, and check its coverage. A zero means either "nothing blocking" or "truncated, unanchored, rate-limited, or answered from a subset". Those are indistinguishable from the count alone, and this is the one place where a false zero ships a known blocker. Read the count against the coverage; treat a refusal as an error, never as a pass.

What this gate does NOT decide

It says nothing about whether the artifacts exist. "Is every package available for this release?" is a different predicate with its own page — Release Availability Gates — and the two are complementary: one asks whether the product is built, this one asks whether it is fit to ship. A release needs both, and neither substitutes for the other.

It also does not restate what release.yml already refuses at tag push (an unsealed set, an unpromoted commit, a mismatched PlatformVersion, missing release notes). A gate that duplicates the lane is a second source of truth that will drift.


Where the tracking lives

GitHub is the ledger — the one place an issue is WRITTEN. The mesh reads it, and adds what GitHub cannot hold.

holds written by
GitHub issues what must be done — the labelled queue, and the gate query people and triage
GitHubIssue at {space}/_Issue/{number} a one-way mirror of the ledger, refreshed by sync and by webhook system-security
Feedback/Feedback intake — a finding as it arrives, before it is a work item agents, users
Hosting/TriageItem what is being done — the thread a defect is worked in, and what came of it (threadPath, status, issueUrl) the triage agent

🚨 The mirror already exists and is one-way by design. MeshWeaver.GitSync's IssueService materialises every issue as a GitHubIssue node under the space's _Issue satellite, refreshed by an explicit sync and by a live webhook. The node is never edited directly: a mutation runs against GitHub and the mirror re-syncs. So there is exactly one writer, and none of the bi-directional problems a two-way mirror would bring.

That one-way direction is the load-bearing part. Making the mirror writable would put two writers over one set of rows for no gain — PRs close GitHub issues natively, and a NoOpenIssues gate reads GitHub directly, so nothing downstream needs the copy to be authoritative.

⚠️ Hosting/Issue is not part of this. It is fleet health — "no replicas ready", "not observed" — machine-written, self-resolving, one node per deployment × condition. It carries its own Severity (Warning/Critical, set by the detector), which is unrelated to the sev: scale here: that one grades a live deployment symptom, this one grades a defect in the product.


Scope: the OPEN set, and only the open set

Closed issues are out of scope. They are not classified, not counted, and no query on this page looks at them — policy issue-taxonomy-scope. Every query here carries state:open, including the gate.

That is affordable because the open set is hundreds, not thousands — 127 across five repositories on the day this was written, which is why the whole of it could be classified in a single pass and kept classified since. A taxonomy is worth maintaining at a scale where every open row can carry it; a backlog that outgrew that would be a backlog problem, not a labelling one.

The corollary for an agent: do not go back over history. Classify what is open, keep it classified as triage files new work, and let closure — not labelling — dispose of what is dead.

The first classification pass — 2026-09-20

All 127 open issues across the five repositories were classified in one pass.

bugs sev:B sev:H sev:M sev:L
MeshWeaver 63 0 18 32 13
MeshWeaver.Plugins 21 0 11 8 2
Crm · Reinsurance · Education 2 0 0 2 0

Read the zero carefully. The classifiers were instructed never to guess B, so it means nothing was proven blocking — not nothing blocks. On the day the taxonomy was introduced the gate was already open, which is a finding rather than a success: a gate that has never refused anything has not yet been tested.

Several sev:H calls sit close to the line and deserve a human ruling — a half-finished migration that gates portal boot, an install whose verify can never succeed, a publicRead that would publish every future user submission. The rubric says each of those could be B; the evidence in the issue did not prove it. That is exactly the judgement the first rule above reserves for a person.

That ruling was made on 2026-09-20: the bar is no sev:H left. It settles the question this paragraph opened without needing a per-issue re-litigation — the three calls above block a release either way now. It also makes the gate non-vacuous for the first time: on the day the bar moved, sev:B was 0 across all nine repositories while sev:H stood at 30 (MeshWeaver 18, MeshWeaver.Plugins 12, every other repo 0). A gate that refuses something is a gate.

🚨 These are a SECOND, later snapshot, not a correction of the table above. The first-pass table records the classification as it stood when the pass finished (Plugins sev:H = 11); this count was read from the live labels at ~12:35Z the same day, when the bar moved, by which time Plugins carried 12. Both are true of their instant. The number that gates a release is never either of them — it is whatever release.cut's NoOpenIssues gates read when the cut is proposed.