Repository navigation
Commit 13d5294
fix(runtime): POST /api/v1/packages parses the manifest
Fixes #19120
Clause-②: no (narrowing)
`POST /api/v1/packages` installed a manifest with no `version` and
answered `201`, while the published declaration for that door has always
required one. The door now parses that leg and refuses loudly.
## What changed
`PackageInstallRequestSchema` binds `manifest: ManifestSchema`, and
`ManifestSchema` declares `version` required with a semantic grammar.
The door parsed nothing at all — `const manifest = body.manifest ||
body` went straight to `installPackage`, with an id check as the only
gate on the way.
The install branch now asks the declaration **by reference** and refuses
with `400` / `VALIDATION_ERROR` plus a prescription naming the key and
its shape.
```
ManifestSchema.shape.version.safeParse(manifest?.version)
```
Three properties of that spelling are load-bearing:
- **By reference, not by copy.** The `PATCH /packages/:id` door further
down the same file keeps its own hand-written semver regex. A second
copy here would be the third judgment of one key on one surface. Because
this door asks the declaration, whatever the open version-grammar canon
question settles on reaches it with no edit to this file.
- **No new error code.** `deps.error(msg, 400)` derives
`VALIDATION_ERROR` from `standardErrorCodeForHttpStatus(400)`, the
standard catalog's member for 400 — the same dialect the
repeated-query-parameter refusal on this route already answers in.
Nothing in `packages/spec` moves, and nothing in it was touched.
- **Ordered ahead of the duplicate-id 409.** A request-shape refusal
must not depend on server state. Placed after the 409, one and the same
under-specified body would answer `400` or `409` according to whether
that id happened to be installed already — two answers to one authoring
mistake. The id gate above it still wins, because with no id there is
nothing to name in the sentence this gate prints.
## The scope fence held — the five classes are SEPARABLE
The card is graded on the `version` leg alone. The declaration's
residual docblock records five classes this door answers `201` to; the
dispatch asked whether enforcing `version` forces the other four along.
**It does not.** The structural reading: the five are produced at three
different levels — a per-key field schema in `ManifestSchema.shape`
(this leg), the union arms' strict close (unknown keys, bare-form
options), and this handler's own `=== true` / `=== 'true'` comparisons
(the string-typed options). The single call that would close four at
once is `PackageInstallBodySchema.safeParse(body)`, and that call is
pointedly not made. `ManifestSchema.shape.version` is an independent
field schema that parses one value with no reference to any other key or
to the envelope.
Measured at this head, after the fix, driving the real door (one-shot
probe, not a committed test):
| class | case | status | installed |
|---|---|---|---|
| 1a **version** | wrapped, no `version` | **400** VALIDATION_ERROR | no
|
| 1a **version** | bare, no `version` | **400** VALIDATION_ERROR | no |
| 1a **version** | off-spec grammar | **400** VALIDATION_ERROR | no |
| 1b type | wrapped, no `type` | 201 | yes |
| 1b type | bare, no `type` | 201 | yes |
| 2 unknown keys | unknown key, wrapped form | 201 | yes |
| 2 unknown keys | unknown key, inside the manifest | 201 | yes |
| 2 unknown keys | unknown key, bare form | 201 | yes |
| 3 string-typed | `enableOnInstall: 'false'` | 201 (enabled) | yes |
| 3 string-typed | `overwrite: 'true'` on the body | 201 | yes |
| 4 bare-form options | `settings` on the bare form | 201 | yes |
| 5 whitespace id | id of spaces only | 400 (pre-existing) | no |
Only the `version` rows moved. The other four classes are untouched in
both directions, and **no committed test asserts anything about them** —
pinning them as `201` would freeze four known residuals as intended
behaviour and turn the card that closes one of them red for doing its
job.
Note for whoever takes the residual next: the docblock's clause 1 reads
"a manifest missing `type` and/or `version`", and after this PR only the
`type` half of it is still true. That text lives in `packages/spec`,
which is the `domain:spec` seat's — flagged in the report, not edited
here.
## Tests
`packages/runtime/src/domains/packages-install-manifest-version.test.ts`,
20 cases:
- **§0** asserts each fixture off-spec **by the declaration** before §1
holds the door to it. If the version-grammar canon moves, §0 goes red
first and names what happened, instead of §1 failing for a reason a
reader has to reconstruct.
- **§1** pins both halves a refusal owes: `400` + `VALIDATION_ERROR` +
`success: false` on the wire, **and** that neither install writer was
called. A door that answered `400` and installed anyway satisfies a
status assertion alone.
- **§2** is the positive control, on both body forms and both install
limbs, asserting the manifest reaches `installPackage` unchanged — this
is a gate, not a normaliser.
- **§3** pins the ordering, with the 409 itself as the control that the
409 was not simply removed.
**One fixture repaired outside the new file.**
`packages/runtime/src/domain-handler-registry.test.ts`'s duplicate-id
case drove the door with `{ id: 'pkg-a', name: 'A' }` — a body
`ManifestSchema` has never admitted. Its subject is the 409 guard and
the `?overwrite=true` bypass, not manifest completeness, so it gets the
`version` it never carried. The repair is owed whatever order the new
gate sits in: that case's `?overwrite=true` limb asserts `201`, which an
under-specified manifest must never reach, so no placement of the gate
leaves the fixture valid.
## Verification
Final head `0ccf7296d4`, after `git merge origin/main`.
- `pnpm --filter @objectstack/runtime test` — **269 files / 3725
passed**, 1 skipped, exit 0
- `pnpm --filter @objectstack/runtime typecheck` — exit 0
- `pnpm --filter '@objectstack/runtime...' build` — exit 0
- **58 of 58** derived gate families run, **0 NOT-MEASURED, 0 UNRUN**
(`dispatch-gates.mjs --ran`, exit codes captured before any pipe).
`check:dual-build-cjs-loads` answered `PREREQUISITE NOT MET` on the
pre-merge run and measures green here once its build closure existed.
- `pnpm lint` — the **full unnarrowed repo union** (`eslint .
--no-inline-config`), exit 0, at this head. Not a narrowed run, so no
narrowing is claimed.
- `check-adr-0087-registration` reads the changeset as
`[BREAKING+clause-②-narrowing]` carrying `not-required
(no-migration-prescription)`.
## Changeset
`minor`, declared breaking. Not `major`: the launch-window guard ships
breaking changes as minor while the stack versions in lockstep. The
ADR-0087 disposition is `not-required (no-migration-prescription)` —
nothing authorable is removed, renamed or reshaped, so `objectstack
migrate meta` has nothing to reach; there is no old spelling that maps
to a new one, only a key the declaration already required.
## Acceptance notes
- **The residual docblock is now half-stale** and needs the
`domain:spec` seat: clause 1 of `PackageInstallBodySchema`'s docblock
records `type` and/or `version` as one `201` class, and only `type`
remains. Reported, not edited — `packages/spec` is not this card's to
touch.
- **The four other residual classes** stay open and each needs its own
reading, per triage. Measured unchanged above.
- **The `PATCH /packages/:id` door keeps a hand-copied semver regex.**
Correct today and not a defect, so not filed — but it is the copy that
has to be found by hand if the version-grammar canon moves, while this
door follows the declaration on its own. Noted for whoever takes that
canon card.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01QCdUBjM47SxioST9z5Zwdf)_
---------
Co-authored-by: Claude <noreply@anthropic.com>version leg instead of installing anything it is handed (#19326)1 parent df0c856 commit 13d5294
4 files changed
Lines changed: 439 additions & 2 deletions
File tree
- .changeset
- packages/runtime/src
- domains
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
579 | 579 | | |
580 | 580 | | |
581 | 581 | | |
582 | | - | |
| 582 | + | |
| 583 | + | |
| 584 | + | |
| 585 | + | |
| 586 | + | |
| 587 | + | |
| 588 | + | |
| 589 | + | |
| 590 | + | |
| 591 | + | |
| 592 | + | |
583 | 593 | | |
584 | | - | |
| 594 | + | |
585 | 595 | | |
586 | 596 | | |
587 | 597 | | |
| |||
0 commit comments