Found while implementing #11063 (the durable half). Not fixed there — different producer, and the #11063 grading authorized only the durable read. Filed unassigned for triage.
The defect
packages/rest/src/package-routes.ts, the GET /api/v1/packages handler, merges two sources into one listing:
- the in-memory registry, read through
options.protocol.getMetaItems({ type: 'package' }) - the durable
sys_packages rows, read through packageService.list()
#11063 removed the swallowing catch around source 2. Source 1 still carries its own:
if(options.protocol&&typeofoptions.protocol.getMetaItems==='function'){try{constresult=awaitoptions.protocol.getMetaItems({type: 'package'});// …merge result.items into packagesMap…}catch{// Protocol unavailable — continue with database only}}So the exact ambiguity #11063 closed on the durable half is still open on the registry half: when getMetaItems throws, the door answers 200 with { packages, total } built from the database alone, and total is reported as a complete count either way. Nothing on the wire separates "these are all the packages" from "these are the packages I could still see". The durable-sourced entries keep source: 'database', which reads as provenance rather than as a warning that the registry half is absent.
This is the same standing family ruling — a read that could not happen must not be reported as a read that found nothing (#10965 · #10677 / PR #10788 · #10789 / PR #10964 · #11063).
Why it was deliberately left out of #11063
Two reasons, both worth carrying into triage rather than re-deriving:
Not measured here
Refs
#11063 · #10965 · #10677 / PR #10788 · #10789 / PR #10964 · ADR-0112
Found while implementing #11063 (the durable half). Not fixed there — different producer, and the #11063 grading authorized only the durable read. Filed unassigned for triage.
The defect
packages/rest/src/package-routes.ts, theGET /api/v1/packageshandler, merges two sources into one listing:options.protocol.getMetaItems({ type: 'package' })sys_packagesrows, read throughpackageService.list()#11063 removed the swallowing catch around source 2. Source 1 still carries its own:
So the exact ambiguity #11063 closed on the durable half is still open on the registry half: when
getMetaItemsthrows, the door answers200with{ packages, total }built from the database alone, andtotalis reported as a complete count either way. Nothing on the wire separates "these are all the packages" from "these are the packages I could still see". The durable-sourced entries keepsource: 'database', which reads as provenance rather than as a warning that the registry half is absent.This is the same standing family ruling — a read that could not happen must not be reported as a read that found nothing (#10965 · #10677 / PR #10788 · #10789 / PR #10964 · #11063).
Why it was deliberately left out of #11063
Two reasons, both worth carrying into triage rather than re-deriving:
GET /api/v1/packagesswallows a failed database read into a 200 registry-only answer — the caller cannot tell a partial listing from a complete one #11063's triage authorized exactly one change — stop absorbing thePackageService.list()failure so the producer's declared refusal reaches the client — and explicitly did not authorize widening the response contract. Extending the same edit to a second producer was outside it.PackageService.list()was taught to declare a refusal (SERVICE_UNAVAILABLE/ 503 with an ADR-0112 status+code on the error) by service-package answers "no such package" / "no packages installed" over a driver it never queried — its own normalizeRows maps a non-answering seam onto zero rows #10965, soGET /api/v1/packagesswallows a failed database read into a 200 registry-only answer — the caller cannot tell a partial listing from a complete one #11063 only had to stop swallowing it — the correct wire answer was already pinned by the producer.protocol.getMetaItemshas no such declared refusal here, so there is no existing envelope to let through, and deciding what this door should answer when the registry read fails is a real decision, not a mechanical removal.Not measured here
sendThrownErrorasGET /api/v1/packagesswallows a failed database read into a 200 registry-only answer — the caller cannot tell a partial listing from a complete one #11063 did, which turns an undeclared throw into a500 INTERNAL_ERROR; (b) teach the producer to declare a refusal first (the service-package answers "no such package" / "no packages installed" over a driver it never queried — its own normalizeRows maps a non-answering seam onto zero rows #10965 route) and then stop swallowing; (c) decide that a registry read failure genuinely is tolerable here and make the tolerance visible — which is a response-shape change, i.e. a contract decision.ifguard means an absentprotocolservice is a different, already-handled case; what is unmeasured is whether a presentgetMetaItemshas a reachable throw. If it does not, this is dead defensive code rather than a live ambiguity — and that finding would be worth recording on its own terms.GET /api/v1/packagesswallows a failed database read into a 200 registry-only answer — the caller cannot tell a partial listing from a complete one #11063:packages/runtime/src/domains/packages.tsserves itsGET /packagesfromregistry.getAllPackages()— a single in-memory read with no durable read attempted and no swallowing catch, so it has nothing of this shape to fix.Refs
#11063 · #10965 · #10677 / PR #10788 · #10789 / PR #10964 · ADR-0112