Repository navigation
finding(spec): the option-B readers disagree with @objectstack/core about whether a non-array packages is a refusal #15293
Description
Related #14122 · surfaced by the isolated contract review of #15282 (PR comment 5538108541), which flagged it as a program-level split rather than that card's fault. Filed unassigned.
Unblocked 2026-09-23T15:49Z: #17518 closed completed via PR #19373, which released the stack.zod.ts serial hold. The earlier Blocked-by: line is retired; see the unblock comment.
The split
Two guards, same input, opposite answers:
| Reader | Guard | packages: {} |
|---|---|---|
@objectstack/core's artifact-collection resolver (#15005 / PR #15261) |
if (!Array.isArray(artifact.packages)) return artifact; |
not a refusal — falls through to the top level |
@objectstack/plugin-dev dev-i18n.ts:108 (#15232 / PR #15282) |
packages === undefined || packages === null → return, else call the resolver |
refusal (ADR-0112 envelope out of resolveArtifactPackageOrder) |
@objectstack/plugin-security declaredPermissionSets (#15007 / PR #15226) |
same as plugin-dev | refusal |
So a stack carrying packages: {} boots one way through the runtime reader and is refused through the other two. Nothing today emits that shape, which is exactly why it is worth pinning before something does: the program's whole thesis is that readers must not disagree about what an artifact CONTAINS.
Which is right is a packages/spec question, not a reader's
Both guards are defensible in isolation:
- Refuse is the reader cards' stated rule —
resolveArtifactPackageOrderis the one traversal and the one gate, so a reader that answers questions about an artifact the loader would refuse is answering about nothing. Under this reading!Array.isArraysilently accepts a malformed artifact. - Fall through keeps the resolver total: it returns the caller's own object by identity for anything that is not a package list, and the refusal happens at the load boundary that owns it. Under this reading the other two readers refuse in a place with no diagnostic budget.
The tie-break is whether a non-array packages is malformed or absent, and that belongs beside AssembledPackageBodySchema / ArtifactPackageSchema, not in three readers.
Acceptance
- One answer, stated once, in
packages/specor inresolveArtifactPackageOrder's own documented contract. - All three readers above spell the guard the same way afterwards, or the divergence is recorded as deliberate with the reason at each site.
- A test pinning
packages: {}(andpackages: 0,packages: 'x') in whichever direction is chosen.
Sibling context: #15005, #15006, #15007, #15229, #15232. Emitter half: #14512.
Generated by Claude Code
Activity