Skip to content

docs(adr): ADR-0028 — scope Backstage publication as a downstream consumer, tiered on the entity-ownership mapping - #149

Merged
mbeacom merged 10 commits into
mainfrom
adr-0028-backstage-publication-surface
Aug 16, 2026
Merged

docs(adr): ADR-0028 — scope Backstage publication as a downstream consumer, tiered on the entity-ownership mapping#149
mbeacom merged 10 commits into
mainfrom
adr-0028-backstage-publication-surface

Conversation

@mbeacom

@mbeacom mbeacom commented Aug 15, 2026

Copy link
Copy Markdown
Owner

Scopes the adrkit → Backstage publication surface. Ratified by @mbeacom after two independent four-lens deep reviews.

Review history

round findings blocking outcome
1 (draft) 26 5 structure unsound → full redraft
2 (redraft) 30 3 all closed → refined and ratified

Round 1 found the direction taxonomy leaked: answering "which decisions govern this component" needs an entity→path mapping, and getting it means reading adrkit.io/owned-paths off catalog entities — which is ingest. A consumer could satisfy every clause while doing the runtime catalog resolution ADR-0013 refused. The two-tier split fixes that and the unimplementable annotation-reuse clause together.

What round 2 caught

Two false claims inside the Enforcement posture — the section whose purpose is honesty about enforcement, which makes an overclaim there worse than the absence it discloses.

  • It cited ADR-0007's and ADR-0020's assertions as each pairing with "an identically named CI job." Verified false: ci.yml declares six jobs; core-has-no-adapter-deps is a step inside clean-clone-builds; and catalog-adapter-accept-path-needs-annotated-real-corpus pairs with nothing — ADR-0020's own clause 8 calls it "currently inert… never fails… records the rule; it does not enforce it." The record cited as precedent an assertion documented as enforcing nothing.
  • It claimed affects: surfaces the record for "the bound contracts." Clause 6 binds all of @adrkit/core's published API, but status/bucket.ts, parse/frontmatter.ts, load/corpus.ts and other index.ts re-exports match no pattern. Declaring all of core was rejected as noise, so the posture now states where detection is silent.

A second evasion route, same shape as the one the redraft closed. Clause 1 admitted "a path the caller supplies explicitly" with caller undefined — so a plugin could read backstage.io/source-location off a live entity, call it explicit, and reach entity-scoped governance inside Tier 1. Now: explicitly supplied means originating outside the plugin; any plugin-derived path is Tier 2; and where clauses 1 and 2 both admit a capability, clause 2 governs.

The release limit did not bind. The redraft's rule that unnumbered prose "binds nothing" unbound its own blockquote — the only place forbidding a release — so a conformant Tier 1 plugin could have shipped to npm. That two-class rule is dropped (it would also have unbound ADR-0007's and ADR-0013's mostly-unnumbered decisions), and the limit is now clause 10.

Clause 7 named a mechanism that does not exist. @adrkit/catalog-backstage exports only PACKAGE_NAME and is unreachable from outside the workspace by design, so the decode could not be "consumed." Tier 2 now consumes the envelope's already-normalized output and never needs the decode.

Also: clause 11 adds adrkit's reciprocal additive-only obligation on consumed shapes (ADR-0025 precedent) — no clause previously bound the producer; clause 9 assigns the document layer to Tier 1; clause 5 permits provisional out-of-repo work; the ADR-0007 adapter-regime narrowing is stated with action item 7 to land the amendment note; blastRadius team → org; relatesTo corrected; action item 2 gains an owner and date.

Honesty posture

status: accepted, authoredBy: agent-drafted, ratifiedBy: "@mbeacom" — both recorded, per ADR-0013's precedent that acceptance be stated rather than blurred. adr explain now reports it [accepted] and governing.

Not everything was fixed, and the record says so. Nothing mechanically enforces any clause; affects: detection is partial and the gap is named; reviewBy: 2027-02-15 has no mechanism once accepted (ratification removes it from adr queue); clause 8 is unenforceable across a repo boundary. These are recorded in Enforcement posture and action items 4, 7 and 8 rather than closed silently.

Two process disclosures: the community-plugin observations come from public docs, not from running it (action item 5), and in both rounds some lens reports arrived with a malformed frontmatter delimiter — adjudication failed closed, delimiters were repaired, originals retained in findings-as-returned/, and both panels re-ran to clean 4/4 ledgers.

Validation

adr lint: 28 records, 0 errors, 0 warnings. Typecheck clean. bun test 2134 pass / 0 fail. check:dco 4 signed. CI 12/12.

Note: the local branch name is a leftover from the PR #98 fix earlier in the same session; the remote branch and this PR are named correctly.

…sumer

ADR-0013 settled catalog composition for one direction of data flow,
Backstage -> adrkit: a standalone offline snapshot generator emitting an
interchange file the core consumes, with "the core still never learns an
adapter exists" as the binding invariant. Feature 010 implements that
direction.

The demand actually on the table runs the other way — adrkit-originated
ADRs surfaced inside Backstage — and no record in the corpus addresses
it. Absent one, the default reading is that ADR-0013 already covers
anything named "Backstage". That is a category error: 0013 constrains
what adrkit may load, not what a downstream consumer may do with
adrkit's published output. Two failure modes follow from the misreading
— a plugin landed under `packages/adapters/catalog-*/**` inherits the
offline-generator constraint by path, and `@backstage/*` enters a
dependency graph that ADR-0007's `core-has-no-adapter-deps` and
`clean-clone-builds` assertions keep clear.

Scopes the publication direction on four clauses: the dependency edge is
one-way (plugin -> adrkit, so 0013's invariant is preserved rather than
excepted); it lives outside the catalog glob, separate repository
preferred; it binds only to contracts already published at 0.7.0 — the
ADR schema, `@adrkit/core` types, and the CLI's JSON outputs — and not to
`@adrkit/catalog-envelope` or `@adrkit/catalog-backstage` while both sit
at 0.0.0 outside `RELEASE_PACKAGES`; and it authorizes work, not a
release, following ADR-0020's pattern.

Does not amend or supersede ADR-0013, which remains authoritative and
unchanged for the ingest direction.

Recorded honestly: `status: proposed`, `authoredBy: agent-drafted`, no
`ratifiedBy`. Per ADR-0013's own precedent against fabricating an
acceptance that never happened, it does not govern until ratified —
`adr explain` reports it under "Active proposals (not yet binding)".
The trade-offs section states the cost the chosen option pays and Option
B does not: cross-repository contract drift is unpoliced by any single
CI run.

Action item 4 records an unrelated defect found while tracing this:
`packages/adapters/catalog-backstage/README.md` on main says six modules
are "not implemented here" and that "no generator has run", while all
six exist in `src/`.

adr lint: 28 records, 0 errors, 0 warnings. typecheck clean, 2134 pass /
0 fail, check:doc-pins ok.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Mark Beacom <m@beacom.dev>
Copilot AI balanced review requested due to automatic review settings August 15, 2026 17:38

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Proposes ADR-0028 to distinguish adrkit-to-Backstage publication from the existing Backstage ingestion adapter architecture.

Changes:

  • Defines a one-way downstream plugin boundary.
  • Restricts integrations to published contracts.
  • Documents placement, release, and governance constraints.
Suppressed comments (1)

docs/adr/0028-scope-backstage-publication-as-a-downstream-consumer-outside-the-catalog-adapter.md:199

  • This repeats the version conflation from clause 3: the schema is published as schema version 0.1.0, not as a 0.7.0 package contract. The inventory criterion should distinguish the schema URL/version from the package and CLI release version; otherwise the action item cannot be completed accurately.
3. [ ] Inventory precisely what the plugin needs from adrkit and confirm each item is
   already published at `0.7.0`. Anything that is not on that list is the
   "how we would know this was wrong" signal firing before a line is written.

💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

@github-actions

github-actions Bot commented Aug 15, 2026

Copy link
Copy Markdown

Decisions governing this change

  • 0001 — Record architecture decisions as versioned markdown in git
    • via path: docs/adr/**
  • 0018 — Adopt MCP SDK v2 and serve protocol revision 2026-07-28 dual-era
    • via path: scripts/check-deps.ts
  • 0030 — Keep extension surfaces that carry a dependency tree outside this repository
    • via path: scripts/check-deps.ts

mbeacom and others added 2 commits August 15, 2026 13:51
Two corrections to the draft, both found while planning the surface it
scopes.

**The prior art was missing entirely.** `@backstage-community/plugin-adr`
already solves the document half — an entity ADR tab, a backend fetching
markdown through Backstage's URL readers, search indexing, New Frontend
System support, and extension points for file filtering, content
decoration, and status rendering. A record proposing a Backstage surface
that does not mention it invites rebuilding work that exists, and is the
first thing a reviewer would ask about. Added as context, as Option E,
and as an externalRef.

Two details make it closer to adrkit than expected: adrkit's
`NNNN-title-with-dashes.md` filenames match the plugin's own documented
custom-filter example, and its sample status component switches on
exactly the six statuses in `schema/adr.schema.json`. Whether its
MADR-oriented parser reads adrkit frontmatter acceptably is *not*
assumed — it is action item 4, to be observed and recorded either way.

Option E is marked **partially chosen** rather than rejected, because
clause 5 adopts it for the document layer. Listing it as refused would
misrepresent the decision.

**And the draft overstated the case.** It said feature 010's work does
"not move this direction forward". The packages do not, but ADR-0012's
`adrkit.io/owned-paths` annotation does: publication needs the same
entity-to-path mapping ingest consumes, in reverse. That is now stated as
shared ground and as an explicit decision not to invent a second
ownership contract.

Also adds clause 6 — resolution runs through `@adrkit/core`, never a
reimplementation — since re-deriving `affects:` matching downstream would
create the very drift the trade-offs section names as this option's main
cost, in the one place it is avoidable. The new third-party extension-
point dependency is recorded as a trade-off rather than left implicit.

Still `proposed`, still `agent-drafted`, still no `ratifiedBy`.

adr lint: 28 records, 0 errors, 0 warnings. 2134 pass / 0 fail.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Mark Beacom <m@beacom.dev>
…nomy leaked

A deep review (adversarial, architect, consumer, operator; 26 findings, 5
blocking, 4 verified against the tree) found the previous draft's central
structure unsound. This replaces it rather than patching, because the
defect was the structure.

**The direction taxonomy leaked, and the leak was exploitable.** The draft
said ADR-0013 governs Backstage -> adrkit and this record governs the
reverse. But answering "which decisions govern this component" needs an
entity-to-path mapping first, and obtaining it means reading
`adrkit.io/owned-paths` off catalog entities — which is ingest. So the
draft's own scope contained the direction it disclaimed, while asserting
it "does not narrow" ADR-0013. Verified reachable: `CatalogSnapshot` is
exported from `@adrkit/core` and `resolveAffects` accepts it via
`snapshots.catalog`, so a consumer could read live entities, build that
snapshot and resolve against it — satisfying every clause of the draft
while performing the dynamic runtime catalog resolution ADR-0013 refused,
and transporting a type ADR-0013 calls "not a wire format" across a
process boundary.

The fix is the tier split, and it closes two blocking findings at once.
Tier 1 (corpus-scoped: queue, graph, status, explicit-path governance)
needs no mapping and is authorized now. Tier 2 (entity-scoped) needs it,
is routed through ADR-0013's envelope by clause 7, and is explicitly NOT
authorized here. That also repairs the draft's unimplementable clause:
it mandated reusing ADR-0012's annotation while clause 3 forbade the only
packages implementing it — `grep -r 'adrkit.io/owned-paths'
packages/core/src` returns nothing; all four implementations sit inside
`@adrkit/catalog-backstage`.

**The clause-2 attribution was false.** The draft called it "load-bearing"
that `core-has-no-adapter-deps` and `clean-clone-builds` keep `@backstage/*`
out. Neither does: `TOOLKIT_DEPENDENCY` is `/^(@actions\/|@octokit\/|octokit$)/`,
`check-deps.ts:187` documents unknown packages as "silently unconstrained",
and `catalog-backstage` already ships `picomatch` and `yaml` with both
assertions green. Regrounded on Constitution Principle III's run-time
clause, which does plausibly reach a plugin needing a Backstage instance.
ADR-0007 Option C and its unmet revisit condition are now engaged rather
than passed over, and Option B's two invented costs are recorded as not
real: the allowlist is per-package, and `versioning: 'independent'`
already exists.

**And the count contradicted the enumeration** — "Four clauses, all
binding:" above six. No count is stated now, and the record says
unnumbered prose binds nothing.

Also: repository home deferred with named criteria and in-repo work
forbidden until decided (the draft offered a branch its own clause 2
foreclosed); clause 6 conditioned on ADR-0020 clause 9's undecided
release rather than an editable `0.0.0`; clause 9 gains ADR-0019's pin
discipline and names venue and acceptor; `affects:` extended to the
contracts the clauses actually bind — verified surfacing on
`packages/core/src/check/index.ts` and `scripts/release-pack.ts`, which
it previously missed; and a new Enforcement posture section states
plainly that nothing enforces this today and that CODEOWNERS is the only
real gate.

Still `proposed`, still `agent-drafted`, still no `ratifiedBy`.

adr lint: 28 records, 0 errors, 0 warnings. typecheck clean, 2134 pass /
0 fail, check:doc-pins ok.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Mark Beacom <m@beacom.dev>
@mbeacom mbeacom changed the title docs(adr): ADR-0028 — scope Backstage publication as a downstream consumer docs(adr): ADR-0028 — scope Backstage publication as a downstream consumer, tiered on the entity-ownership mapping Aug 15, 2026
A second independent four-lens panel over the redraft returned 30
findings, 3 blocking. All three are closed, plus every major finding
either fixed or explicitly accepted and recorded. Ratified by @mbeacom.

**Two false claims, both inside the Enforcement posture** — the section
whose entire purpose is honesty about enforcement, which makes an
overclaim there worse than the absence it discloses.

The first cited ADR-0007's and ADR-0020's assertions as each pairing with
"an identically named CI job." Verified false: ci.yml declares six jobs;
`core-has-no-adapter-deps` is a *step* ("Verify dependency boundaries")
inside `clean-clone-builds`; and
`catalog-adapter-accept-path-needs-annotated-real-corpus` pairs with
nothing — ADR-0020's own clause 8 calls it "currently inert", "never
fails", "records the rule; it does not enforce it". The record cited as
precedent an assertion documented as enforcing nothing. Now states the
accurate picture: exactly one of the three demonstrates the pattern.

The second claimed `affects:` surfaces this record for "the bound
contracts". Clause 6 binds all of `@adrkit/core`'s published API, but
`status/bucket.ts`, `parse/frontmatter.ts`, `load/corpus.ts` and the other
re-exports of `index.ts` match no declared pattern. Declaring all of core
was rejected as noise, so the posture now states where detection is
silent rather than implying it is complete.

**A second evasion route, same shape as the one the redraft closed.**
Clause 1 admitted "a path the caller supplies explicitly" with "caller"
undefined, so a plugin could read `backstage.io/source-location` off a
live entity, call it explicit, and reach entity-scoped governance inside
Tier 1. "Explicitly supplied" now means originating outside the plugin,
any plugin-derived path is Tier 2, and where clauses 1 and 2 both admit a
capability clause 2 governs.

**The release limit did not bind.** The previous draft's rule that
unnumbered prose "binds nothing" unbound its own blockquote, which was
the only place forbidding a release — so a conformant Tier 1 plugin could
have shipped to npm. That corpus-wide two-class rule is dropped (it also
would have unbound ADR-0007's and ADR-0013's mostly-unnumbered decisions),
and the limit is now clause 10.

**Clause 7 named a mechanism that does not exist.** It said the
`owned-paths` decode is "consumed, not copied", but
`@adrkit/catalog-backstage` exports only `PACKAGE_NAME`, declares no entry
point, and is unreachable from outside the workspace by design. Tier 2
consumes the envelope's already-normalized output and never needs the
decode.

Also: clause 11 adds adrkit's reciprocal additive-only obligation on the
consumed shapes, which no clause carried (ADR-0025 precedent); clause 9
assigns the document layer to Tier 1 and states its version range is empty
until the spike; clause 5 says Tier 1 work may begin out-of-repo as
provisional placement; the ADR-0007 adapter-regime narrowing is stated
rather than left implicit, with action item 7 to land the amendment note;
blastRadius team -> org to match every org-scope ARB record; relatesTo
corrected; action item 2 gains an owner and a date.

adr lint: 28 records, 0 errors, 0 warnings. typecheck clean, 2134 pass /
0 fail. `adr explain` now reports it [accepted] and governing.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Mark Beacom <m@beacom.dev>
@mbeacom
mbeacom marked this pull request as ready for review August 16, 2026 02:51
@mbeacom mbeacom self-assigned this Aug 16, 2026
mbeacom and others added 5 commits August 16, 2026 00:38
…ck wired

Resolves ADR-0028 action item 2. The deferral rested on an obstacle the
corpus had already dissolved.

**Principle III's run-time clause does not reach this plugin.** ADR-0028
asserted it "plausibly reaches a plugin that requires a running Backstage
instance." But `@adrkit/ci` lives at `packages/ci/`, ships at 0.1.0,
declares `@actions/github`, and at run time calls `getOctokit(token)`
against the authenticated GitHub REST API. If that clause reached what a
package talks to when a user runs it, `@adrkit/ci` would violate it
outright. It does not: the clause is read and enforced as build/test
hermeticity plus dependency confinement, which is exactly what
`CI_SURFACE_PACKAGE` and `TOOLKIT_DEPENDENCY` implement. A Backstage
plugin talking to a Backstage backend is the same shape as an Action
talking to GitHub.

**And ADR-0007's own revisit condition is unmet.** It rejected separate
repositories because they fragment "a project with one maintainer and no
users," to be revisited "once an adapter acquires independent
contributors." Separating now is premature by ADR-0007's own terms.

The costs ADR-0028 attributed to staying in-repo are already solved:
`isAdapterPackage()` is location-based and `packages/catalog-envelope/`
is the precedent for a non-adapter sited by location; `versioning:
'independent'` already exists for `@adrkit/spec-kit`; and `check-deps.ts`
is a per-package allowlist, so admitting a package weakens nothing
elsewhere.

Decision: `packages/backstage-plugin/`, confined on the `@adrkit/ci`
pattern — `@backstage/*` admitted there and forbidden everywhere else.

**Enforcement is wired now rather than deferred**, which is the point.
Siting the plugin here converts ADR-0028 clause 4 from prose into a
check; separation would have made it permanently unenforceable from this
repository. The rule keys on the `@backstage/` prefix across every
package rather than on the surface's allowlist entry, because
`allowedDependenciesFor` returns `undefined` for an unrecognized package
— which check-deps.ts itself documents as "silently unconstrained," so an
allowlist-only rule would pass a brand-new directory smuggling the SDK in.
That case is covered by a test.

Observed failing before counted as coverage (ADR-0016): with the rule
disabled, the two discriminating tests fail while the other 27 still
pass.

**ADR-0028's proposed narrowing of ADR-0007 is withdrawn** — `@adrkit/ci`
already establishes that a first-party integration surface may live
outside `packages/adapters/` under confinement, so no ADR-0007 clause is
narrowed. ADR-0028 action items 2 and 7 are closed by reference, 7 as not
required, following the mechanism ADR-0013 used on ADR-0007.

adr lint: 29 records, 0 errors, 0 warnings. typecheck clean, 2140 pass /
0 fail, check:deps and check:doc-pins pass.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Mark Beacom <m@beacom.dev>
This record's first draft decided in-repo. Measuring the dependency tree
reversed it, and the argument that justified in-repo turned out to be
overstated. Both are recorded rather than quietly replaced.

**The measurement.** Installing the plugin's plausible dependency set:
94 packages / 65 MB / 0 lifecycle scripts today, against 607 / 480 MB for
the runtime set alone and 1,274 / 1.0 GB / 2 lifecycle scripts with the
community ADR plugin and @backstage/cli. Even the floor — runtime deps
only, building with Bun per ADR-0010 — is 7.4x the disk and 6.5x the
packages. `clean-clone-builds` pays it cold on every pull request,
including documentation-only ones, so the measured 39s is a lower bound.
Confinement does not help: an allowlist bounds which package may import
the SDK, not what gets installed.

**The overstatement.** The first draft's decisive claim was that
separation makes ADR-0028 unenforceable. Broken down, exactly one
mechanism is lost — contract drift caught in a single CI run. Clause 8
was never mechanically checkable either way (a dependency graph cannot
detect re-derived `affects` matching), clause 11 governs this repository
regardless, and the SDK prohibition is *stronger* out-of-repo than the
confined form, because a blanket rule has no exception to get wrong.

**The generalization, which is the actually useful part.** The
distinguishing property was never plugin-ness. `@adrkit/spec-kit` is an
extension surface that belongs here precisely because it declares no
dependencies of any kind — asserted by its own packaging test — and ships
no dist. So the rule is: a surface may live here only if it adds no
install cost; otherwise it lives outside and consumes published
contracts. Future surfaces get placed by that rule instead of a
case-by-case argument.

The check flips from confinement to blanket prohibition
(`no-backstage-sdk-in-this-repository`), which is simpler and catches the
case an allowlist-keyed rule would wave through: `allowedDependenciesFor`
returns `undefined` for an unrecognized package, which check-deps.ts
itself documents as "silently unconstrained". Observed failing per
ADR-0016 — with the rule disabled all three discriminating tests fail
while the other 26 pass. One of them names `@adrkit/backstage-plugin`
specifically, since that is the single input the withdrawn confinement
rule would have allowed.

ADR-0007's Option C narrowing comes back, on better grounds than the
first draft's taxonomy argument: ADR-0007 weighed fragmentation when no
integration carried a dependency tree, and did not weigh one 15x the
repository it joins. Its stated revisit condition (independent
contributors) is still unmet and is not claimed.

The accepted loss is stated with mitigations rather than merely admitted:
the downstream repo pins and tests published @adrkit/* versions, and
action item 5 builds a conformance fixture so a breaking change is
detectable downstream. Without that fixture clause 5 is an intention, not
a control, and the record says so.

adr lint: 29 records, 0 errors, 0 warnings. typecheck clean, 2140 pass /
0 fail, check:deps ok.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Mark Beacom <m@beacom.dev>
ADR-0029 accepted (ratified by @mbeacom). ADR-0030 proposed, and it
amends ADR-0028 clauses 6 and 11 by reference, because clause 11 is
measurably false as ratified.

**The defect being fixed.** ADR-0028 clause 11 binds adrkit to
additive-only change on "the consumed shapes," defined as `@adrkit/core`'s
exported API. That surface is 173 symbols across 23 re-export lines. The
only existing consumer, `@adrkit/ci`, imports three. A promise of that
breadth resolves one of two ways: freeze the engine against ordinary
refactoring, or break the promise silently. The second is what happens,
and a governing clause routinely and invisibly violated is worse than
none. The SDK's primary purpose is making that promise keepable by
narrowing what it attaches to; reducing consumer boilerplate is
secondary.

**Why library-first rather than both.** Both modes already run here —
`@adrkit/ci` consumes the library, `spec-kit` shells out to `adr check`.
But the CLI's JSON is only sometimes a core shape: `queue` delegates to
`formatQueueReportJson` and `check` serializes `CheckOutcome`, while
`explain`, `lint`, and `new` assemble CLI-specific objects inline. So
"one interface, two adapters" would additionally require a normalization
layer for three commands plus a cross-mode equivalence property tested
forever — widening the very surface the SDK exists to narrow.

Process isolation, the honest argument for CLI-first, buys little here:
core depends on picomatch, semver, yaml and zod, so a library consumer
inherits almost nothing. The dependency asymmetry that pushed Backstage
out (1,274 packages) does not exist in reverse.

Both modes stay reachable. Clause 6 converges `explain`, `lint` and `new`
onto core formatters as hygiene, after which a CLI adapter is nearly
free — and the direction matters, because adding an adapter later is
additive while withdrawing one is a break.

The CLI JSON is kept as a documented sibling contract rather than an SDK
adapter, since its consumers are language-agnostic and want a stable
schema, not a JavaScript package.

Clause 4 has the SDK declare its own types rather than re-export core's:
a facade that re-exports is an alias and insulates nothing. That deliberate
duplication follows `packages/catalog-envelope/`, whose `//boundary` note
records the identical choice for the same reason.

Marked `one-way-door`: a published surface cannot be withdrawn once
depended on, only deprecated. Clause 8 therefore withholds the release
until a real consumer has exercised the surface, so the first shape is not
hardened against a hypothetical one.

adr lint: 30 records, 0 errors, 0 warnings. 2140 pass / 0 fail.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Mark Beacom <m@beacom.dev>
CI caught a real id collision that the local tree did not have: `main`
merged a different ADR-0028 (portable agent plugin) while this branch was
open, so `adr check` failed with `unique-id 0028 is used by multiple
records`. self-dogfood, clean-clone-builds and both node-smoke jobs went
red on it.

This is the risk flagged when 0028 was first scaffolded — the number had
already been used and reverted (#144/#145), so `adr new` offered it again
and a concurrent branch took it first. The corpus is right to fail closed
here; a duplicate id makes every `affects:` resolution ambiguous.

Renumbered highest-first through placeholders so 0028 -> 0029 could not
then be re-caught by the 0029 -> 0030 pass:

  0028 scope-backstage-publication...      -> 0029
  0029 keep-extension-surfaces...          -> 0030
  0030 publish-a-narrow-consumer-sdk...    -> 0031

Filenames, frontmatter ids, `# ADR-XXXX` headings, `relatesTo`, every
cross-link between the three, the ADR-0031 amendment note on ADR-0029,
and the ADR-0030 references in `scripts/check-deps.ts` and its test
(comment, violation reason string, and the describe block) all moved
together. Verified that no other record's number was touched: the only
ADR references remaining in the three files are 0003, 0007, 0010, 0012,
0013, 0014, 0016, 0019, 0020, 0025 plus the new 0029/0030/0031.

Ratification carries over unchanged — 0029 and 0030 remain accepted and
ratified by @mbeacom, 0031 remains proposed.

adr lint: 31 records, 0 errors, 0 warnings. typecheck clean, 2173 pass /
0 fail, check:deps ok. `adr check` over the branch diff now reports 0
changed-record errors, which is the failure CI reported.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Mark Beacom <m@beacom.dev>
@mbeacom
mbeacom merged commit 256a6f9 into main Aug 16, 2026
12 checks passed
@mbeacom
mbeacom deleted the adr-0028-backstage-publication-surface branch August 16, 2026 16:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants