- Status: done
- Date: 2026-07-02
- Specs touched:
docs/specs/CAPABILITIES.md(new — governing spec),docs/dev/capabilities.yml(new — the manifest),docs/dev/catalog-import-gaps.md(triage step wired to the manifest)
Published a machine-readable capabilities manifest so catalog curation stops depending on someone remembering which platform gaps have closed. When an app is withdrawn from the store because malmo lacks a feature, and that feature later ships, the app can graduate — but the only signal today is a human grepping the gap ledger. This makes the connection a data lookup.
docs/dev/capabilities.yml— a versioned list of the capabilities malmo has shipped. Each entry is{id, since, ref, summary}. Theidis the gap-class tag fromcatalog-import-gaps.md, reused verbatim, so the manifest, the ledger, and a curation record's blocker reference all speak one vocabulary — except for a documented partial-closure case (below). Seeded with the seven gap-classes/facets the ledger already marksimplemented/resolved/shipped:secret-injection,oneshot-job-restart(#92),managed-mysql(#108),managed-redis(#159),service-user(#116, #147 — the shipped facet ofnonroot-data-ownership),smtp-relay(#122),operator-env-config(#264).sinceis"0.x"for all of them — no brain version is cut yet (RELEASE_MANIFEST.md), soref(PR /DECISIONS.mddate / spec) carries the real provenance until releases are tagged. A top-levelversionis bumped on every list change so a consumer can tell how current the set it validated against was.docs/specs/CAPABILITIES.md— the governing spec: why the manifest exists, its shape, and the append-only discipline (a shipped capability never un-ships; append, never remove or renumber). It names the one nuance the ledger already carries — a gap-class can be partially closed (nonroot-data-ownershipshippedservice-userfor adopting images but the hardcoded-internal-UID facet still waits on the unshippeduserns-remap), so list only the capability that actually shipped, under its own id, with the mapping written into the ledger entry that names the shipped facet (thenonroot-data-ownership — poznoteentry now says whichcapabilities.ymlid its mechanism maps to) — otherwise the id-verbatim invariant silently breaks for exactly the case it's supposed to cover. Also notessincemust be a quoted YAML string.docs/dev/catalog-import-gaps.md— the triage instruction now says: when a ledger entry flips toimplemented/resolved, add its gap-class tocapabilities.ymland bumpversionin the same change, because that entry is what mechanically fires the re-screen; and, for a partial closure, coin a narrower id for the shipped facet instead of the gap-class tag, and record the mapping in the ledger entry. Turns the ledger's forgettable "grep for waiting apps" note into a data edit.
Realizes the freshness-coupling mechanism: the OS side publishes capabilities as gap-class-keyed data; a curation record names the gap-classes it waits on; a re-screen cross-check surfaces every app whose blockers are all shipped. This repo owns only the manifest and the discipline that keeps it honest — the cross-check consumer lives with the catalog curation records outside this repo, and this file names capabilities in OS terms only, one-directional.
sinceis"0.x"for every entry. No brain version is cut yet (RELEASE_MANIFEST.mddescribes the release manifest but no releases are tagged), so a real semver would be fiction.refcarries the honest provenance; backfillsincewhen releases start tagging.- No CLI emitter. The manifest is a plain checked-in YAML read directly, not a
malmo capabilitiessubcommand. It is data, not the manifest schema the brain enforces — the "delegate validation to the CLI" discipline (the seed-wire pattern) does not apply, and a plain file is readable by any consumer without building the Go binary. - Documentation-only change. No brain/host-agent/UI code moves; nothing enforces the manifest inside a box. It is a curation-tooling contract consumed downstream.
- The re-screen cross-check that consumes this manifest lives with the catalog curation records (the tracking item is off-repo). With the manifest as-is, the first app it should surface is one whose only blocker (
operator-env-config) already shipped in #264 — the mechanism's headline value: catching a graduation nobody re-screened. - Backfill
sincewith real brain semvers onceRELEASE_MANIFEST.mdreleases are tagged.