From 32a9a89738f5888fab472bb71ba1bfd3cf6ccd00 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 22 Sep 2026 06:30:34 +0000 Subject: [PATCH 1/5] spec: enableOnInstall becomes optional() on all three declarations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The published declarations claimed `.default(true)` — "absent means enabled" — while the install door has honoured 「缺省 = 保持,有旗 = 设置」 since the runtime half landed: absent makes no lifecycle call, so a package an operator disabled stays disabled across a re-install. A `.default()` resolves absence at parse time, which erases the third state from the published surface and leaves a declared-but-unenforced default on a contract this repo does not own both ends of. `optional()` keeps the state visible; the `true` and `false` arms are unchanged. Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1 Co-authored-by: Claude --- packages/spec/src/api/package-api.zod.ts | 47 +++++++++++++++---- .../spec/src/kernel/package-registry.zod.ts | 24 +++++++--- .../spec/src/marketplace/marketplace.zod.ts | 17 ++++++- 3 files changed, 69 insertions(+), 19 deletions(-) diff --git a/packages/spec/src/api/package-api.zod.ts b/packages/spec/src/api/package-api.zod.ts index acb73653a4a..4607de3bcbc 100644 --- a/packages/spec/src/api/package-api.zod.ts +++ b/packages/spec/src/api/package-api.zod.ts @@ -369,23 +369,48 @@ export const PackageInstallRequestSchema = lazySchema(() => z.object({ /** * Whether to enable the package immediately after install. * + * ## ⭐ THREE STATES, and absence is one of them — that is why it is + * `optional()` and NOT `.default(true)` + * + * - `true` — the row is ENABLED after this install, existing or fresh. + * - `false` — the row is DISABLED after this install: present-but-not-active, + * and the disable survives a restart. + * - ABSENT — the row KEEPS ITS CURRENT LIFECYCLE STATE. No lifecycle call is + * made at all, so a package an operator disabled stays disabled across an + * upgrade or a re-install. A FRESH id has no state to keep and lands + * ENABLED, which is the registry's own new-row value, ⛔ not a default + * this declaration applies. + * + * ⭐ 「缺省 = 保持,有旗 = 设置」 — ruled in maintainer batch #157 item 5 + * letter C and implemented at the door (`packages/runtime/src/domains/packages.ts`), + * which reads the raw body and makes NO lifecycle call when the key is + * absent. The declaration followed in batch #210 item 4 letter A. + * + * ⛔ `.default(true)` is what this key may never go back to, and the reason + * is mechanical rather than stylistic: a default RESOLVES absence at parse + * time, so a parsed request that omitted the key becomes byte-identical to + * one that set `true`, and the third state stops existing on the published + * surface while the door still honours it — 「declared ≠ enforced」 on a + * contract this repo does not own both ends of. + * + * ⛔ Nor may the key be made to MEAN nothing in the name of making absence + * visible: the `true` and `false` arms are unchanged by that ruling and are + * re-read as such in `package-install-one-authority.test.ts`. + * * ## ⭐ THE ONE AUTHORITY for this key, and the map to the other two * * `enableOnInstall` is declared in three published schemas. This one is the * authority, because it is the request contract of the door that HONOURS it: - * `POST /api/v1/packages` writes the registry row's `enabled` from - * `enableOnInstall ?? true`, through the same registry flip and durable - * state write `PATCH /packages/:id/disable` uses - * (`packages/runtime/src/domains/packages.ts`). A `false` here installs the - * package present-but-not-active and survives a restart; `true` and absent - * install it enabled, which is this declaration's default. + * `POST /api/v1/packages` moves the registry row through the same registry + * flip and durable state write `PATCH /packages/:id/enable` and + * `PATCH /packages/:id/disable` use. * * The other two are re-read here so a reader never has to guess which of * three identical-looking declarations governs: * * - `InstallPackageRequestSchema` (`src/kernel/package-registry.zod.ts`) — * **a COPY of this key**, restated on the in-process protocol primitive - * `ObjectStackProtocol.installPackage`. Same type, same default, same + * `ObjectStackProtocol.installPackage`. Same type, same optionality, same * meaning; its own implementation does not read it, and this door does not * forward it down that seam. Held to this declaration by * `package-install-one-authority.test.ts`, not by an import: the authority @@ -396,13 +421,15 @@ export const PackageInstallRequestSchema = lazySchema(() => z.object({ * listing, its door is the control plane's `POST /api/v1/marketplace/install`, * and its `enableOnInstall` is what a caller asks the marketplace channel * to request on its behalf, one translation upstream of this one. It stays - * a declaration of its own and says why at its own site. + * a declaration of its own and says why at its own site. Its 缺省 cell moved + * with the other two so the matrix stays readable as one row per state, ⛔ + * not because the key was folded. * * ⛔ Never unify the three silently, in either direction: two of them are * one commitment and the third is a different party's. */ - enableOnInstall: z.boolean().default(true) - .describe('Whether to enable immediately after install — honoured at POST /api/v1/packages: the installed row\'s `enabled` is written from this key'), + enableOnInstall: z.boolean().optional() + .describe('Whether to enable immediately after install — honoured at POST /api/v1/packages: `true` enables the installed row, `false` disables it, and ABSENT keeps the row\'s current lifecycle state (a fresh install lands enabled)'), /** * Opt back in to overwriting an already-installed package id. diff --git a/packages/spec/src/kernel/package-registry.zod.ts b/packages/spec/src/kernel/package-registry.zod.ts index 64a17a8bc36..1383586a284 100644 --- a/packages/spec/src/kernel/package-registry.zod.ts +++ b/packages/spec/src/kernel/package-registry.zod.ts @@ -285,14 +285,24 @@ export const InstallPackageRequestSchema = lazySchema(() => z.object({ * ## A RESTATEMENT of the install-request key — the one authority is * `PackageInstallRequestSchema` in `src/api/package-api.zod.ts` * - * Same type, same default, same meaning: this is a COPY of the request key, - * not a second key that happens to share a spelling. The authority is the - * request contract bound to the door that actually serves — - * `POST /api/v1/packages`, which writes the registry row's `enabled` from - * `enableOnInstall ?? true` (`packages/runtime/src/domains/packages.ts`). + * Same type, same optionality, same meaning: this is a COPY of the request + * key, not a second key that happens to share a spelling. The authority is + * the request contract bound to the door that actually serves — + * `POST /api/v1/packages` (`packages/runtime/src/domains/packages.ts`). * ⛔ Never let the two drift: `src/api/package-install-one-authority.test.ts` * parses BOTH over one matrix and reds when they disagree on any cell. * + * ## ⭐ THREE STATES — absence is one, and it is not a default + * + * `true` enables the row, `false` disables it, and ABSENT keeps the row's + * current lifecycle state; a fresh id has no state to keep and lands + * ENABLED. 「缺省 = 保持,有旗 = 设置」, ruled in maintainer batch #157 item 5 + * letter C for the door and carried onto the declarations in batch #210 + * item 4 letter A. ⛔ This key is therefore `optional()` and never + * `.default(true)`: a default resolves absence at parse time, which erases + * the third state from the published surface while the door still honours + * it. + * * ## ⚠️ This contract's own implementation does not read the key * * This schema types the in-process protocol primitive @@ -321,8 +331,8 @@ export const InstallPackageRequestSchema = lazySchema(() => z.object({ * `PackageInstallRequestSchema.shape.enableOnInstall` — the pin above is the * mechanical half of the reference, and it is the half that can fail. */ - enableOnInstall: z.boolean().default(true) - .describe('Whether to enable immediately after install — restates the install-door request key, whose one authority is api/PackageInstallRequest; this protocol primitive does not read it'), + enableOnInstall: z.boolean().optional() + .describe('Whether to enable immediately after install — restates the install-door request key, whose one authority is api/PackageInstallRequest (`true` enables, `false` disables, ABSENT keeps the row\'s current lifecycle state); this protocol primitive does not read it'), /** * Current platform version for compatibility checking. * When provided, the system compares this against the package's diff --git a/packages/spec/src/marketplace/marketplace.zod.ts b/packages/spec/src/marketplace/marketplace.zod.ts index 0a9874458ec..5a3785f0818 100644 --- a/packages/spec/src/marketplace/marketplace.zod.ts +++ b/packages/spec/src/marketplace/marketplace.zod.ts @@ -520,9 +520,22 @@ export const MarketplaceInstallRequestSchema = lazySchema(() => z.object({ * control-plane contract that no PR in this repo can even see. * `src/api/package-install-one-authority.test.ts` pins the difference that * carries this reading, so a later fold cannot be a silent one. + * + * ## ⭐ Why the 缺省 cell moved here too, on a key that is NOT the door's + * + * `true` asks the channel to enable, `false` asks it not to, and ABSENT asks + * it to leave the package's lifecycle state where it is — a fresh install + * lands enabled. That is the same three-state reading the platform door was + * ruled onto (batch #157 item 5 letter C, carried onto the declarations by + * batch #210 item 4 letter A), and the consistency pin holds all three + * declarations to one row per state. ⛔ It is a matrix that moved, not a + * fold: what this request means by 「enable」 is still one translation + * upstream of the door, and a `.default(true)` here would re-introduce a + * value the caller never wrote — this time into a control-plane contract no + * PR in this repo can see the other end of. */ - enableOnInstall: z.boolean().default(true) - .describe('Whether to enable immediately after install — the marketplace channel\'s own install option, not the platform install-door key (api/PackageInstallRequest)'), + enableOnInstall: z.boolean().optional() + .describe('Whether to enable immediately after install — the marketplace channel\'s own install option, not the platform install-door key (api/PackageInstallRequest); `true` asks the channel to enable, `false` not to, and ABSENT leaves the package\'s current lifecycle state alone'), /** Artifact reference (resolved from listing version, or provided directly) */ artifactRef: ArtifactReferenceSchema.optional() From 7a039b65f1e7d49423f0731dfcddee897b830be0 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 22 Sep 2026 06:36:43 +0000 Subject: [PATCH 2/5] =?UTF-8?q?spec:=20flip=20the=20one-authority=20pin's?= =?UTF-8?q?=20=E7=BC=BA=E7=9C=81=20cell=20and=20declare=20the=20defaults?= =?UTF-8?q?=20move?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The #18605 consistency pin asserted `true` on every 缺省 (absent) reading, which was the behaviour this change moves. The cell is flipped with its registered flip-trigger phrase rather than patched green, so the next reader meets the reason instead of a silently edited expectation. Only the 缺省 cell moves. The `true` and `false` arms are re-read after the change on all three declarations — a fix that makes absence visible by making the key mean nothing would be worse than the defect. `check:authorable-surface` refuses an undeclared default move, so the four published def keys are declared in DEFAULT_CHANGES_BY_MAJOR. Four keys from three declarations: `InstallPackageRequestSchema` is re-exported through `src/api/protocol.zod.ts` and publishes under both `kernel/` and `api/`. Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1 Co-authored-by: Claude --- packages/spec/authorable-defaults/api.json | 2 - packages/spec/authorable-defaults/kernel.json | 1 - .../spec/authorable-defaults/marketplace.json | 1 - packages/spec/scripts/lib/default-changes.ts | 101 ++++++++++++++++++ packages/spec/src/api/package-api.test.ts | 10 +- .../api/package-install-one-authority.test.ts | 91 ++++++++++++++-- .../spec/src/kernel/package-registry.test.ts | 16 ++- .../spec/src/marketplace/marketplace.test.ts | 8 +- 8 files changed, 211 insertions(+), 19 deletions(-) diff --git a/packages/spec/authorable-defaults/api.json b/packages/spec/authorable-defaults/api.json index a0330e85e21..b0b39dc1982 100644 --- a/packages/spec/authorable-defaults/api.json +++ b/packages/spec/authorable-defaults/api.json @@ -87,7 +87,6 @@ "api/ImportValidationConfig:trimWhitespace = true", "api/InitiateChunkedUploadRequest:chunkSize = 5242880", "api/InitiateChunkedUploadRequest:scope = \"user\"", - "api/InstallPackageRequest:enableOnInstall = true", "api/ListExportJobsRequest:limit = 20", "api/ListFlowsRequest:limit = 50", "api/ListImportJobsRequest:limit = 50", @@ -129,7 +128,6 @@ "api/OpenApiGenerationConfig:version = \"3.0.3\"", "api/OpenApiSpec:openapi = \"3.0.0\"", "api/OpenApiSpec:servers = []", - "api/PackageInstallRequest:enableOnInstall = true", "api/PackageRollbackRequest:rollbackCustomizations = true", "api/PackageUpgradeRequest:createSnapshot = true", "api/PackageUpgradeRequest:dryRun = false", diff --git a/packages/spec/authorable-defaults/kernel.json b/packages/spec/authorable-defaults/kernel.json index e4997e515e6..c06defe52a9 100644 --- a/packages/spec/authorable-defaults/kernel.json +++ b/packages/spec/authorable-defaults/kernel.json @@ -46,7 +46,6 @@ "kernel/HotReloadConfig:preserveState = true", "kernel/HotReloadConfig:shutdownTimeout = 30000", "kernel/HotReloadConfig:stateStrategy = \"memory\"", - "kernel/InstallPackageRequest:enableOnInstall = true", "kernel/InstalledPackage:enabled = true", "kernel/InstalledPackage:status = \"installed\"", "kernel/KernelContext:features = {}", diff --git a/packages/spec/authorable-defaults/marketplace.json b/packages/spec/authorable-defaults/marketplace.json index 5c9679f4632..fc991f3ed77 100644 --- a/packages/spec/authorable-defaults/marketplace.json +++ b/packages/spec/authorable-defaults/marketplace.json @@ -3,7 +3,6 @@ "category": "marketplace", "defaults": [ "marketplace/ArtifactReference:format = \"tgz\"", - "marketplace/MarketplaceInstallRequest:enableOnInstall = true", "marketplace/MarketplaceListing:packageType = \"app\"", "marketplace/MarketplaceListing:pricing = \"free\"", "marketplace/MarketplaceListing:status = \"draft\"", diff --git a/packages/spec/scripts/lib/default-changes.ts b/packages/spec/scripts/lib/default-changes.ts index fa3dbead7ed..7fbd0dd9773 100644 --- a/packages/spec/scripts/lib/default-changes.ts +++ b/packages/spec/scripts/lib/default-changes.ts @@ -141,6 +141,79 @@ const AUTONUMBER_FORMAT_DEFAULT_REASON = + 'consumer reading `FieldParsed.autonumberFormat` still sees `undefined` when the ' + 'author omitted it, and asks `resolveAutonumberFormat` what that means.'; +/** + * Shared by the THREE platform-side `enableOnInstall` rows below. One edit to + * `PackageInstallRequestSchema` and one to `InstallPackageRequestSchema` move + * three published defaults, because the second schema is re-exported through + * `src/api/protocol.zod.ts` and therefore publishes under TWO def keys + * (`kernel/InstallPackageRequest` and `api/InstallPackageRequest`, byte-identical + * but for the `$id`) — the `CreateImportJobRequest` / `ImportRequest` shape + * above. The ratchet names keys, not schemas, so dropping any row leaves that + * def's default unauthorised and the gate red. + */ +const ENABLE_ON_INSTALL_PRESERVE_REASON = + 'The declared default was WRONG about the shipped runtime, and this row corrects the ' + + 'declaration rather than the behaviour — the `api/ImportRequest:runAutomations` shape, ' + + 'spec follows runtime. `POST /api/v1/packages` was ruled onto three states in maintainer ' + + 'batch #157 item 5 letter C — 「缺省 = 保持,有旗 = 设置」 — and implements them in ' + + '`packages/runtime/src/domains/packages.ts`: `enableOnInstall: true` calls `enablePackage`, ' + + '`false` calls `disablePackage`, and an ABSENT key makes NO lifecycle call at all, so the ' + + "row keeps whatever state it already had. A FRESH id has no state to keep and lands enabled " + + "— that is the registry's own new-row value, not a default this schema applies. The schema " + + 'said something else, in both machine-readable and human-readable form: `.default(true)` ' + + "shipped in `@objectstack/spec`'s JSON Schema for all three defs, and the `describe` prose " + + 'rendered into the published reference tables. ' + + 'NO deployed server behaviour moves here: nothing parses an install body through these ' + + 'schemas on the serving path — the door reads the raw body, and `PackageApiContracts` is a ' + + 'declarative catalog entry rather than a parse — so a request that omitted the key ' + + "preserved the row's state before this change and preserves it after. " + + 'The consumer who WAS affected, and who is the reason this is a correction rather than a ' + + 'cosmetic edit, lives outside this repo: a client or SDK that validates its request through ' + + 'the published schema materialised `enableOnInstall: true` from the declared default and ' + + 'SENT it explicitly. Under letter C an explicit `true` is a FORCE-ENABLE, so that caller ' + + 'silently re-enables a package an operator deliberately disabled — on every upgrade — while ' + + 'a non-validating caller sending the identical body preserves the disable. Identical request ' + + 'bodies, opposite behaviour, decided by whether the caller validated before sending. ' + + 'To keep an unconditional enable on every install, WRITE it — `enableOnInstall: true` — ' + + 'which is the only spelling the door has ever read as "enable". To get "leave this ' + + "package's lifecycle state where it is\", omit the key, which is now what the published " + + 'schema says absence means. `enableOnInstall: false` is unchanged in every respect. ' + + 'Reading a materialised `PackageInstallRequestParsed.enableOnInstall` (or the ' + + '`InstallPackageRequestParsed` copy) now yields `boolean | undefined` where it yielded ' + + '`boolean`; `undefined` is the third state, and it is the one the door acts on. ' + + 'No `semantic` migration entry accompanies this: these are REQUEST bodies, not stored ' + + 'metadata — no `sys_metadata` document carries the key, so `os migrate meta` has nothing to ' + + 'rewrite, the same reading as the two request-schema rows above. Maintainer ruling batch ' + + '#210 item 4 letter A, 2026-09-22, which explicitly refused the other direction (runtime ' + + 'back to default-on) because it re-enables a disabled package on re-install.'; + +const MARKETPLACE_ENABLE_ON_INSTALL_REASON = + 'The 缺省 cell moved on all three `enableOnInstall` declarations together, and this is the ' + + 'third — the one that is NOT the platform install door\'s key. This request names a ' + + 'marketplace LISTING and its door is the control plane\'s ' + + '`POST /api/v1/marketplace/install`, which resolves the artefact and validates the licence ' + + 'before mapping what it holds into a platform install; so this key is what a caller asks ' + + 'the MARKETPLACE to request on its behalf, one translation upstream of the door key. It is ' + + 'held to the same three states by the consistency pin in ' + + '`src/api/package-install-one-authority.test.ts`, whose matrix is one row per state across ' + + 'all three declarations: `true` asks the channel to enable, `false` asks it not to, and ' + + 'ABSENT asks it to leave the package\'s lifecycle state alone (a fresh install lands ' + + 'enabled). Keeping `.default(true)` here alone would have re-materialised, for the ' + + 'marketplace channel, exactly the value the platform door stopped applying — and done it ' + + 'inside a control-plane contract no PR in this repo can see the other end of, which is the ' + + 'worst place for a caller-invented value to live. ' + + 'Nothing in this repo changes behaviour: a runtime mounts `/api/v1/marketplace/*` only as a ' + + 'read-only proxy to the configured control plane (`MarketplaceProxyPlugin`), so no install ' + + 'body is parsed through this schema here at all. The consumer affected is the control-plane ' + + 'caller who validates through the published schema: to keep asking the channel for an ' + + 'unconditional enable, write `enableOnInstall: true`; to leave the package\'s state alone, ' + + 'omit the key; `false` is unchanged. Reading ' + + '`MarketplaceInstallRequestParsed.enableOnInstall` now yields `boolean | undefined` where it ' + + 'yielded `boolean`. ⛔ This is a matrix that moved, NOT a fold — the two requests remain ' + + 'separately owned on separate release cadences, and the pin still asserts that neither can ' + + 'be sent where the other is expected. Maintainer ruling batch #210 item 4 letter A, ' + + '2026-09-22.'; + export const DEFAULT_CHANGES_BY_MAJOR: Readonly> = { 17: [ { @@ -397,6 +470,34 @@ export const DEFAULT_CHANGES_BY_MAJOR: Readonly { - it('should accept a minimal install request', () => { + it('should accept a minimal install request — and leave an absent `enableOnInstall` UNDEFINED', () => { const result = PackageInstallRequestSchema.parse({ manifest: { id: 'com.acme.crm', @@ -162,7 +162,13 @@ describe('PackageInstallRequestSchema', () => { type: 'plugin', }, }); - expect(result.enableOnInstall).toBe(true); + // ⭐ [#19273] This assertion read `toBe(true)` while the declaration spelled + // `.default(true)`, and it was the lit control proving absence really was + // erased at parse time. The declaration is `optional()` now — 「缺省 = 保持, + // 有旗 = 设置」 — so the absence survives the parse and the door's three-way + // read has a third state to see. The full matrix, with the flip-trigger it + // was registered under, is in `package-install-one-authority.test.ts`. + expect(result.enableOnInstall).toBeUndefined(); }); it('should accept full install request with platform version', () => { diff --git a/packages/spec/src/api/package-install-one-authority.test.ts b/packages/spec/src/api/package-install-one-authority.test.ts index 8dd3ec204ee..204185961a4 100644 --- a/packages/spec/src/api/package-install-one-authority.test.ts +++ b/packages/spec/src/api/package-install-one-authority.test.ts @@ -24,6 +24,30 @@ * its door is the control plane's, not this platform's install door — so * what is pinned here is the difference that carries that reading, not the * sameness. + * + * ## ⭐ [#19273] THE 缺省 CELL FLIPPED — and this is the registered reason + * + * When this file was written, every 缺省 (absent) reading below was `true`, + * because all three declarations spelled `z.boolean().default(true)`. They now + * spell `z.boolean().optional()` and the 缺省 readings are `undefined`. + * + * ⛔ That is a FLIP, not a repair, and it was expected on the day this pin + * landed — {@link FLIP_TRIGGER} is the phrase this file registered in advance + * so the next reader meets the reason instead of a silently edited + * expectation. The trigger fired: the install door was ruled onto + * 「缺省 = 保持,有旗 = 设置」 (maintainer batch #157 item 5 letter C) and stopped + * making any lifecycle call on an absent key, which left the declarations + * claiming a default the runtime deliberately no longer applies. The + * declarations followed in maintainer batch #210 item 4 letter A. + * + * ⭐ What did NOT move, and is re-read below precisely because of that: the + * `true` and `false` arms. A fix that makes absence visible by making the key + * mean nothing would be worse than the defect it closes, so both booleans are + * asserted after the flip rather than assumed to have survived it. + * + * ⛔ Only the 缺省 cell moved. The string and `null` cells still refuse, and + * the authority/copy agreement is still judged cell by cell — the flip is one + * row of the matrix, never a relaxation of the matrix. */ import { describe, it, expect } from 'vitest'; @@ -39,23 +63,53 @@ const MANIFEST = { type: 'app', } as const; +/** + * ⭐ THE FLIP-TRIGGER, registered in this file rather than patched away. + * + * The phrase the install door was ruled onto. While the declarations spelled + * `.default(true)`, every assertion here read the 缺省 cell as `true` — and + * that reading was known to be living on borrowed time, because this phrase + * says absence is a state the door ACTS ON (by making no lifecycle call at + * all), and a `.default()` resolves absence at parse time so the state cannot + * survive to the published surface. + * + * So: a 缺省 reading of `undefined` below is this trigger having fired. It is + * spelled once, here, and quoted into the cell name so a test run prints it. + * ⛔ Reading a red on the 缺省 cell as "the pin needs updating" and writing the + * new value in silently is the failure this const exists to prevent. + */ +const FLIP_TRIGGER = '缺省 = 保持,有旗 = 设置'; + /** * The matrix. Each cell is a body the two contracts must answer identically — - * absent (the default), both booleans, and the non-boolean spelling the door - * itself treats as absent (recorded on `PackageInstallBodySchema`'s residual). + * absent (a state of its own, {@link FLIP_TRIGGER}), both booleans, and the + * non-boolean spelling the door itself treats as absent (recorded on + * `PackageInstallBodySchema`'s residual). */ const MATRIX: ReadonlyArray<{ name: string; enableOnInstall?: unknown }> = [ - { name: 'absent — the declared default applies' }, + { name: `absent — a state of its own, 「${FLIP_TRIGGER}」: no default resolves it` }, { name: 'false — install present, not active', enableOnInstall: false }, - { name: 'true — the default, spelled', enableOnInstall: true }, + { name: 'true — the enable request, spelled', enableOnInstall: true }, { name: "'false' — a string, refused by the declaration", enableOnInstall: 'false' }, { name: 'null — refused by the declaration', enableOnInstall: null }, ]; describe('#18605 — `enableOnInstall` has ONE authority', () => { describe('the authority: `PackageInstallRequestSchema`', () => { - it('defaults to `true` — the value the install door installs enabled on', () => { + it(`leaves an absent key \`undefined\` — 「${FLIP_TRIGGER}」, so the door still sees the absence`, () => { const parsed = PackageInstallRequestSchema.parse({ manifest: MANIFEST }); + expect(parsed.enableOnInstall).toBeUndefined(); + // ⛔ Not merely "not `true`": the key must be ABSENT-shaped after the + // parse, because the door's three-way read is `=== true` / `=== false` / + // neither. Any other resolved value would be a fourth state. + expect('enableOnInstall' in parsed).toBe(false); + }); + + it('carries `true` through — ⭐ re-read after the flip, not assumed to have survived it', () => { + // The control in the other direction: making absence visible by making + // the key mean nothing would be worse than the defect. This arm and the + // `false` one below are what say the key still means something. + const parsed = PackageInstallRequestSchema.parse({ manifest: MANIFEST, enableOnInstall: true }); expect(parsed.enableOnInstall).toBe(true); }); @@ -92,11 +146,21 @@ describe('#18605 — `enableOnInstall` has ONE authority', () => { }); } - it('declares the key with the same type and default, not merely the same name', () => { + it('declares the key with the same type and the same OPTIONALITY, not merely the same name', () => { + // ⭐ [#19273] This assertion used to read `typeof copyOnly === 'boolean'`, + // which only held while a `.default(true)` was resolving the absence. + // The flip-trigger removed that default from both declarations, so what + // is pinned now is that neither one invents a value — and the boolean + // half of the type is held by the spelled cells in the matrix above. const authorityOnly = PackageInstallRequestSchema.parse({ manifest: MANIFEST }).enableOnInstall; const copyOnly = InstallPackageRequestSchema.parse({ manifest: MANIFEST }).enableOnInstall; - expect(typeof copyOnly).toBe('boolean'); + expect(copyOnly).toBeUndefined(); expect(copyOnly).toBe(authorityOnly); + + // And the copy still carries a spelled value through, both ways — the + // same control the authority gets above. + expect(InstallPackageRequestSchema.parse({ manifest: MANIFEST, enableOnInstall: true }).enableOnInstall).toBe(true); + expect(InstallPackageRequestSchema.parse({ manifest: MANIFEST, enableOnInstall: false }).enableOnInstall).toBe(false); }); }); @@ -116,9 +180,18 @@ describe('#18605 — `enableOnInstall` has ONE authority', () => { expect(PackageInstallRequestSchema.safeParse({ listingId: 'com.acme.crm' }).success).toBe(false); }); - it('declares `enableOnInstall` in its own right, defaulting to `true`', () => { + it('declares `enableOnInstall` in its own right, and leaves an absent one `undefined` too', () => { + // ⭐ [#19273] The 缺省 cell moved here as well, so the matrix reads as one + // row per state across all three declarations. ⛔ Not a fold: what this + // request means by 「enable」 is still one translation upstream of the + // install door, which is what the two assertions above pin. const parsed = MarketplaceInstallRequestSchema.parse({ listingId: 'com.acme.crm' }); - expect(parsed.enableOnInstall).toBe(true); + expect(parsed.enableOnInstall).toBeUndefined(); + + // Re-read in the other direction here too — the key still means + // something on this request after the flip. + expect(MarketplaceInstallRequestSchema.parse({ listingId: 'com.acme.crm', enableOnInstall: true }).enableOnInstall).toBe(true); + expect(MarketplaceInstallRequestSchema.parse({ listingId: 'com.acme.crm', enableOnInstall: false }).enableOnInstall).toBe(false); }); }); }); diff --git a/packages/spec/src/kernel/package-registry.test.ts b/packages/spec/src/kernel/package-registry.test.ts index 70038c6b44d..842b40a715a 100644 --- a/packages/spec/src/kernel/package-registry.test.ts +++ b/packages/spec/src/kernel/package-registry.test.ts @@ -190,9 +190,21 @@ describe('InstallPackageRequestSchema', () => { expect(() => InstallPackageRequestSchema.parse(request)).not.toThrow(); }); - it('should apply default enableOnInstall', () => { + it('should leave an absent enableOnInstall UNDEFINED — it declares no default', () => { + // ⭐ [#19273] Was `should apply default enableOnInstall` ⇒ `true`. This + // declaration restates the install door's key, which was ruled onto + // 「缺省 = 保持,有旗 = 设置」: absence is a state the door acts on, so no + // default may resolve it away. The matrix that holds this copy equal to + // its authority is `src/api/package-install-one-authority.test.ts`. const parsed = InstallPackageRequestSchema.parse({ manifest: validManifest }); - expect(parsed.enableOnInstall).toBe(true); + expect(parsed.enableOnInstall).toBeUndefined(); + }); + + it('should still carry a spelled enableOnInstall through — both arms', () => { + // The control in the other direction: absence became visible without the + // key ceasing to mean anything. + expect(InstallPackageRequestSchema.parse({ manifest: validManifest, enableOnInstall: true }).enableOnInstall).toBe(true); + expect(InstallPackageRequestSchema.parse({ manifest: validManifest, enableOnInstall: false }).enableOnInstall).toBe(false); }); it('should accept install request with settings', () => { diff --git a/packages/spec/src/marketplace/marketplace.test.ts b/packages/spec/src/marketplace/marketplace.test.ts index e92aab0e3ab..7eb2e141b6b 100644 --- a/packages/spec/src/marketplace/marketplace.test.ts +++ b/packages/spec/src/marketplace/marketplace.test.ts @@ -323,12 +323,16 @@ describe('MarketplaceSearchResponseSchema', () => { }); describe('MarketplaceInstallRequestSchema', () => { - it('should accept minimal install request', () => { + it('should accept minimal install request — and leave an absent enableOnInstall UNDEFINED', () => { const request = { listingId: 'listing-001', }; const parsed = MarketplaceInstallRequestSchema.parse(request); - expect(parsed.enableOnInstall).toBe(true); + // ⭐ [#19273] Was `toBe(true)`. The 缺省 cell moved on all three + // `enableOnInstall` declarations together so the consistency matrix in + // `src/api/package-install-one-authority.test.ts` stays one row per state. + // ⛔ Not a fold — this request's subject is still a marketplace listing. + expect(parsed.enableOnInstall).toBeUndefined(); }); it('should accept full install request with license', () => { From eb90d50a03360a2aa4994effc5ee873056e5af98 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 22 Sep 2026 06:44:07 +0000 Subject: [PATCH 3/5] spec: regenerate reference docs and add the minor changeset `gen:docs` reprojects the four published defs whose `describe` moved: the `(default: true)` cell is gone and the prose now states all three states. Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1 Co-authored-by: Claude --- .../enableoninstall-optional-preserve.md | 62 +++++++++++++++++++ content/docs/references/api/package-api.mdx | 4 +- content/docs/references/api/protocol.mdx | 2 +- .../references/kernel/package-registry.mdx | 2 +- .../references/marketplace/marketplace.mdx | 2 +- 5 files changed, 67 insertions(+), 5 deletions(-) create mode 100644 .changeset/enableoninstall-optional-preserve.md diff --git a/.changeset/enableoninstall-optional-preserve.md b/.changeset/enableoninstall-optional-preserve.md new file mode 100644 index 00000000000..e25b69e246f --- /dev/null +++ b/.changeset/enableoninstall-optional-preserve.md @@ -0,0 +1,62 @@ +--- +'@objectstack/spec': minor +--- + +fix(spec): `enableOnInstall` becomes `optional()` so absence survives the parse + +The install door was ruled onto three states — 「缺省 = 保持,有旗 = 设置」 — and +implements them: `enableOnInstall: true` enables the row, `false` disables it, +and an **absent** key makes no lifecycle call at all, so a package an operator +disabled stays disabled across an upgrade or a re-install. A fresh id has no +state to keep and lands enabled. + +The published declarations said something else. `z.boolean().default(true)` +resolves absence **at parse time**, so a request that omitted the key came out +of the parse byte-identical to one that set `true` — the third state did not +exist on the published surface, while the door went on acting on it. That is a +declared default the runtime deliberately stops applying, on a contract this +repo does not own both ends of. + +All three declarations now spell `z.boolean().optional()`, with the semantics +written on the field in the `describe` and the docblock: + +- `api/PackageInstallRequest` (`src/api/package-api.zod.ts`) — the authority. +- `kernel/InstallPackageRequest` (`src/kernel/package-registry.zod.ts`) — the + copy restated on the in-process protocol primitive. It is re-exported through + `src/api/protocol.zod.ts`, so it publishes under `api/InstallPackageRequest` + too: one declaration, two published defs. +- `marketplace/MarketplaceInstallRequest` — a different party's key on a + different door, moved with the others so the consistency matrix stays one row + per state. Not a fold. + +**Runtime behaviour is deliberately UNCHANGED**, and nothing in this repo starts +or stops being refused. Nothing parses an install body through these schemas on +the serving path — the door reads the raw body, and `PackageApiContracts` is a +declarative catalog entry rather than a parse. The accept set does not move +either: absent, `true` and `false` are accepted before and after, and a string +or `null` is refused before and after. + +### Migration: FROM → TO + +| FROM | TO | +| :--- | :--- | +| omitting the key and expecting an unconditional enable, because the schema said `default: true` | send `enableOnInstall: true` — the only spelling the door has ever read as "enable" | +| omitting it and expecting the package's current state to be left alone | change nothing; that is what the door already does, and now what is declared | +| sending `enableOnInstall: false` | unchanged in every respect | +| reading `PackageInstallRequestParsed.enableOnInstall` (or the `InstallPackageRequestParsed` / `MarketplaceInstallRequestParsed` copies) after parsing a body without the key | it now yields `undefined` instead of `true` — the third state, and the one the door acts on | +| reading the published JSON Schema's `default` keyword for this key | it is gone; the key is still `type: "boolean"` and still not `required` | + +**Who is actually affected:** a client or SDK outside this repo that validates +its request through the published schema and sends the **parsed** object. It +materialised `enableOnInstall: true` from the declared default and sent it +explicitly — and an explicit `true` is a force-enable, so that caller silently +re-enables a package an operator deliberately disabled, on every upgrade, while +a caller sending the identical body without validating preserves the disable. +Identical request bodies, opposite behaviour, decided by whether the caller +validated before sending. A caller that never parsed its own request body is +unaffected in every direction. + +The four moved published defaults are declared in +`DEFAULT_CHANGES_BY_MAJOR` (`packages/spec/scripts/lib/default-changes.ts`), +each with the consumer prescription above; `check:authorable-surface` prints +them in full on every build that accepts them. diff --git a/content/docs/references/api/package-api.mdx b/content/docs/references/api/package-api.mdx index d09cd420711..7233d76be74 100644 --- a/content/docs/references/api/package-api.mdx +++ b/content/docs/references/api/package-api.mdx @@ -495,7 +495,7 @@ Install package request | :--- | :--- | :--- | :--- | | **manifest** | `{ id: string; namespace?: string; defaultDatasource?: string; version: string; … }` | ✅ | Package manifest to install (AUTHORING stage: `objects` are glob patterns) | | **settings** | `Record` | optional | User-provided settings at install time | -| **enableOnInstall** | `boolean` | optional (default: `true`) | Whether to enable immediately after install — honoured at POST /api/v1/packages: the installed row's `enabled` is written from this key | +| **enableOnInstall** | `boolean` | optional | Whether to enable immediately after install — honoured at POST /api/v1/packages: `true` enables the installed row, `false` disables it, and ABSENT keeps the row's current lifecycle state (a fresh install lands enabled) | | **overwrite** | `boolean` | optional | Overwrite an already-installed package id instead of answering 409 Conflict | | **platformVersion** | `string` | optional | Current platform version for compatibility verification | | **artifactRef** | `{ url: string; sha256: string; size: integer; format?: Enum<'tgz' \| 'zip'>; … }` | optional | Artifact reference for marketplace installation | @@ -658,7 +658,7 @@ Install package request | :--- | :--- | :--- | :--- | | **manifest** | `{ id: string; namespace?: string; defaultDatasource?: string; version: string; … }` | ✅ | Package manifest to install (AUTHORING stage: `objects` are glob patterns) | | **settings** | `Record` | optional | User-provided settings at install time | -| **enableOnInstall** | `boolean` | optional (default: `true`) | Whether to enable immediately after install — honoured at POST /api/v1/packages: the installed row's `enabled` is written from this key | +| **enableOnInstall** | `boolean` | optional | Whether to enable immediately after install — honoured at POST /api/v1/packages: `true` enables the installed row, `false` disables it, and ABSENT keeps the row's current lifecycle state (a fresh install lands enabled) | | **overwrite** | `boolean` | optional | Overwrite an already-installed package id instead of answering 409 Conflict | | **platformVersion** | `string` | optional | Current platform version for compatibility verification | | **artifactRef** | `{ url: string; sha256: string; size: integer; format?: Enum<'tgz' \| 'zip'>; … }` | optional | Artifact reference for marketplace installation | diff --git a/content/docs/references/api/protocol.mdx b/content/docs/references/api/protocol.mdx index 88d647fc075..3bffb8a78cc 100644 --- a/content/docs/references/api/protocol.mdx +++ b/content/docs/references/api/protocol.mdx @@ -1910,7 +1910,7 @@ Install package request | :--- | :--- | :--- | :--- | | **manifest** | `{ id: string; namespace?: string; defaultDatasource?: string; version: string; … }` | ✅ | Package manifest to install | | **settings** | `Record` | optional | User-provided settings at install time | -| **enableOnInstall** | `boolean` | optional (default: `true`) | Whether to enable immediately after install — restates the install-door request key, whose one authority is api/PackageInstallRequest; this protocol primitive does not read it | +| **enableOnInstall** | `boolean` | optional | Whether to enable immediately after install — restates the install-door request key, whose one authority is api/PackageInstallRequest (`true` enables, `false` disables, ABSENT keeps the row's current lifecycle state); this protocol primitive does not read it | | **platformVersion** | `string` | optional | Current platform version for compatibility verification | ### Nested Shape: `InstallPackageRequest.manifest` diff --git a/content/docs/references/kernel/package-registry.mdx b/content/docs/references/kernel/package-registry.mdx index ce4b5b76eba..40cf920e0c2 100644 --- a/content/docs/references/kernel/package-registry.mdx +++ b/content/docs/references/kernel/package-registry.mdx @@ -184,7 +184,7 @@ Install package request | :--- | :--- | :--- | :--- | | **manifest** | `{ id: string; namespace?: string; defaultDatasource?: string; version: string; … }` | ✅ | Package manifest to install | | **settings** | `Record` | optional | User-provided settings at install time | -| **enableOnInstall** | `boolean` | optional (default: `true`) | Whether to enable immediately after install — restates the install-door request key, whose one authority is api/PackageInstallRequest; this protocol primitive does not read it | +| **enableOnInstall** | `boolean` | optional | Whether to enable immediately after install — restates the install-door request key, whose one authority is api/PackageInstallRequest (`true` enables, `false` disables, ABSENT keeps the row's current lifecycle state); this protocol primitive does not read it | | **platformVersion** | `string` | optional | Current platform version for compatibility verification | ### Nested Shape: `InstallPackageRequest.manifest` diff --git a/content/docs/references/marketplace/marketplace.mdx b/content/docs/references/marketplace/marketplace.mdx index 4bfecc61a83..913d9c2cc25 100644 --- a/content/docs/references/marketplace/marketplace.mdx +++ b/content/docs/references/marketplace/marketplace.mdx @@ -145,7 +145,7 @@ Install from marketplace request | **version** | `string` | optional | Version to install | | **licenseKey** | `string` | optional | License key for paid packages | | **settings** | `Record` | optional | User-provided settings at install time | -| **enableOnInstall** | `boolean` | optional (default: `true`) | Whether to enable immediately after install — the marketplace channel's own install option, not the platform install-door key (api/PackageInstallRequest) | +| **enableOnInstall** | `boolean` | optional | Whether to enable immediately after install — the marketplace channel's own install option, not the platform install-door key (api/PackageInstallRequest); `true` asks the channel to enable, `false` not to, and ABSENT leaves the package's current lifecycle state alone | | **artifactRef** | `{ url: string; sha256: string; size: integer; format: Enum<'tgz' \| 'zip'>; … }` | optional | Artifact reference for direct installation | | **tenantId** | `string` | optional | Tenant identifier | From b3ec2123c4afa44620851c161d91bffa749d1d67 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 22 Sep 2026 08:14:23 +0000 Subject: [PATCH 4/5] =?UTF-8?q?spec:=20correct=20the=20pin=20header=20?= =?UTF-8?q?=E2=80=94=20the=20flip-trigger=20was=20NOT=20pre-registered?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The header block claimed this file "registered in advance" the phrase the 缺省 cell would later flip on. Measured on the parent commit: the file carried no such phrase and no flip or trigger note of any kind — zero hits for 缺省 / 保持 / 有旗 / flip / trigger, with `MATRIX` and `absent` as the controls proving the file was read. The claim was false about this repository, and it contradicted the const's own docblock twenty lines below, which said correctly that the phrase is registered here. The const's version is the true one and is kept. The header now states that the trigger is registered WITH the flip rather than ahead of it, and records the gap as the lesson: a pin written against a contested cell needs its trigger when the pin lands, while the contest is known, not when the cell finally moves. Comment-only. No assertion, matrix cell, changeset or declaration moves. Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1 Co-authored-by: Claude --- .../api/package-install-one-authority.test.ts | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/packages/spec/src/api/package-install-one-authority.test.ts b/packages/spec/src/api/package-install-one-authority.test.ts index 204185961a4..206fdc422a5 100644 --- a/packages/spec/src/api/package-install-one-authority.test.ts +++ b/packages/spec/src/api/package-install-one-authority.test.ts @@ -31,15 +31,25 @@ * because all three declarations spelled `z.boolean().default(true)`. They now * spell `z.boolean().optional()` and the 缺省 readings are `undefined`. * - * ⛔ That is a FLIP, not a repair, and it was expected on the day this pin - * landed — {@link FLIP_TRIGGER} is the phrase this file registered in advance - * so the next reader meets the reason instead of a silently edited - * expectation. The trigger fired: the install door was ruled onto + * ⛔ That is a FLIP, not a repair: the install door was ruled onto * 「缺省 = 保持,有旗 = 设置」 (maintainer batch #157 item 5 letter C) and stopped * making any lifecycle call on an absent key, which left the declarations * claiming a default the runtime deliberately no longer applies. The * declarations followed in maintainer batch #210 item 4 letter A. * + * ⚠️ {@link FLIP_TRIGGER} is registered HERE, WITH the flip — ⛔ it was NOT + * pre-registered when this pin landed. Measured on the parent commit: this + * file carried the phrase nowhere, and no flip or trigger note of any kind + * (zero hits for 缺省 / 保持 / 有旗 / flip / trigger, against `MATRIX` and + * `absent` as the controls proving the file was read). The card's own protocol + * — 「本卡 pin 断言兄弟卡在改的行为 ⇒ 在用例内预登记翻转触发词」 — expected a + * pin asserting a behaviour a sibling card was already changing to carry the + * trigger in advance, and that did not happen. ⭐ The gap is left recorded + * rather than papered over, because a reader who learns the pre-registration + * was MISSED is better served than one told it happened: the lesson is that a + * pin written against a contested cell needs its trigger at the moment the pin + * lands, when the contest is known, not at the moment the cell finally moves. + * * ⭐ What did NOT move, and is re-read below precisely because of that: the * `true` and `false` arms. A fix that makes absence visible by making the key * mean nothing would be worse than the defect it closes, so both booleans are From 6584b9dcd72706e4ca9af7291424297fbc50461a Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 22 Sep 2026 09:44:02 +0000 Subject: [PATCH 5/5] spec: regenerate the reference pages from the merged tree MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Discharges the merge commit's os-regen deferral. The three pages both sides moved were restored to main's side by the regen-merge script's step 2 — the driver had merged them with exit 0 while silently dropping one side — and are re-derived here from a dist built on the merged source, never text-merged. Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1 Co-authored-by: Claude --- content/docs/references/api/package-api.mdx | 14 +++++++------- content/docs/references/api/protocol.mdx | 2 +- .../docs/references/kernel/package-registry.mdx | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/content/docs/references/api/package-api.mdx b/content/docs/references/api/package-api.mdx index 7233d76be74..dedc1f2a6ce 100644 --- a/content/docs/references/api/package-api.mdx +++ b/content/docs/references/api/package-api.mdx @@ -47,7 +47,7 @@ Installed package row whose manifest is the assembled package body | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **manifest** | `{ id: string; namespace?: string; defaultDatasource?: string; version: string; … }` | ✅ | The ASSEMBLED package body this row carries | +| **manifest** | `{ id: string; namespace?: string; defaultDatasource?: string; version: string; … }` | ✅ | The ASSEMBLED package body this row carries, at the stage the registry records it | | **status** | `Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>` | optional (default: `"installed"`) | Package state: installed, disabled, installing, upgrading, uninstalling, or error | | **enabled** | `boolean` | optional (default: `true`) | Whether the package is currently enabled | | **installedAt** | `string` | optional | Installation timestamp | @@ -89,7 +89,7 @@ Installed package row whose manifest is the assembled package body | **packaging** | `Enum<'bundled' \| 'manifest-deps'>` | optional | Dependency packaging strategy (ADR-0025 §3.3) | | **main** | `string` | optional | Entry module of a code-bearing plugin, relative to the plugin root; `os plugin build` bundles it and writes `dist/index.mjs` here in the compiled manifest (ADR-0025 §3.4) | | **integrity** | `Record` | optional | Per-file content digests of the plugin artifact (ADR-0025 §3.2) | -| **functions** | `any` | optional | Named handler functions, as they survived the record JSON projection | +| **functions** | `Record }> \| { name: string; handler?: string; packageId?: string; effect?: Enum<'pure' \| 'writes'> }[]` | optional | Named handler functions, lowered to the refs a JSON document carries | | **datasourceMapping** | `{ namespace?: string; package?: string; objectPattern?: string; default?: boolean; … }[]` | optional | Centralized datasource routing rules for packages/namespaces/objects | | **translations** | `Record; apps?: Record; messages?: Record; globalActions?: Record; … }>[]` | optional | I18n Translation Bundles | | **objectExtensions** | `{ extend: string; fields?: Record; label?: string; pluralLabel?: string; … }[]` | optional | Extensions to objects owned by other packages | @@ -113,7 +113,7 @@ Installed package row whose manifest is the assembled package body | **agents** | `{ name: string; label: string; avatar?: string; role: string; … }[]` | optional | AI Agents — platform-internal (ADR-0063 §2): the kernel ships exactly two (ask/build); third parties extend via skills, not agents | | **tools** | `{ name: string; label: string; description: string; parameters: Record; … }[]` | optional | AI Tool metadata records — optional refinement layer, never required: the default path is skills referencing platform tools or materialised action_`` tools (ADR-0109) | | **skills** | `{ name: string; label: string; description?: string; surface?: Enum<'ask' \| 'build' \| 'both'>; … }[]` | optional | AI Skills (reusable capability bundles — the third-party AI extension primitive, ADR-0063) | -| **hooks** | `any` | optional | Object lifecycle hooks, as they survived the record JSON projection | +| **hooks** | `{ name: string; label?: string; object: string \| string[]; events: Enum<'beforeFind' \| 'afterFind' \| 'beforeInsert' \| 'afterInsert' \| 'beforeUpdate' \| …>[]; … }[]` | optional | Object Lifecycle Hooks, as a JSON document carries them | | **mappings** | `{ name: string; label?: string; sourceFormat?: Enum<'csv' \| 'json' \| 'xml' \| 'sql'>; targetObject: string; … }[]` | optional | Data Import/Export Mappings | | **analyticsCubes** | `{ name: string; title?: string; description?: string; sql: string; … }[]` | optional | Analytics Semantic Layer Cubes | | **connectors** | `{ name: string; label: string; type: Enum<'saas' \| 'database' \| 'file_storage' \| 'message_queue' \| 'api' \| 'custom'>; description?: string; … }[]` | optional | External System Connectors. A provider-bound entry (has `provider`: openapi/mcp/rest) is materialized into a live, dispatchable connector at boot and referenced by flows via `connector_action`; credentials are `auth.credentialRef` references, never inline secrets. An entry with no `provider` is a catalog descriptor only (NOT dispatchable) — set `enabled: false` on deliberate descriptors. Unknown provider / unresolvable credentialRef / name conflict ⇒ hard boot error (ADR-0097). | @@ -208,7 +208,7 @@ Installed package row whose manifest is the assembled package body | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **manifest** | `{ id: string; namespace?: string; defaultDatasource?: string; version: string; … }` | ✅ | The ASSEMBLED package body this row carries | +| **manifest** | `{ id: string; namespace?: string; defaultDatasource?: string; version: string; … }` | ✅ | The ASSEMBLED package body this row carries, at the stage the registry records it | | **status** | `Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>` | optional (default: `"installed"`) | Package state: installed, disabled, installing, upgrading, uninstalling, or error | | **enabled** | `boolean` | optional (default: `true`) | Whether the package is currently enabled | | **installedAt** | `string` | optional | Installation timestamp | @@ -303,7 +303,7 @@ Installed package row whose manifest is the assembled package body | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **manifest** | `{ id: string; namespace?: string; defaultDatasource?: string; version: string; … }` | ✅ | The ASSEMBLED package body this row carries | +| **manifest** | `{ id: string; namespace?: string; defaultDatasource?: string; version: string; … }` | ✅ | The ASSEMBLED package body this row carries, at the stage the registry records it | | **status** | `Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>` | optional (default: `"installed"`) | Package state: installed, disabled, installing, upgrading, uninstalling, or error | | **enabled** | `boolean` | optional (default: `true`) | Whether the package is currently enabled | | **installedAt** | `string` | optional | Installation timestamp | @@ -345,7 +345,7 @@ Installed package row whose manifest is the assembled package body | **packaging** | `Enum<'bundled' \| 'manifest-deps'>` | optional | Dependency packaging strategy (ADR-0025 §3.3) | | **main** | `string` | optional | Entry module of a code-bearing plugin, relative to the plugin root; `os plugin build` bundles it and writes `dist/index.mjs` here in the compiled manifest (ADR-0025 §3.4) | | **integrity** | `Record` | optional | Per-file content digests of the plugin artifact (ADR-0025 §3.2) | -| **functions** | `any` | optional | Named handler functions, as they survived the record JSON projection | +| **functions** | `Record }> \| { name: string; handler?: string; packageId?: string; effect?: Enum<'pure' \| 'writes'> }[]` | optional | Named handler functions, lowered to the refs a JSON document carries | | **datasourceMapping** | `{ namespace?: string; package?: string; objectPattern?: string; default?: boolean; … }[]` | optional | Centralized datasource routing rules for packages/namespaces/objects | | **translations** | `Record; apps?: Record; messages?: Record; globalActions?: Record; … }>[]` | optional | I18n Translation Bundles | | **objectExtensions** | `{ extend: string; fields?: Record; label?: string; pluralLabel?: string; … }[]` | optional | Extensions to objects owned by other packages | @@ -369,7 +369,7 @@ Installed package row whose manifest is the assembled package body | **agents** | `{ name: string; label: string; avatar?: string; role: string; … }[]` | optional | AI Agents — platform-internal (ADR-0063 §2): the kernel ships exactly two (ask/build); third parties extend via skills, not agents | | **tools** | `{ name: string; label: string; description: string; parameters: Record; … }[]` | optional | AI Tool metadata records — optional refinement layer, never required: the default path is skills referencing platform tools or materialised action_`` tools (ADR-0109) | | **skills** | `{ name: string; label: string; description?: string; surface?: Enum<'ask' \| 'build' \| 'both'>; … }[]` | optional | AI Skills (reusable capability bundles — the third-party AI extension primitive, ADR-0063) | -| **hooks** | `any` | optional | Object lifecycle hooks, as they survived the record JSON projection | +| **hooks** | `{ name: string; label?: string; object: string \| string[]; events: Enum<'beforeFind' \| 'afterFind' \| 'beforeInsert' \| 'afterInsert' \| 'beforeUpdate' \| …>[]; … }[]` | optional | Object Lifecycle Hooks, as a JSON document carries them | | **mappings** | `{ name: string; label?: string; sourceFormat?: Enum<'csv' \| 'json' \| 'xml' \| 'sql'>; targetObject: string; … }[]` | optional | Data Import/Export Mappings | | **analyticsCubes** | `{ name: string; title?: string; description?: string; sql: string; … }[]` | optional | Analytics Semantic Layer Cubes | | **connectors** | `{ name: string; label: string; type: Enum<'saas' \| 'database' \| 'file_storage' \| 'message_queue' \| 'api' \| 'custom'>; description?: string; … }[]` | optional | External System Connectors. A provider-bound entry (has `provider`: openapi/mcp/rest) is materialized into a live, dispatchable connector at boot and referenced by flows via `connector_action`; credentials are `auth.credentialRef` references, never inline secrets. An entry with no `provider` is a catalog descriptor only (NOT dispatchable) — set `enabled: false` on deliberate descriptors. Unknown provider / unresolvable credentialRef / name conflict ⇒ hard boot error (ADR-0097). | diff --git a/content/docs/references/api/protocol.mdx b/content/docs/references/api/protocol.mdx index 3bffb8a78cc..04a73e91547 100644 --- a/content/docs/references/api/protocol.mdx +++ b/content/docs/references/api/protocol.mdx @@ -1910,7 +1910,7 @@ Install package request | :--- | :--- | :--- | :--- | | **manifest** | `{ id: string; namespace?: string; defaultDatasource?: string; version: string; … }` | ✅ | Package manifest to install | | **settings** | `Record` | optional | User-provided settings at install time | -| **enableOnInstall** | `boolean` | optional | Whether to enable immediately after install — restates the install-door request key, whose one authority is api/PackageInstallRequest (`true` enables, `false` disables, ABSENT keeps the row's current lifecycle state); this protocol primitive does not read it | +| **enableOnInstall** | `boolean` | optional | Whether to enable immediately after install — restates the install-door request key, whose one authority is api/PackageInstallRequest; this protocol primitive honours it on the registry row: `true` enables, `false` disables, and ABSENT keeps the row's current lifecycle state (a fresh install lands enabled) | | **platformVersion** | `string` | optional | Current platform version for compatibility verification | ### Nested Shape: `InstallPackageRequest.manifest` diff --git a/content/docs/references/kernel/package-registry.mdx b/content/docs/references/kernel/package-registry.mdx index 40cf920e0c2..d9638df2145 100644 --- a/content/docs/references/kernel/package-registry.mdx +++ b/content/docs/references/kernel/package-registry.mdx @@ -184,7 +184,7 @@ Install package request | :--- | :--- | :--- | :--- | | **manifest** | `{ id: string; namespace?: string; defaultDatasource?: string; version: string; … }` | ✅ | Package manifest to install | | **settings** | `Record` | optional | User-provided settings at install time | -| **enableOnInstall** | `boolean` | optional | Whether to enable immediately after install — restates the install-door request key, whose one authority is api/PackageInstallRequest (`true` enables, `false` disables, ABSENT keeps the row's current lifecycle state); this protocol primitive does not read it | +| **enableOnInstall** | `boolean` | optional | Whether to enable immediately after install — restates the install-door request key, whose one authority is api/PackageInstallRequest; this protocol primitive honours it on the registry row: `true` enables, `false` disables, and ABSENT keeps the row's current lifecycle state (a fresh install lands enabled) | | **platformVersion** | `string` | optional | Current platform version for compatibility verification | ### Nested Shape: `InstallPackageRequest.manifest`