From b685c67ad2aa9d61ccac8cec5e369f6361ed3de0 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 24 Sep 2026 00:32:22 +0000 Subject: [PATCH 1/3] docs(spec): split the install door's residual clause 1 into its type and version halves The PackageInstallBodySchema docblock recorded "a manifest missing `type` and/or `version`" as one class the install door answers 201 to. The door now parses ManifestSchema.shape.version by reference and answers 400 / VALIDATION_ERROR to a manifest missing `version`, so only the `type` half is still residual. - package-api.zod.ts (docblock only): clause 1 splits into 1a (`version`, marked closed, naming the door-side pin) and 1b (`type`, still open). The count sentence says class 1 stays open through its `type` half. The paragraph quoting the runtime's door drives quotes the duplicate-id drive's current body, which carries a `version`. - package-api.test.ts: the registry drive transcription matches the body the drive posts now. The residual list is hoisted and gains a pin that every body in it carries a declared-valid `version`, with a lit control. - The pending 18058 changeset restated the same clause; its sentence is split the same way so the next CHANGELOG entry is true. - New patch changeset for @objectstack/spec: files[] ships the docblock. Claude-Session: https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr Co-authored-by: Claude --- .../18058-install-door-contract-rebind.md | 2 +- .../19327-install-door-residual-split.md | 29 +++++++++++ packages/spec/src/api/package-api.test.ts | 52 ++++++++++++++----- packages/spec/src/api/package-api.zod.ts | 25 ++++++--- 4 files changed, 87 insertions(+), 21 deletions(-) create mode 100644 .changeset/19327-install-door-residual-split.md diff --git a/.changeset/18058-install-door-contract-rebind.md b/.changeset/18058-install-door-contract-rebind.md index c670fa2a2bc..9f6c751c005 100644 --- a/.changeset/18058-install-door-contract-rebind.md +++ b/.changeset/18058-install-door-contract-rebind.md @@ -25,7 +25,7 @@ The durable write happens on **both** arms, not just the disable. `POST /package **What the declaration does NOT cover — the measured residual** -This is a subset description of the live door, deliberately, and it is recorded rather than implied. Measured through `HttpDispatcher.handlePackages`, the door also answers `201` to: a manifest missing `type` and/or `version` (both of the runtime's own door drives post one); unknown keys on either form (refused by name on the bare branch, dropped on the wrapped one, `201` either way); a string-typed `enableOnInstall` / `overwrite`, which is compared against `true`/`false`/`'true'` and therefore treated as absent — `enableOnInstall: 'false'` installs ENABLED; and install options spelled on the bare form, which are ignored. In the opposite direction the door answers `400` to a whitespace-only `id` this declaration admits. `ManifestSchema` is not relaxed to close any of that. +This is a subset description of the live door, deliberately, and it is recorded rather than implied. Measured through `HttpDispatcher.handlePackages`, the door also answers `201` to: a manifest missing `type` (both of the runtime's own door drives post one; a manifest missing `version` was answered `201` too until #19326 made the door refuse it with `400` / `VALIDATION_ERROR`); unknown keys on either form (refused by name on the bare branch, dropped on the wrapped one, `201` either way); a string-typed `enableOnInstall` / `overwrite`, which is compared against `true`/`false`/`'true'` and therefore treated as absent — `enableOnInstall: 'false'` installs ENABLED; and install options spelled on the bare form, which are ignored. In the opposite direction the door answers `400` to a whitespace-only `id` this declaration admits. `ManifestSchema` is not relaxed to close any of that. **Documentation** diff --git a/.changeset/19327-install-door-residual-split.md b/.changeset/19327-install-door-residual-split.md new file mode 100644 index 00000000000..a24202781eb --- /dev/null +++ b/.changeset/19327-install-door-residual-split.md @@ -0,0 +1,29 @@ +--- +'@objectstack/spec': patch +--- + +`PackageInstallBodySchema`'s residual docblock splits clause 1: a manifest missing `version` is refused by the install door now, and only the `type` half is still residual + +Clause-②: no + +The docblock lists the bodies `POST /api/v1/packages` answers `201` to while +the declaration refuses them. Its clause 1 recorded "a manifest missing `type` +and/or `version`" as ONE class. Since #19326 the door parses +`ManifestSchema.shape.version` by reference, so a manifest missing `version` +answers `400` / `VALIDATION_ERROR` and installs nothing; a manifest missing +`type` still answers `201`. Half of the clause had become false. + +The clause is now split: **1a** (missing `version`) is marked CLOSED by #19326 +and names the door-side pin, and **1b** (missing `type`) stays an open residual. +The count of five classes is unchanged and still true, because class 1 stays +open through its `type` half; the count sentence now says so. The paragraph +that quotes the runtime's two door drives is updated too: the duplicate-id +drive has posted a `version` since #19326, and the docblock now quotes that body. + +⛔ No behaviour changes. No schema, accept set, export or runtime code moves, +and the other four residual classes are untouched. + +**Why this carries a changeset and not `skip-changeset`.** `@objectstack/spec`'s +`files[]` ships `src/**/*.zod.ts` verbatim, and the docblock is also emitted +into `dist/api/index.d.ts` and `dist/api/index.d.mts`. The published content +changes, even though no line of code does. diff --git a/packages/spec/src/api/package-api.test.ts b/packages/spec/src/api/package-api.test.ts index 20ab05ed743..e976bff69a0 100644 --- a/packages/spec/src/api/package-api.test.ts +++ b/packages/spec/src/api/package-api.test.ts @@ -919,14 +919,18 @@ describe('#18058 — install contract bound to the live door', () => { describe('the measured bare-form senders are the RESIDUAL, not green fixtures', () => { /** The `manifest` helper in `packages/runtime/src/package-door-namespace-conflict-code.test.ts` — no `type`. */ const DOOR_DRIVE_CONFLICT = { id: 'com.acme.crm', name: 'com.acme.crm', namespace: 'crm', version: '1.0.0' }; - /** The duplicate-id drive in `packages/runtime/src/domain-handler-registry.test.ts` — no `type`, no `version`. */ - const DOOR_DRIVE_REGISTRY = { id: 'pkg-a', name: 'A' }; + /** + * The duplicate-id drive in `packages/runtime/src/domain-handler-registry.test.ts` — no `type`. + * It posted no `version` either until PR #19326 made the door refuse that half (the docblock's + * clause 1a, CLOSED) and gave the drive a `version`; this is the body it posts now. + */ + const DOOR_DRIVE_REGISTRY = { id: 'pkg-a', name: 'A', version: '1.0.0' }; it('the namespace-conflict drive is REFUSED — it carries no `type`', () => { expect(PackageInstallBodySchema.safeParse(DOOR_DRIVE_CONFLICT).success).toBe(false); }); - it('the domain-handler-registry drive is REFUSED — no `type`, no `version`', () => { + it('the domain-handler-registry drive is REFUSED — it carries no `type`', () => { expect(PackageInstallBodySchema.safeParse(DOOR_DRIVE_REGISTRY).success).toBe(false); }); @@ -936,11 +940,11 @@ describe('#18058 — install contract bound to the live door', () => { expect(PackageInstallBodySchema.safeParse({ ...DOOR_DRIVE_CONFLICT, type: 'app' }).success).toBe(true); // ⭐ #17534 moved this half. `ManifestSchema.id` carries // `MANIFEST_ID_PATTERN` now, and `pkg-a` is not reverse-domain notation, - // so completing the missing keys is no longer sufficient for THIS drive — + // so completing the missing `type` is no longer sufficient for THIS drive — // it stays refused, on the id's shape rather than on an absent key. // ⛔ The remedy is to say that, not to relax the pattern: the drive posts // an id the registry face has always refused to publish. - const registryKeysCompleted = { ...DOOR_DRIVE_REGISTRY, version: '1.0.0', type: 'app' }; + const registryKeysCompleted = { ...DOOR_DRIVE_REGISTRY, type: 'app' }; expect(PackageInstallBodySchema.safeParse(registryKeysCompleted).success).toBe(false); // Lit control — the id is what decides it now: the same body with a // reverse-domain id parses green, so the refusal above is not the missing @@ -950,23 +954,45 @@ describe('#18058 — install contract bound to the live door', () => { }).success).toBe(true); }); + /** Bodies the door still answers `201` to while this declaration refuses them — the live residual. */ + const DOOR_201_RESIDUALS: ReadonlyArray> = [ + DOOR_DRIVE_CONFLICT, + DOOR_DRIVE_REGISTRY, + { ...SDK_MANIFEST, label: 'an unknown key on the bare form' }, + { ...SDK_MANIFEST, enableOnInstall: false }, + { manifest: SDK_MANIFEST, enableOnInstall: 'false' }, + { manifest: SDK_MANIFEST, overwrite: 'true' }, + ]; + it('the door answers 201 to all of them anyway — so this declaration is a SUBSET of the door', () => { // Pinned as prose-with-a-parse rather than a live HTTP drive: the door // lives in `@objectstack/runtime`, which this package cannot import. // `packages/runtime/src/domains/packages-install-enable-on-install.test.ts` // and the two drive files above are where the 201s are measured. - for (const residual of [ - DOOR_DRIVE_CONFLICT, - DOOR_DRIVE_REGISTRY, - { ...SDK_MANIFEST, label: 'an unknown key on the bare form' }, - { ...SDK_MANIFEST, enableOnInstall: false }, - { manifest: SDK_MANIFEST, enableOnInstall: 'false' }, - { manifest: SDK_MANIFEST, overwrite: 'true' }, - ]) { + for (const residual of DOOR_201_RESIDUALS) { expect(PackageInstallBodySchema.safeParse(residual).success).toBe(false); } }); + it('✅ clause 1a is CLOSED — every body in the live residual carries a `version` the declaration accepts', () => { + // PR #19326 made the door parse `ManifestSchema.shape.version` by + // reference: a manifest missing `version` answers `400` / + // `VALIDATION_ERROR` there now and installs nothing. The door-side pin is + // §1 of `packages/runtime/src/domains/packages-install-manifest-version.test.ts`, + // cited rather than repeated — this package cannot import the door. On + // that key the declaration and the door agree, so a versionless body in + // the list above would record a `201` the door no longer answers, which + // is what the registry drive's old transcription did. + // ⛔ Clause 1b stays open: both drives above still carry no `type`. + for (const residual of DOOR_201_RESIDUALS) { + const manifest = ('manifest' in residual ? residual.manifest : residual) as { version?: unknown }; + expect(ManifestSchema.shape.version.safeParse(manifest.version).success).toBe(true); + } + // Lit control — the check bites: a manifest with no `version` key reads + // `undefined` above, and the declaration refuses that. + expect(ManifestSchema.shape.version.safeParse(undefined).success).toBe(false); + }); + it('⭐ #17534 closed the one spelling that ran the OTHER way — a whitespace-only `id` is refused HERE now, not only by the door', () => { // What this pinned before: `handlePackages` trims before keying and // refuses an empty id, while this declaration ADMITTED `' '` — the one diff --git a/packages/spec/src/api/package-api.zod.ts b/packages/spec/src/api/package-api.zod.ts index c644667b664..82b7c16bf58 100644 --- a/packages/spec/src/api/package-api.zod.ts +++ b/packages/spec/src/api/package-api.zod.ts @@ -478,10 +478,12 @@ export type PackageInstallRequestParsed = z.infer Date: Thu, 24 Sep 2026 00:48:37 +0000 Subject: [PATCH 2/3] docs(spec): leave the pending 18058 release note untouched check-empty-changeset's foreign-changeset rule refuses a PR that modifies a pending changeset it did not add. Correcting another PR's pending release note is a release decision that needs a person to confirm it, so the restated clause in that note is reported to the seat instead of being rewritten here. The file is byte-identical to the merge base again. Claude-Session: https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr Co-authored-by: Claude --- .changeset/18058-install-door-contract-rebind.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/18058-install-door-contract-rebind.md b/.changeset/18058-install-door-contract-rebind.md index 9f6c751c005..c670fa2a2bc 100644 --- a/.changeset/18058-install-door-contract-rebind.md +++ b/.changeset/18058-install-door-contract-rebind.md @@ -25,7 +25,7 @@ The durable write happens on **both** arms, not just the disable. `POST /package **What the declaration does NOT cover — the measured residual** -This is a subset description of the live door, deliberately, and it is recorded rather than implied. Measured through `HttpDispatcher.handlePackages`, the door also answers `201` to: a manifest missing `type` (both of the runtime's own door drives post one; a manifest missing `version` was answered `201` too until #19326 made the door refuse it with `400` / `VALIDATION_ERROR`); unknown keys on either form (refused by name on the bare branch, dropped on the wrapped one, `201` either way); a string-typed `enableOnInstall` / `overwrite`, which is compared against `true`/`false`/`'true'` and therefore treated as absent — `enableOnInstall: 'false'` installs ENABLED; and install options spelled on the bare form, which are ignored. In the opposite direction the door answers `400` to a whitespace-only `id` this declaration admits. `ManifestSchema` is not relaxed to close any of that. +This is a subset description of the live door, deliberately, and it is recorded rather than implied. Measured through `HttpDispatcher.handlePackages`, the door also answers `201` to: a manifest missing `type` and/or `version` (both of the runtime's own door drives post one); unknown keys on either form (refused by name on the bare branch, dropped on the wrapped one, `201` either way); a string-typed `enableOnInstall` / `overwrite`, which is compared against `true`/`false`/`'true'` and therefore treated as absent — `enableOnInstall: 'false'` installs ENABLED; and install options spelled on the bare form, which are ignored. In the opposite direction the door answers `400` to a whitespace-only `id` this declaration admits. `ManifestSchema` is not relaxed to close any of that. **Documentation** From 0251069c51acf362b2219cced9dd24a9e7e0239c Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 24 Sep 2026 12:53:39 +0000 Subject: [PATCH 3/3] docs(spec): quote the registry door drive as it posts, com.example.pkg-a The install door's residual docblock, its pinning test and the changeset transcribed the duplicate-id drive in domain-handler-registry.test.ts as { id: 'pkg-a', name: 'A', version: '1.0.0' }. That drive stopped posting that body at PR #19473, which made the door parse the manifest's id leg and repaired the fixture's id to com.example.pkg-a. The old body is the REVERSED pin beside it and is answered 400. It had been filed as a 201 residual under clause 1b. Read at the merge base fdeeea0cc9 and at origin/main 2c1011b01b (the file is byte-identical at both): the drive posts { id: 'com.example.pkg-a', name: 'A', version: '1.0.0' }, answered 409 and then 201 on ?overwrite=true. The declaration refuses it on `type` alone. Clause 1b's "both door drives above" is true of that body, so 1b is unchanged. - package-api.zod.ts: the drives paragraph quotes the real body and credits both repairs, the version to PR #19326 and the id to PR #19473. - package-api.test.ts: DOOR_DRIVE_REGISTRY transcribes the real body. The control that rested on the stale id now asserts that the registry drive parses once `type` is added, like the conflict drive. The old `pkg-a` reading is kept, pointed at the old body: it is refused on the id alone. - changeset: the drives sentence names the body the drive posts. Prose and test only. No schema, accept set, export or runtime change. Claude-Session: https://claude.ai/code/session_019c3Hi6ZMU1p6m6aA6Bz45d Co-authored-by: Claude --- .../19327-install-door-residual-split.md | 8 +++- packages/spec/src/api/package-api.test.ts | 38 ++++++++++--------- packages/spec/src/api/package-api.zod.ts | 15 +++++--- 3 files changed, 35 insertions(+), 26 deletions(-) diff --git a/.changeset/19327-install-door-residual-split.md b/.changeset/19327-install-door-residual-split.md index a24202781eb..d97845e674e 100644 --- a/.changeset/19327-install-door-residual-split.md +++ b/.changeset/19327-install-door-residual-split.md @@ -17,8 +17,12 @@ The clause is now split: **1a** (missing `version`) is marked CLOSED by #19326 and names the door-side pin, and **1b** (missing `type`) stays an open residual. The count of five classes is unchanged and still true, because class 1 stays open through its `type` half; the count sentence now says so. The paragraph -that quotes the runtime's two door drives is updated too: the duplicate-id -drive has posted a `version` since #19326, and the docblock now quotes that body. +that quotes the runtime's two door drives is updated too. It quoted the +duplicate-id drive as `{ id: 'pkg-a', name: 'A' }`, but that drive has posted a +`version` since #19326 and the reverse-domain id `com.example.pkg-a` since +#19473, and the door answers `400` to `pkg-a`. The docblock now quotes the body +the drive posts, `{ id: 'com.example.pkg-a', name: 'A', version: '1.0.0' }`, +which the declaration refuses on `type` alone and the door answers `201`. ⛔ No behaviour changes. No schema, accept set, export or runtime code moves, and the other four residual classes are untouched. diff --git a/packages/spec/src/api/package-api.test.ts b/packages/spec/src/api/package-api.test.ts index e976bff69a0..37e99f04ca8 100644 --- a/packages/spec/src/api/package-api.test.ts +++ b/packages/spec/src/api/package-api.test.ts @@ -921,10 +921,13 @@ describe('#18058 — install contract bound to the live door', () => { const DOOR_DRIVE_CONFLICT = { id: 'com.acme.crm', name: 'com.acme.crm', namespace: 'crm', version: '1.0.0' }; /** * The duplicate-id drive in `packages/runtime/src/domain-handler-registry.test.ts` — no `type`. - * It posted no `version` either until PR #19326 made the door refuse that half (the docblock's - * clause 1a, CLOSED) and gave the drive a `version`; this is the body it posts now. + * Two of its keys were repaired, each by the PR that made the door parse that leg: PR #19326 + * gave it the `version` it lacked (the docblock's clause 1a, CLOSED), and PR #19473 replaced its + * id `pkg-a`, which `MANIFEST_ID_PATTERN` refuses. The old body is the REVERSED pin beside the + * drive, answered `400`, so ⛔ it is no residual and is not transcribed here. This is the body + * the drive posts: `409` first, then `201` on `?overwrite=true`. */ - const DOOR_DRIVE_REGISTRY = { id: 'pkg-a', name: 'A', version: '1.0.0' }; + const DOOR_DRIVE_REGISTRY = { id: 'com.example.pkg-a', name: 'A', version: '1.0.0' }; it('the namespace-conflict drive is REFUSED — it carries no `type`', () => { expect(PackageInstallBodySchema.safeParse(DOOR_DRIVE_CONFLICT).success).toBe(false); @@ -934,24 +937,22 @@ describe('#18058 — install contract bound to the live door', () => { expect(PackageInstallBodySchema.safeParse(DOOR_DRIVE_REGISTRY).success).toBe(false); }); - it('the missing keys are what decide it — and since #17534 the registry drive needs its id repaired too', () => { + it('the missing `type` is what decides it, for BOTH drives — the registry drive\'s old id is refused on its own', () => { // The control that makes the two refusals above a measurement of the // MANIFEST's required keys rather than of the bare branch existing at all. expect(PackageInstallBodySchema.safeParse({ ...DOOR_DRIVE_CONFLICT, type: 'app' }).success).toBe(true); - // ⭐ #17534 moved this half. `ManifestSchema.id` carries - // `MANIFEST_ID_PATTERN` now, and `pkg-a` is not reverse-domain notation, - // so completing the missing `type` is no longer sufficient for THIS drive — - // it stays refused, on the id's shape rather than on an absent key. - // ⛔ The remedy is to say that, not to relax the pattern: the drive posts - // an id the registry face has always refused to publish. const registryKeysCompleted = { ...DOOR_DRIVE_REGISTRY, type: 'app' }; - expect(PackageInstallBodySchema.safeParse(registryKeysCompleted).success).toBe(false); - // Lit control — the id is what decides it now: the same body with a - // reverse-domain id parses green, so the refusal above is not the missing - // keys coming back. - expect(PackageInstallBodySchema.safeParse({ - ...registryKeysCompleted, id: 'com.acme.pkg-a', - }).success).toBe(true); + expect(PackageInstallBodySchema.safeParse(registryKeysCompleted).success).toBe(true); + // ⭐ Until PR #19473 this half ran the other way. The drive posted `pkg-a`, + // which `MANIFEST_ID_PATTERN` has refused since #17534 (PR #18319), so + // completing its keys was not sufficient and this case pinned the + // refusal. PR #19473 made the door parse the id leg as well, so the door + // answers that body `400` (the REVERSED pin beside the drive), and it + // repaired the drive's id. ⛔ The old reading is kept, pointed at the old + // body: refused on the id alone (the lit control is the green parse just + // above, the same body with the drive's current id), so it is no part of + // the `201` residual. + expect(PackageInstallBodySchema.safeParse({ ...registryKeysCompleted, id: 'pkg-a' }).success).toBe(false); }); /** Bodies the door still answers `201` to while this declaration refuses them — the live residual. */ @@ -982,7 +983,8 @@ describe('#18058 — install contract bound to the live door', () => { // cited rather than repeated — this package cannot import the door. On // that key the declaration and the door agree, so a versionless body in // the list above would record a `201` the door no longer answers, which - // is what the registry drive's old transcription did. + // is what the registry drive's first transcription, `{ id: 'pkg-a', + // name: 'A' }`, did. // ⛔ Clause 1b stays open: both drives above still carry no `type`. for (const residual of DOOR_201_RESIDUALS) { const manifest = ('manifest' in residual ? residual.manifest : residual) as { version?: unknown }; diff --git a/packages/spec/src/api/package-api.zod.ts b/packages/spec/src/api/package-api.zod.ts index 82b7c16bf58..0675946a8cf 100644 --- a/packages/spec/src/api/package-api.zod.ts +++ b/packages/spec/src/api/package-api.zod.ts @@ -478,12 +478,15 @@ export type PackageInstallRequestParsed = z.infer