Skip to content

A compound-name draft has no REST promotion door — POST /meta/:type/:name/publish is mounted in one arity while its read twin /published is mounted in both #11932

Description

@os-zhuang

Measured while implementing #11712 (threading ?mode=draft on the compound-name PUT /meta/:type/:section/:name). Separate surface, filed rather than folded: #11712 is about a write door dropping a query parameter, this is about a route that is never registered.

What was measured

Driven through the real registered server — new RestServer(...).registerRoutes(), then every route whose path contains publish, read off getRoutes():

POST  /api/v1/meta/:type/:name/publish
GET   /api/v1/meta/:type/:name/published
GET   /api/v1/meta/:type/:section/:name/published

Three routes, and the arities do not line up:

Verb single-segment compound
PUT (save / stage) mounted mounted
GET .../published (read the published snapshot) mounted mounted
POST .../publish (promote the draft) mounted not mounted

POST /api/v1/meta/object/crm/task/publish is four segments after /meta. /:type/:name/publish is three plus a literal, and /:type/:section/:name/published requires the literal published, not publish. No registered route matches it.

Why this is a real gap and not a shape the platform declined

The compound arity is not an accident on this surface — registerMetadataEndpoints mounts the published READ in both arities deliberately, and says why in its own comment:

Both arities, mirroring the getItem / saveItem twins: the SDK documents getPublished('lead', 'views/all_leads'), and a compound name is how every other read on this surface addresses a sub-resource.

And nothing below the route refuses a compound name. publishMetaItem takes { type, name } and keys the draft on type/name/organization/package; the name's spelling is never read as a shape. Driven directly against a seeded draft it promotes normally:

PROMOTE -> {"success":true,"version":"sha256:8d86f0c6…","seq":1,
            "message":"Published draft — type=object, name=crm_task [seq=1]"}

The consequence, once #11712 lands

Before #11712 the gap was unreachable: the compound PUT ignored ?mode=draft, so a compound-named draft could not be created through REST at all. After it, a caller can stage one — and then has no REST door to promote it. The draft is writable and readable and not publishable, over REST, by the same caller.

Workarounds that exist today, neither of which is the per-item door: POST /packages/:id/publish-drafts promotes a whole package's drafts at once, and the runtime dispatcher's own meta.publish verb is reachable without the REST route.

Where it would land

packages/rest/src/rest-server.ts — the POST ${metaPath}/:type/:name/publish registration, which would become a two-arity for loop over both path spellings exactly as the published read above it already is, with section folded into a compound name the same way. Registration ORDER matters here the way it does for /history, /audit and /published: Hono is first-match-wins and the three-segment form collides with GET /:type/:section/:name.

Related: #11712 (the write half of the same arity gap), #7526 (mounted the published read in both arities), #7019 (the twin-parity ruling this pair keeps being closed on).

Measured on origin/main at 1f6d04703.


Generated by Claude Code

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions