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.
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 —
resolveArtifactPackageOrder is 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.isArray silently 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/spec or in resolveArtifactPackageOrder'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: {} (and packages: 0, packages: 'x') in whichever direction is chosen.
⚠️ Sequencing: this is cheapest after the reader half lands, as part of the convergence pass — three private walks collapse then anyway, and re-spelling a guard twice is worse than once. Do not fold it into any open reader card.
Sibling context: #15005, #15006, #15007, #15229, #15232. Emitter half: #14512.
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.
The split
Two guards, same input, opposite answers:
packages: {}@objectstack/core's artifact-collection resolver (#15005 / PR #15261)if (!Array.isArray(artifact.packages)) return artifact;@objectstack/plugin-devdev-i18n.ts:108(#15232 / PR #15282)packages === undefined || packages === null→ return, else call the resolverresolveArtifactPackageOrder)@objectstack/plugin-securitydeclaredPermissionSets(#15007 / PR #15226)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/specquestion, not a reader'sBoth guards are defensible in isolation:
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.The tie-break is whether a non-array
packagesis malformed or absent, and that belongs besideAssembledPackageBodySchema/ArtifactPackageSchema, not in three readers.Acceptance
packages/specor inresolveArtifactPackageOrder's own documented contract.packages: {}(andpackages: 0,packages: 'x') in whichever direction is chosen.Sibling context: #15005, #15006, #15007, #15229, #15232. Emitter half: #14512.