Skip to content

[finding] protocol.deletePackage has no declared spec shape — three hand-rolled types that disagree, and the runtime twin reaches it through as any #9960

Description

@huangyiirene

Measured while implementing #9846 (sourcing package-routes.ts's getMetaItems
option types from the spec). That card's body asserts "Both verbs are already
declared in packages/spec"
. Re-measured on origin/main @ 301bf26be1:
that is true of getMetaItems and false of deletePackage, so #9846 was
scoped to the getMetaItems half and this is the other half, recorded rather
than acted on — minting a protocol verb declaration is a contract act for the
spec seat, not a typing cleanup.

The measurement

DeletePackageRequest, DeletePackageResponse and DeletePackageRequestSchema
return zero hits across the whole repo (--include=*.ts, excluding
node_modules). deletePackage is also absent from the declared protocol
interface in packages/spec/src/api/protocol.zod.ts. The only spec-side
mentions are prose in two semantic-migration entries
(migrations/entries/semantic/17.package-uninstall-explicit-all-tenants.ts and
the registry), which describe the surface without declaring it.

Why it is worth a card: the three hand-rolled shapes DISAGREE

This is not only "an undeclared verb". Each site states its own shape, and they
are not the same shape.

1. The implementationpackages/metadata-protocol/src/protocol.ts:15379,
itself an inline structural type rather than a named one:

asyncdeletePackage(request: {packageId: string;organizationId?: string;allTenants?: boolean;actor?: string;keepData?: boolean;}): Promise<{success;deletedCount;failedCount;deleted;failed;cleanups}>

2. The REST direct-mount optionpackages/rest/src/package-routes.ts:256:

deletePackage?(req: {packageId: string;actor?: string;allTenants?: boolean}): Promise<>

It names neither organizationId nor keepData, and its response omits
deleted.

3. The runtime dispatcher twinpackages/runtime/src/domains/packages.ts:895,
which does not type the seam at all:

if(protocol&&typeof(protocolasany).deletePackage==='function'){persisted=await(protocolasany).deletePackage({packageId: id,
...(organizationId ? { organizationId } : {}),
...(keepData ? {keepData: true} : {}),});

So the runtime twin routinely sends exactly the two keys the REST option's type
cannot express, and the only reason that is not a compile error is the as any.

Not asserted: no defect today

The REST registrar calls deletePackage({ packageId, allTenants: true }) and
never passes organizationId or keepData, so no live call is currently
mistyped. What is recorded is that one verb has three independent statements of
its own contract, one of them cast away entirely — the exact blindness class
#9846 and #9805 were filed about, with no declared shape at the seam to name.

The tenant-scope semantics make it load-bearing rather than cosmetic: the
protocol refuses a call naming neither allTenants nor an organizationId
(TENANT_SCOPE_REQUIRED, 400), so organizationId is precisely the key whose
presence decides an uninstall's blast radius — and it is the key the REST
option's type does not name.

The judgement this does not make

Whether deletePackage should be declared in packages/spec (and if so,
whether it joins MetadataProtocol or a package-management protocol of its
own), or whether the three consumers should instead converge on one exported
type from @objectstack/metadata-protocol. Both are contract decisions for the
spec seat.

Repo: objectstack. Files: packages/spec/src/api/protocol.zod.ts (absence),
packages/metadata-protocol/src/protocol.ts:15379,
packages/rest/src/package-routes.ts:256,
packages/runtime/src/domains/packages.ts:895.
Related: #9846 (the getMetaItems half, implemented in #9959), #9805, #9741.


Generated by Claude Code

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions