ADR-0015: Validate descriptors against Backstage field formats before canonicalizing identity
| Field | Value |
|---|---|
| Status | proposed |
| Date | 2026-07-25 |
| Review by | 2027-01-25 |
| Schema version | 0.1.0 |
| Reversibility | two-way-door |
| Blast radius | org |
| Scope | org |
| Tags | catalog, governance, matching, core |
| Deciders | @mbeacom |
| Authored by | agent-drafted |
| Review tier | arb |
| Review reason | Adds a precondition to the canonicalization step ADR-0012 pins, and adds a trigger condition to its org-scope, one-way-door atomic fail-closed clause. ADR-0013 set the precedent that narrowing or extending a frozen clause of an org-scope record takes the ARB tier. A misjudged admissibility rule silently removes entities from governance reach entirely, so that no ADR assertion ever fires against them — an org-wide consequence irrespective of how narrow the rule’s own mechanism is. |
| Assertions | entity-admissibility-precedes-canonicalization (custom, error) |
| External refs | Decide catalog entity-to-path binding before feature 009 |
| Relates to | 0009, 0012, 0013 |
| Affects | path:packages/core/src/affects/catalog.ts, path:packages/adapters/catalog-*/**, path:specs/009-catalog-binding-viability/contracts/entity-identity.md |
| Source | docs/adr/0015-validate-descriptors-against-backstage-field-formats-before-canonicalizing.md |
Status:
proposed. This record is a draft for maintainer review and is not ratified. It authorizes nothing on its own. In particular it does not claim that feature009 is unblocked, does not authorize any spike task, re-run, or generator invocation, and does not sanction any production catalog adapter.
Context
Section titled “Context”ADR-0012 pinned the catalog entity-to-path binding contract against a frozen
Backstage commit, 1121a4facd9e321179d0402c3f355e4a649e84d9, and located the
identity rules in specs/009-catalog-binding-viability/contracts/entity-identity.md.
That contract’s §1 defines canonicalization as exactly two steps:
- If
metadata.namespaceis omitted, default it todefault. canonicalId := ${K}:${NS}/${N}, lowercased in full.
There is no admissibility or validation step anywhere ahead of those two
steps, and nothing under specs/009-catalog-binding-viability/** references
Backstage’s entity-name validators at any point. Whatever string appears at
metadata.name is accepted as a name and folded into a canonical identity.
At the same pinned commit, Backstage’s own catalog model does not treat those
fields as free-form.
packages/catalog-model/src/validation/makeValidator.ts binds four distinct
per-field validators, and FieldFormatEntityPolicy applies them to every entity
it is invoked on:
| Field | Validator binding | Predicate | Applied |
|---|---|---|---|
apiVersion |
isValidApiVersion → CommonValidatorFunctions.isValidPrefixAndOrSuffix |
prefix is a DNS subdomain (CommonValidatorFunctions.isValidDnsSubdomain, ≤253 total, each dot-separated label ≤63); suffix is /^[a-z0-9A-Z]+$/, ≤63 |
required |
kind |
isValidKind |
/^[a-zA-Z][a-z0-9A-Z]*$/, ≤63 |
required |
metadata.name |
isValidEntityName → KubernetesValidatorFunctions.isValidObjectName |
/^([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]$/, ≤63 |
required |
metadata.namespace |
isValidNamespace → KubernetesValidatorFunctions.isValidNamespace → CommonValidatorFunctions.isValidDnsLabel |
/^[a-z0-9]+(?:\-+[a-z0-9]+)*$/, ≤63 |
only when present |
Two further properties of the apiVersion binding, since the split is not
obvious from the predicate alone: isValidPrefixAndOrSuffix splits on / and
rejects any value containing two or more separators, and a value with no
separator is validated against the suffix predicate alone — so a bare v1
passes without the subdomain rule ever being consulted.
Every row in this table was verified by executing the pinned sources directly; see “How these bindings were checked” below.
The consequence is a category error in the contract, and it is not hypothetical.
Sixteen descriptors in the two corpora feature009 pinned — five in
community-plugins at 92e9e4e09c76cc57f3475029b73e5ec84498a459, eleven in
rhdh-plugins at 3b355ddfedb23c6656bd9effc8510f9926b765c1 — carry an
unsubstituted, un-rendered scaffolder placeholder at metadata.name rather than
a name. Fourteen of them (five and nine respectively) carry the identical string
${{ values.name | dump }}. The remaining two, both in rhdh-plugins, carry
${{ values.name }} (bulk-import) and ${{ values.entityName }}
(orchestrator) — equally unsubstituted, but distinct strings that canonicalize
distinctly. All three forms fail isValidObjectName on character class: $,
{, } and the spaces are outside the permitted set in every case, and | in
the first, while ordinary descriptor names pass the same predicate unchanged.
Backstage’s own metadata.name validator rejects every one of these
descriptors. The contract instead accepts each as a well-formed name, lowercases
it, concatenates it — and, for the fourteen that share a string, reports the
result as triggerClass: "duplicate-canonical-id".
The other two are the sharper case, and the reason this is a contract gap rather than a reporting one. Being canonically distinct, they collide with nothing. They are exactly as invalid as the other fourteen, and the contract has no mechanism of any kind that would notice. The duplicate rule catches the fourteen only incidentally, as a side effect of their sharing a string; behind it there is nothing. Duplicate detection is not a validity check, and the corpus contains two descriptors demonstrating that the conditions come apart.
The fail-closed rejection there is correct and required by §3. What is wrong is the classification: a descriptor that is not a valid Backstage entity at all is being characterized as an identity collision between valid entities. Those are different defects with different remedies, and the contract currently cannot tell them apart, because it never asks whether the descriptor is a valid entity in the first place.
This record exists to close that gap. It is warranted by the validator bindings above and by nothing else.
How these bindings were checked
Section titled “How these bindings were checked”Action item 2 of the first draft called for an independent review from a fresh
context. That review was performed by a claude-opus-5 reviewer with no
authorship of this record, against
1121a4facd9e321179d0402c3f355e4a649e84d9. It returned FAIL, with three
blocking defects in the table above. All three are corrected here, and are
recorded rather than quietly fixed:
- A cited member did not exist. The namespace row read
isValidNamespace→KubernetesValidatorFunctions.isValidDnsLabel.KubernetesValidatorFunctionshas no such member; the chain runs throughKubernetesValidatorFunctions.isValidNamespacetoCommonValidatorFunctions.isValidDnsLabel. The quoted regex was correct throughout; the class attribution was not. - The
apiVersionbound was understated. “≤63 per part” was wrong for the prefix, which is a DNS subdomain: ≤253 in total with each dot-separated label ≤63. A 243-character prefix passes. The two-separator rejection and the suffix-only path for unseparated values were also unstated. - An ordering claim exceeded the evidence. The draft asserted that the
contract “never asks the question Backstage itself asks first” and that
Option A “matches Backstage’s own ordering”. Neither is established by the
four cited files: they contain no uniqueness, duplicate, conflict, collision
or dedup logic of any kind, and
FieldFormatEntityPolicyis a per-entityEntityPolicy.enforcewith no cross-entity comparison. Catalog-wide uniqueness lives inplugin-catalog-backend, outside this record’s cited evidence. Both phrases are removed, not substantiated.
Defect 3 is the one worth dwelling on, because the correct response to it was to drop the claim rather than go looking for support. This record’s stated warrant is the validator bindings “and nothing else”; an appeal to Backstage’s internal ordering is outside that warrant even if it happens to be true. Ordering admissibility ahead of canonicalization is adrkit’s own decision and needs no upstream precedent. Removing the appeal makes the record narrower and stronger.
Every row of the table was subsequently re-verified by executing the pinned
sources directly — makeValidator.ts, KubernetesValidatorFunctions.ts and
CommonValidatorFunctions.ts fetched at the pinned commit and run — confirming
that isValidEntityName and isValidNamespace are reference-identical to their
KubernetesValidatorFunctions counterparts, that KubernetesValidatorFunctions
exposes no isValidDnsLabel, and that a 243-character apiVersion prefix passes
while a 254-character one, an over-63 label, and a two-separator value all fail.
The reviewer pre-cleared the outcome of these corrections: “On re-review after
these three corrections, items 1, 2 and 4 stand verified and the record’s core
warrant is sound.” That is a conditional clearance of the corrections, not a
ratification of the record, which remains proposed.
Residual limit. “Applies them to every entity” above is now qualified to
“every entity it is invoked on”. The cited files show what
FieldFormatEntityPolicy.enforce does when called; they do not show its
installation in any particular catalog pipeline, and this record does not assert
it.
Subsequent reviews: four further passes, all FAIL
Section titled “Subsequent reviews: four further passes, all FAIL”This record has been independently reviewed five times, across two model lineages. Every pass returned FAIL. The later passes were each bounded to the diff produced by the one before.
Per-pass reviewer identities are attested by the orchestrating session and are not checkable from anything in this repository — the model assignment exists only in that orchestration. This section therefore records the review count and the fact of two lineages, both of which follow from the standing model policy, rather than a pass-by-pass roster. A reviewer reading this record cannot verify such a roster and should not attempt to confirm one; an earlier version stated one and it was wrong on both order and totals.
Review 1 corrected substance. It changed two rows of the validator table —
the apiVersion binding and its numeric bound, and the metadata.namespace
chain — and removed a claim from Option A. Those corrections are described above.
Reviews 2 to 5 found nothing wrong with the substance. No pass after the first altered the Decision, the options, the validator table, or any figure. The validator bindings and predicate bodies, the executed regexes, the descriptor counts, the over-length pair, the residual Nexus duplicate and the governance metadata have all stood unchanged since review 1’s corrections. What failed in those four passes was prose — and, in the last three, prose describing the earlier failures.
Defects found and corrected across passes 2 to 5:
- A separability claim was false: two enumeration mistakes were said to inflate a count independently. They are conjunctive for that count, though one of them alone does corrupt a different count. Replaced with a four-way table.
- “Both unsubstituted forms” — the pinned corpora hold three.
- “The placeholder descriptors recorded above at least collide” —
bulk-importandorchestratorcanonicalize uniquely and collide with nothing. - “Backstage would reject … outright”, and five sibling phrases, asserted behaviour of Backstage as a running system. The cited files establish what the validators do when applied, not that any deployment applies them. All are now scoped to the validators’ own behaviour. Eight such phrases existed in the original draft; the first review had already caught two of them, and the rest were found here.
- “Depth-based heuristics do not” separate the two lines was too categorical; indentation can distinguish them. The argument for structural extraction is fragility, not incapacity.
- The sweep for those Backstage-system phrases claimed completeness while
incomplete — it was line-oriented, so
input that Backstage itself rejectssurvived in Option E because the phrase wrapped a line break. Re-run whitespace-normalised. - A superseded claim survived above its own replacement, still asserting that skipping structural extraction “will over-count”.
- A passage about false universals contained one.
- A section lead-in contradicted its own conclusion.
- The account of these defects was itself wrong, twice, in opposite directions — first attributing them to revision between reviews, then to a claim having been true when written and later falsified.
What the provenance analysis concluded
Section titled “What the provenance analysis concluded”Four claims were traced against 4773d25, the document the first review
examined, and against the pinned corpora themselves. All four were false at
the moment they were written. Two pre-dated the first review and were missed by
it; two were introduced during correction rounds. The category the record twice
reached for — accurate when written, later falsified — is empty.
Two root causes, each transferable
Section titled “Two root causes, each transferable”The warrant is a set of pure predicates read at a pinned commit. Any sentence about what Backstage as a system does — its ordering, its acceptance, its rejection — reaches past that warrant even when it is probably true. The reliable form is to state what the predicate returns.
A claim about a frozen artifact must be indexed to the artifact, not to the author’s knowledge of it. “Both unsubstituted forms present in the corpora” was defended on the grounds that only two forms were then known. That conflates what had been enumerated with what was true: the corpora are pinned, the third form was already in the tree, and the sentence was therefore false on arrival. For claims of this shape there is no window of excusable truth — which makes the class larger than it first appears, and also entirely checkable by anyone, at any time, without a reviewer’s judgement.
The corollary corrects a lesson this record previously drew the wrong way round: a count change does not falsify prose; it reveals prose that was already false. Re-read quantified prose when a count changes — not because the change broke it, but because the change is often the first occasion anyone re-reads it. And note that not every nearby quantifier is wrong: the work is re-reading, not substituting a digit.
Why this section is short
Section titled “Why this section is short”An earlier version narrated each defect’s origin commit by commit, and then narrated the corrections to that narration. Successive passes kept finding defects in that material rather than in the record’s substance — it grew with every pass and each addition was new surface to be wrong on, while nothing in it was load-bearing for a reader of this record. It has been compressed deliberately. No defect has been removed from the list above; what was removed describes only itself.
Decision
Section titled “Decision”A descriptor MUST be admissible before it is canonicalized. Admissibility is
evaluated against Backstage’s own field-format validators at the commit ADR-0012
pins, and it is evaluated before entity-identity.md §1 runs.
The admissibility check
Section titled “The admissibility check”A descriptor is admissible when all of the following hold at the pinned commit’s semantics:
apiVersionis present and satisfiesisValidApiVersion.kindis present and satisfiesisValidKind.metadata.nameis present and satisfiesisValidEntityName.metadata.namespace, if and only if present, satisfiesisValidNamespace.
An omitted metadata.namespace is admissible; §1’s default substitution
applies afterwards, unchanged. The namespace is validated as authored, never
after defaulting.
Ordering
Section titled “Ordering”Admissibility is a precondition of §1, not a step inside it. §1’s two steps and its worked casing/namespace examples are untouched, and §3’s duplicate-collision table is untouched. No canonical identity is computed for an inadmissible descriptor, so no inadmissible descriptor can participate in a uniqueness comparison.
Failure semantics — fail-closed is preserved exactly, not relaxed
Section titled “Failure semantics — fail-closed is preserved exactly, not relaxed”An inadmissible descriptor aborts the entire operation with a non-zero
status and no usable partial output, under a new, distinct trigger class
inadmissible-descriptor, carrying the offending path, the failing field, and
the validator that rejected it.
Inadmissible descriptors are not filtered, skipped, excluded from the input set, or treated as non-entities. This record adds a failure class; it removes no failure. Every condition that aborts a run today still aborts a run.
Relationship to the existing collision rule
Section titled “Relationship to the existing collision rule”duplicate-canonical-id retains its exact current meaning: two or more
admissible descriptors canonicalizing to the same identity. Because
admissibility runs first, a corpus containing both inadmissible descriptors and a
genuine collision between valid entities will now report the inadmissible ones,
which is the earlier and more specific defect.
This narrows what duplicate-canonical-id reports; it does not make either
condition non-fatal. A corpus that fails today fails after this change too. The
report says something truer about why.
Options considered
Section titled “Options considered”Option A: Validate before canonicalizing; distinct fatal inadmissible-descriptor class (chosen)
Section titled “Option A: Validate before canonicalizing; distinct fatal inadmissible-descriptor class (chosen)”Preserves fail-closed exactly and corrects the classification. Strictly additive on the failure surface; removes no abort. The ordering — admissibility before canonicalization — is adrkit’s own design choice, made because a validity question and an identity question are different questions and the cheaper, per-descriptor one should not be answered by the more expensive cross-descriptor one. It is not claimed to mirror any ordering in Backstage; see “How these bindings were checked”.
Option B: Validate, then exclude inadmissible descriptors from the entity set
Section titled “Option B: Validate, then exclude inadmissible descriptors from the entity set”Rejected. This is the version that would let a corpus containing invalid
descriptors produce a populated snapshot. It converts a fatal condition into a
silent drop, directly contradicting ADR-0012’s owned-paths-fail-closed-atomic
assertion (“never a partial or silently-dropped set”). It is also the option
whose appeal comes from the result it produces rather than from the validator
evidence — which is precisely the reasoning this record must not adopt. If
descriptor exclusion is ever wanted, it needs its own record, its own
justification, and an explicit amendment to that assertion.
Option C: Validate, warn, and canonicalize anyway
Section titled “Option C: Validate, warn, and canonicalize anyway”Rejected. Keeps the wrong classification while adding noise. A descriptor that fails Backstage’s own field-format validators is not a lesser entity; by those validators’ own standard it is not an entity.
Option D: Exclude by path convention (e.g. scaffolder-template directories)
Section titled “Option D: Exclude by path convention (e.g. scaffolder-template directories)”Rejected. Path shape is not the property that makes a descriptor invalid, and the tracked record’s own evidence shows placeholder descriptors are not confined to a single directory convention. A path rule would be both unprincipled and incomplete.
Option E: Do nothing
Section titled “Option E: Do nothing”Rejected. The contract would go on knowingly canonicalizing input that Backstage’s own field-format validators reject, and go on mis-reporting at least one real, recorded condition. The gap is independent of whether feature009 ever executes again.
Trade-offs
Section titled “Trade-offs”What this buys. Classification precision at the identity boundary, and alignment with the validator semantics ADR-0012 already pinned but never applied. Failures name the defect that actually occurred.
On the governance metadata, since the combination is unusual. This record is
deliberately scope: org and blastRadius: org — equal in reach to ADR-0012,
ADR-0013 and ADR-0014 — while remaining two-way-door, where all three of those
are one-way-door. The asymmetry is intended, not an oversight.
The reach is org-wide because of what a misjudged admissibility rule does: it removes descriptors from the governed entity set entirely, so no ADR assertion ever fires against them. Entities do not appear mis-governed; they appear absent. That is precisely the failure mode ADR-0012 exists to prevent, and its consequence does not shrink just because this rule’s mechanism is a single field-format check. A record cannot coherently claim ARB review on the grounds that it amends an org-scope clause while describing its own blast radius as narrower than the clause it amends.
The reversibility is genuinely lower-risk, though, and that difference is worth
preserving rather than rounding to match the siblings. Admissibility can be
widened additively at any time — accepting a descriptor previously rejected
strictly enlarges the entity set and invalidates no prior output. What is not
freely reversible is the emitted inadmissible-descriptor trigger class, once
consumers branch on it. So: org-wide in what it can cost if wrong, two-way-door
in how hard it is to walk back.
What is given up:
- A public failure surface grows.
inadmissible-descriptorbecomes a contract-visible trigger class that consumers may branch on. Renaming or removing it later is a breaking change to the failure contract. This is the main reason the record istwo-way-doorrather than trivially reversible: the precondition itself is cheap to delete, but the emitted class is not. - A pinned-commit dependency becomes load-bearing. Admissibility is defined by Backstage’s validators at one frozen commit. If upstream relaxes or tightens a predicate, this contract silently diverges from live Backstage until the pin is deliberately moved. That divergence is already implicit in ADR-0012; this record makes it consequential.
- Strictly more corpora become unusable, not fewer. Because admissibility is fatal rather than filtering, descriptors that previously canonicalized without complaint — for example a name exceeding the 63-character limit that happened not to collide with anything — will now abort a run that previously completed. This record makes the contract stricter. Anyone reading it as a route to making more real-world corpora usable has read it backwards.
- It buys nothing for annotation-derivation confidence. Taken up below.
On what real-corpus evidence can and cannot establish
Section titled “On what real-corpus evidence can and cannot establish”A cross-check contributed during review holds that real corpora are 100%
annotation-absent — no public corpus carries adrkit.io/owned-paths, because
it is adrkit’s own proposed annotation — and therefore that real-corpus evidence
can never validate annotation-derivation itself, only enumeration and
determinism.
Agreed on the substance, with one qualification. The positive derivation
paths — explicit-paths and explicit-empty — are unreachable from any public
corpus by construction, and no volume of real-corpus data will ever exercise
them; only synthetic fixtures can. Nothing in this record changes that, and this
record should not be read as improving it.
The qualification is that “only enumeration and determinism” understates the
case slightly. A 100%-absent result is itself load-bearing: it exercises the
annotation-absent branch of the three-state discriminator at scale, and it
independently substantiates ADR-0012’s founding premise that no authoritative
entity-to-path binding exists in the wild. That is a real finding, not merely an
absence of one.
Why this matters for scoping this record honestly. Admissibility sits upstream of annotation handling entirely — it decides whether a descriptor is an entity at all, before any annotation is read. So its value lands squarely in the enumeration, classification, and determinism layer, which is the layer real corpora can speak to. That is a point in favour of deciding it on validator grounds. But it also means adopting this record moves the annotation-derivation evidence gap not at all. Any future claim that catalog binding is empirically validated must still rest on synthetic fixtures for the derivation paths, exactly as it did before.
This rule does not clear the corpora, and does not unblock feature009
Section titled “This rule does not clear the corpora, and does not unblock feature009”This is the most important limitation on this record, and it is stated here rather than buried in Consequences because it is the strongest available evidence about why this rule is being proposed.
Admissibility does not make the two pinned corpora usable. At least one
duplicate-canonical-id collision in community-plugins, pinned at
92e9e4e09c76cc57f3475029b73e5ec84498a459, is irreducible under any
admissibility rule of the kind proposed here. The file
workspaces/nexus-repository-manager/plugins/nexus-repository-manager/catalog-info.yaml
contains two YAML documents. Both are kind: Component; both declare
metadata.name: backstage-community-nexus-repository-manager; they differ only
in title, spec.type and spec.owner, with the second declaring itself as
its own subcomponentOf. That name is 44 characters of lowercase alphanumerics
and hyphens. It passes isValidObjectName cleanly, as does every other field on
both documents.
Both documents are therefore fully admissible. This is a genuine duplicate of a
valid entity — precisely the case entity-identity.md §3 exists to catch — and
the fail-closed abort is the correct outcome for it. No field-format rule can
reach it, and none should. It is also still present on the upstream default
branch, so re-pinning the corpus to a newer commit does not avoid it.
The consequence for feature009 is direct. spec.md SC-010 names all three
required real-corpus passes explicitly by corpus, so the community-plugins
pass cannot be substituted away. Adopting this record would reduce the number
of collisions in these corpora and would reclassify the placeholder descriptors
correctly; it would not produce a populated SnapshotEnvelope for
community-plugins, and it would not satisfy SC-010. The spike’s blocked
verdict stands, on its own merits, with or without this record.
Why this is stated so prominently. A rule of this kind invites the suspicion
that it was reverse-engineered from a desired verdict. The check for that
suspicion is whether the rule survives the discovery that it does not deliver
the convenient outcome. It does. The warrant for this record is the validator
binding at 1121a4fa… and nothing else: a descriptor whose metadata.name
Backstage’s own validator rejects is not an entity whose identity we should be
canonicalizing. That argument is unchanged by the fact that the corpora remain
unusable afterwards, which is the test it needed to pass.
Consequences
Section titled “Consequences”entity-identity.md§1 gains an admissibility precondition; §1’s own two steps and §3’s collision table are unchanged.- The atomic fail-closed clause ADR-0012 pins gains one trigger condition and loses none.
- Reports distinguish “this is not a valid Backstage entity” from “these valid entities collide.”
- The recorded feature009 verdict is unaffected by this record. It remains
blockedwithblockedShortfall = "envelope-or-scale-evidence-incomplete". Ratifying this record does not re-open, re-run, or re-decide that spike, and does not by itself make SC-010 satisfiable. - A residual, irreducible collision exists independent of admissibility. As
set out in Trade-offs,
community-pluginsat its pinned commit contains a genuine duplicate of a fully valid entity that no field-format rule can reach, and which is still present upstream. Adopting this record would not produce a populated envelope for that corpus. Anyone reading this record as a route to unblocking feature009 is reading it wrong. - The carry-forward
reference-oracle.jsonblocker recorded inspecs/009-catalog-binding-viability/checklists/evidence-index.mdis untouched and remains in full force: any future feature009 execution must still begin from a fresh T014 → T014a cycle before producing generator output. - No production catalog adapter is authorized, created, or implied. No
packages/adapters/catalog-*package exists or is sanctioned by this record.
Action items
Section titled “Action items”-
Confirm the validator bindings at the pinned commit before ratification.Done. All four rows are now verified by execution against the pinned sources, and the two defective rows were corrected. See “How these bindings were checked”. -
Obtain one independent review from a fresh context.Done — five independent reviews across two model lineages, all FAIL, every finding accepted and corrected. Every defect is listed above. Review 1 corrected substance — two validator-table rows and an Option A claim. No pass after it found anything wrong with the substance: bindings, predicates, executed regexes, counts, the Nexus duplicate and the governance metadata have stood unchanged since. What failed in passes 2 to 5 was prose precision, and latterly prose about the earlier failures, not the warrant.The maintainer should decide the ratification standard explicitly rather than inherit it. Three considerations, offered without a recommendation:
- Correction rounds have themselves introduced defects. Of the four claims traced for provenance, two were introduced during correction rounds rather than pre-dating the first review. So “review until clean” is not obviously convergent; each round has had some chance of adding what the next one finds.
- The later failures are concentrated in the review history, not the record’s substance. Passes 3 to 5 found defects in the account of the earlier failures — including, in Option E, one instance of a defect class that account had claimed was fully swept. A maintainer may reasonably weigh a defect in that section differently from a defect in the Decision.
- The largest identified defect class is enumerable and drained.
System-level claims about Backstage: eight instances existed at
4773d25, all eight corrected, verifiable by a whitespace-normalised sweep rather than by another reviewer’s judgement. A later pass reproduced that count independently.
This record does not presume which standard applies.
-
Only after ratification, prepare the corresponding
specs/009-**contract amendment as a separately-scoped change. Nospecs/009-**edit is authorized by this draft. -
Decide separately whether descriptor exclusion (Option B) is ever wanted. It is out of scope here and would require amending ADR-0012’s fail-closed assertion.