From 829674095da9fa04fc15f6f965c552c2294f8ace Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 17:23:46 +0000 Subject: [PATCH 01/10] wip(packages): declare the installed-package row's source and the served writable verdict Claude-Session: https://claude.ai/code/session_01RGdjFAX2oq1VdGNT5WGdKN Co-authored-by: Claude --- .../src/marketplace-install-local-plugin.ts | 11 +++++-- packages/metadata-protocol/src/protocol.ts | 4 ++- packages/objectql/src/engine.ts | 26 +++++++++++++-- packages/objectql/src/plugin.ts | 9 ++++-- packages/objectql/src/registry.ts | 21 +++++++++++- packages/runtime/src/domains/packages.ts | 7 +++- .../services/service-package/src/index.ts | 5 ++- .../spec/src/api/package-api-assembled.zod.ts | 24 ++++++++++++-- .../spec/src/contracts/objectql-engine.ts | 9 ++++-- .../spec/src/kernel/package-registry.zod.ts | 32 +++++++++++++++++++ 10 files changed, 132 insertions(+), 16 deletions(-) diff --git a/packages/cloud-connection/src/marketplace-install-local-plugin.ts b/packages/cloud-connection/src/marketplace-install-local-plugin.ts index 7770ed0fd59..1e6d446df04 100644 --- a/packages/cloud-connection/src/marketplace-install-local-plugin.ts +++ b/packages/cloud-connection/src/marketplace-install-local-plugin.ts @@ -740,7 +740,7 @@ export class MarketplaceInstallLocalPlugin implements Plugin { if (entries.length === 0) return; - let manifestService: { register(m: any): void | Promise } | null = null; + let manifestService: { register(m: any, options?: { source?: 'code' | 'marketplace' }): void | Promise } | null = null; try { manifestService = ctx.getService('manifest') as any; } catch { @@ -777,7 +777,10 @@ export class MarketplaceInstallLocalPlugin implements Plugin { // the metadata service (late-registration bridge in // ObjectQLPlugin) — wait for that so metadata consumers see // the package as soon as rehydrate reports success. - await manifestService!.register(entry.manifest); + // [#23972] Every ledger entry is a marketplace install, so its + // registry row says so (`source: 'marketplace'`) after a restart + // exactly as it did when it was installed. + await manifestService!.register(entry.manifest, { source: 'marketplace' }); // Sync schemas so the driver creates tables for the newly- // registered objects (idempotent — already-synced tables // are no-ops). @@ -1551,7 +1554,9 @@ export class MarketplaceInstallLocalPlugin implements Plugin { // Awaited: register also bridges the manifest's objects into the // metadata service — a caller that reads metadata right after a // 200 (AI describe_object, Studio object list) must see them. - await manifestService.register(manifest); + // [#23972] The registry row records where this package came + // from: the marketplace (`source: 'marketplace'`). + await manifestService.register(manifest, { source: 'marketplace' }); } catch (err: any) { const registerError = String(err?.message ?? err); // For offline file imports we treat a register failure as a hard diff --git a/packages/metadata-protocol/src/protocol.ts b/packages/metadata-protocol/src/protocol.ts index 74fb23d7fcf..ead219b8790 100644 --- a/packages/metadata-protocol/src/protocol.ts +++ b/packages/metadata-protocol/src/protocol.ts @@ -31390,7 +31390,9 @@ export class ObjectStackProtocolImplementation implements // install REPLACES the row object (and may register the namespace). const undoInstall = this.packageInstallUndo(manifest); - let pkg = this.engine.registry.installPackage(manifest as any, request.settings); + // [#23972] Every package this primitive installs is AUTHORED — a Studio + // base, a duplicate, an HTTP install — persisted to `sys_packages` below. + let pkg = this.engine.registry.installPackage(manifest as any, 'authored', request.settings); // [#19277] HONOUR `enableOnInstall` — the key THIS request contract // declares and this primitive read past. `InstallPackageRequestSchema` diff --git a/packages/objectql/src/engine.ts b/packages/objectql/src/engine.ts index d45112dd186..337b4843974 100644 --- a/packages/objectql/src/engine.ts +++ b/packages/objectql/src/engine.ts @@ -98,7 +98,7 @@ import { resolveBundleLocale, SystemFieldName, } from '@objectstack/spec/system'; -import { ExecutionContext, ExecutionContextSchema } from '@objectstack/spec/kernel'; +import { ExecutionContext, ExecutionContextSchema, type InstalledPackageSource } from '@objectstack/spec/kernel'; // [#21120] The family-wide stored-metadata-body seam. A `data.record.*` event // carries the written row in `after` (and the input patch in `changes`), so a // subscriber to `sys_metadata` / `sys_metadata_history` events would receive @@ -7863,10 +7863,30 @@ export class ObjectQL implements IObjectQLEngine { * * Key: Package ≠ App. The manifest is the package. The apps[] array inside * the manifest contains UI navigation definitions (AppSchema). + * + * [#23972] `source` is where the package came from, stamped on its registry + * row (`InstalledPackageSchema.source`). This is the door the deployment's + * own code registers through — a `defineStack()` host, a plugin's bundled + * manifest, a platform package — so `'code'` is what the door means when the + * caller says nothing. The one other producer on this door, the marketplace + * install-local plugin, states `'marketplace'` through the `manifest` + * service's `register(artifact, { source })`. An authored package never + * comes through here (it is installed by `installPackage` in + * `@objectstack/metadata-protocol`), so `'authored'` is not accepted. */ - registerApp(registeredBody: any, scope?: ArtifactInstallScope) { + registerApp(registeredBody: any, scope?: ArtifactInstallScope, source: Exclude = 'code') { // The CONVERTED body, for everything below — the package record, the // live manifest, every item it registers (see {@link convertPackageBodyForward}). + // [#23972] Refused before anything registers — see the docblock: an + // untyped caller handing this door `'authored'` (or any other value) + // would record a row whose origin contradicts the read-only verdict + // this door's `manifests` entry produces. + if (source !== 'code' && source !== 'marketplace') { + throw new TypeError( + `[ObjectQL] registerApp: source must be 'code' or 'marketplace'; received ${JSON.stringify(source)}. ` + + `An authored package is installed through the protocol's installPackage, not registered here.`, + ); + } const manifest = this.convertPackageBodyForward(registeredBody); const id = manifest.id || manifest.name; const namespace = manifest.namespace as string | undefined; @@ -7899,7 +7919,7 @@ export class ObjectQL implements IObjectQLEngine { // tell a CO-OWNER (another package from this same artifact) from a // stranger — the question D1 corrected the gate to ask. Absent for every // single-package caller, where the gate behaves exactly as before (D7). - this._registry.installPackage(manifest, undefined, scope); + this._registry.installPackage(manifest, source, undefined, scope); this.logger.debug('Installed Package', { id: manifest.id, name: manifest.name, namespace }); // 2. Register owned objects diff --git a/packages/objectql/src/plugin.ts b/packages/objectql/src/plugin.ts index c4ec5086e02..28b1d035366 100644 --- a/packages/objectql/src/plugin.ts +++ b/packages/objectql/src/plugin.ts @@ -667,7 +667,12 @@ export class ObjectQLPlugin implements Plugin { // register/bridge interleave would bridge a body that the very next // package is about to change. For the single-package artifact this is the // same one register followed by the same one bridge as before (D7). - register: (registeredArtifact: any) => { + // + // [#23972] `options.source` is where every package in this artifact came + // from, stamped on each registry row (`InstalledPackageSchema.source`). + // Absent means `'code'`, the deployment's own code (`registerApp`'s + // default); the marketplace install-local plugin states `'marketplace'`. + register: (registeredArtifact: any, options?: { source?: 'code' | 'marketplace' }) => { // ADR-0087 D2 at the registration door, for an artifact that carries // `packages[]`: its bodies are strictly parsed by the ordering below // BEFORE `registerApp` could convert each one, so a bundle handed here @@ -734,7 +739,7 @@ export class ObjectQLPlugin implements Plugin { } for (const manifest of ordered) { - ql.registerApp(manifest, scope); + ql.registerApp(manifest, scope, options?.source); ctx.logger.debug('Manifest registered via manifest service', { id: manifest.id || manifest.name }); diff --git a/packages/objectql/src/registry.ts b/packages/objectql/src/registry.ts index 154295ad543..db17afe5981 100644 --- a/packages/objectql/src/registry.ts +++ b/packages/objectql/src/registry.ts @@ -44,7 +44,7 @@ import { SystemFieldName, scalarOverridesPackagedBase } from '@objectstack/spec/ import { resolveTenancyPosture, resolveSearchPinyinEnabled } from '@objectstack/types'; import { postureEnforcesWall } from '@objectstack/spec/security'; import { provisionSearchCompanion, SEARCH_COMPANION_FIELD } from './search-companion.js'; -import { ObjectStackManifest, ManifestSchema, InstalledPackage, InstalledPackageSchema, checkFieldCompleteness } from '@objectstack/spec/kernel'; +import { ObjectStackManifest, ManifestSchema, InstalledPackage, InstalledPackageSchema, InstalledPackageSourceSchema, type InstalledPackageSource, checkFieldCompleteness } from '@objectstack/spec/kernel'; import { DEFAULT_FLOW_FUNCTION_EFFECT } from '@objectstack/spec/automation'; import { AppSchema } from '@objectstack/spec/ui'; import { applyProtection } from '@objectstack/spec/shared'; @@ -5439,9 +5439,27 @@ export class SchemaRegistry { installPackage( manifest: ObjectStackManifest, + source: InstalledPackageSource, settings?: Record, scope?: ArtifactInstallScope, ): InstalledPackage { + // [#23972] `source` is the CALLER's statement of where this package came + // from — the door that registers it is the only party that knows + // (`registerApp`: code or marketplace; `installPackage` in + // `@objectstack/metadata-protocol`, the `POST /packages` fallback and the + // `sys_packages` rehydrate: authored). The registry records it and never + // infers it, so an untyped caller that omits it, or spells a value outside + // the closed set, is refused here — ahead of every check and mutation + // below, so a refused call leaves no row behind — rather than recording a + // row the read door would serve without an origin. + if (!InstalledPackageSourceSchema.safeParse(source).success) { + throw new TypeError( + `[Registry] installPackage('${String(manifest?.id)}') needs the package's source, one of ` + + `${InstalledPackageSourceSchema.options.map((v) => `'${v}'`).join(' | ')}; received ` + + `${source === undefined ? 'none' : JSON.stringify(source)}. The door that registers a package ` + + `states where it came from; the registry does not guess.`, + ); + } // [ADR-0130 D1] The co-owners of this package — the OTHER packages the same // release artifact is delivering. Computed once and read by both halves of // the install-time pair below, so the relaxation and the refusal cannot @@ -5567,6 +5585,7 @@ export class SchemaRegistry { // equal so the structural projection reports BOTH, without teaching the // projection a key name. manifest: toRecordManifest(withDeclaredFunctionEntries(manifest)), + source, ...lifecycle, installedAt: now, updatedAt: now, diff --git a/packages/runtime/src/domains/packages.ts b/packages/runtime/src/domains/packages.ts index 93319cef1c9..531bc15816c 100644 --- a/packages/runtime/src/domains/packages.ts +++ b/packages/runtime/src/domains/packages.ts @@ -268,6 +268,9 @@ export function createPackagesDomain(deps: DomainHandlerDeps): DomainRoute { */ const INSTALLED_PACKAGE_RESPONSE_FIELDS = [ 'manifest', + // [#23972] The origin the registering door stamped on the row — read off + // the record as-is, ⛔ never derived here from `writable` or a second list. + 'source', 'status', 'enabled', 'installedAt', @@ -1406,7 +1409,9 @@ export async function handlePackagesRequest(deps: DomainHandlerDeps, path: strin // the primitive's, so no-range and unparsed ranges warn the // same way on both arms. assertProtocolCompat(manifest); - pkg = registry.installPackage(manifest, settings); + // [#23972] The same origin the protocol primitive stamps: + // a package installed through this door is authored. + pkg = registry.installPackage(manifest, 'authored', settings); } } catch (err) { // [#21727] The protocol refusal's typed carrier, recognised diff --git a/packages/services/service-package/src/index.ts b/packages/services/service-package/src/index.ts index 9c6cfa75f2b..11e28de72ae 100644 --- a/packages/services/service-package/src/index.ts +++ b/packages/services/service-package/src/index.ts @@ -712,7 +712,10 @@ export class PackageServicePlugin implements Plugin { `'${compat.requiredRange}'; skipping the protocol handshake (ADR-0087).`, ); } - registry.installPackage(rec.manifest); + // [#23972] `sys_packages` holds the packages `installPackage` + // persisted (Studio bases, duplicates, HTTP installs), so every row + // it rehydrates is authored — the origin the install stamped. + registry.installPackage(rec.manifest, 'authored'); hydrated++; } } diff --git a/packages/spec/src/api/package-api-assembled.zod.ts b/packages/spec/src/api/package-api-assembled.zod.ts index 84c731fbcf4..54714603c2f 100644 --- a/packages/spec/src/api/package-api-assembled.zod.ts +++ b/packages/spec/src/api/package-api-assembled.zod.ts @@ -146,6 +146,26 @@ export type AssembledInstalledPackage = z.input; +/** + * The keys a read door adds to a registry row — the server's OWN verdicts, + * computed per read and never stored on the registry record, so they are + * declared on the SERVED row (both stage branches below) and not on + * {@link InstalledPackageSchema}, which is the record. + * + * - `writable` — the server's writability verdict (ADR-0070 D2, ADR-0130 + * Consequences row 6): `isWritablePackage` in `@objectstack/metadata-protocol`, + * the same predicate the authoring and lifecycle gates use. `true` is a + * writable base; `false` is read-only (booted by code or installed from the + * marketplace, or a `system` / `cloud` scope). + * + * ⛔ `writable` and the row's `source` are two statements, neither derived from + * the other: `source` says where the package came from, `writable` what this + * server lets an org do to it. + */ +const SERVED_ROW_VERDICTS = { + writable: z.boolean().describe('Whether this server lets an org author into and manage this package (a writable base); false for a read-only package. Computed per read by the server\'s own predicate, never stored'), +}; + /** * One installed-package row at WHICHEVER manifest stage it was installed at — * the element the read doors (`GET /packages`, `GET /packages/:id`) serve. @@ -186,8 +206,8 @@ export type AssembledInstalledPackageParsed = z.infer z.union([ - InstalledPackageSchema, - AssembledInstalledPackageSchema, + InstalledPackageSchema.extend(SERVED_ROW_VERDICTS), + AssembledInstalledPackageSchema.extend(SERVED_ROW_VERDICTS), ]).describe('Installed package row at whichever manifest stage it was installed at')); export type InstalledPackageAtEitherStage = z.input; /** Post-parse shape of {@link InstalledPackageAtEitherStage} — defaults applied, transforms run (ADR-0122). */ diff --git a/packages/spec/src/contracts/objectql-engine.ts b/packages/spec/src/contracts/objectql-engine.ts index 6b7db32fcf8..82c89b2d63a 100644 --- a/packages/spec/src/contracts/objectql-engine.ts +++ b/packages/spec/src/contracts/objectql-engine.ts @@ -56,6 +56,7 @@ import type { IDataDriver } from './data-driver'; import type { FlowFunctionEffect, FlowFunctionEntry } from '../automation/flow-function.zod'; import type { ServiceObject } from '../data/object.zod'; import type { BaseEngineOptions, EngineQueryOptions } from '../data/data-engine.zod'; +import type { InstalledPackageSource } from '../kernel/package-registry.zod'; /** * The engine's schema-registry view — the members reached through the @@ -110,8 +111,12 @@ export interface EngineSchemaRegistryView { // protocol service writes through. /** One installed package by id, or `undefined` — the duplicate-install guard's reader. */ getPackage(id: string): unknown; - /** Register a package manifest in the in-memory registry. */ - installPackage(manifest: unknown, settings?: Record): unknown; + /** + * Register a package manifest in the in-memory registry. `source` is the + * caller's statement of where the package came from, stamped on the row; a + * value outside the closed set is refused. + */ + installPackage(manifest: unknown, source: InstalledPackageSource, settings?: Record): unknown; /** Drop a package from the registry; `false` when no package had that id. */ uninstallPackage(id: string): boolean; /** Flip a package to enabled; `undefined` when no package had that id. */ diff --git a/packages/spec/src/kernel/package-registry.zod.ts b/packages/spec/src/kernel/package-registry.zod.ts index 7694ddaddee..4ef3510b253 100644 --- a/packages/spec/src/kernel/package-registry.zod.ts +++ b/packages/spec/src/kernel/package-registry.zod.ts @@ -46,6 +46,31 @@ export const PackageStatusEnum = z.enum([ ]).describe('Package installation status'); export type PackageStatus = z.input; +/** + * Where an installed package came from — the closed origin every registry row + * carries, stamped by the door that registered it. + * + * - `code` — registered by the running deployment's own code: a `defineStack()` + * host's packages, a plugin's bundled manifest, a platform package + * (`ObjectQL.registerApp`, the `manifest` service's default). + * - `marketplace` — installed from the marketplace: the install-local plugin + * registers it (install and boot rehydrate from its ledger) and says so. + * - `authored` — a database-authored package (a Studio base, a duplicate, an + * HTTP install) installed through `installPackage` and rehydrated from + * `sys_packages`. + * + * ⛔ The value is the PRODUCER's statement, made at the moment it registers the + * package; no reader derives it from other keys (`writable`, `manifest.scope`, + * a second list). `SchemaRegistry.installPackage` takes it as a required + * argument and refuses a value outside this set. + */ +export const InstalledPackageSourceSchema = z.enum([ + 'code', + 'marketplace', + 'authored', +]).describe('Where the installed package came from: code (registered by the deployment\'s own code), marketplace (installed from the marketplace), or authored (a database-authored package)'); +export type InstalledPackageSource = z.infer; + /** * Installed Package Schema * @@ -85,6 +110,13 @@ export const InstalledPackageSchema = lazySchema(() => z.object({ */ manifest: ManifestSchema.describe('Package manifest at the AUTHORING stage; a row installed by a `defineStack()` host carries the assembled body instead — see `AssembledInstalledPackageSchema` / `InstalledPackageAtEitherStageSchema`'), + /** + * Where this package came from — see {@link InstalledPackageSourceSchema}. + * Required: every door that registers a package states it, and the + * registry refuses a registration that does not. + */ + source: InstalledPackageSourceSchema, + /** * Current lifecycle status. */ From b06c951f6a387db7ca7f079d82ed83ddae9c79c6 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 17:30:16 +0000 Subject: [PATCH 02/10] wip(packages): pins and changeset for the installed-package source Claude-Session: https://claude.ai/code/session_01RGdjFAX2oq1VdGNT5WGdKN Co-authored-by: Claude --- .changeset/23972-installed-package-source.md | 42 +++++++ .../src/packages-write-envelope.test.ts | 6 +- ...rketplace-install-local-hot-loaded.test.ts | 26 +++- .../src/durable-package.test.ts | 2 +- .../protocol.package-delete-refusal.test.ts | 2 +- .../objectql/src/package-source-stamp.test.ts | 114 ++++++++++++++++++ .../protocol-boot-hydration-scoped.test.ts | 4 +- ...-install-package-enable-on-install.test.ts | 13 ++ .../registry-artifact-co-ownership.test.ts | 16 +-- .../src/registry-default-record-page.test.ts | 6 +- ...gistry-install-preserves-lifecycle.test.ts | 28 ++--- .../objectql/src/registry-log-level.test.ts | 8 +- .../registry-namespace-install-gate.test.ts | 32 ++--- ...stry-package-manifest-serializable.test.ts | 36 +++--- ...try-security-catalog-names-held-by.test.ts | 8 +- ...egistry-security-catalog-namespace.test.ts | 4 +- packages/objectql/src/registry.test.ts | 4 +- .../src/app-plugin.disabled-seed.test.ts | 8 +- ...s-read-delete-response-conformance.test.ts | 2 +- .../domains/packages-readonly-gate.test.ts | 10 +- .../domains/packages-writable-verdict.test.ts | 46 ++++++- .../service-package/src/hydration.test.ts | 16 +++ packages/spec/authorable-surface/api.json | 1 + packages/spec/authorable-surface/kernel.json | 1 + .../spec/json-schema.manifest/kernel.json | 1 + packages/spec/src/api/package-api.test.ts | 29 ++++- .../spec/src/kernel/package-registry.test.ts | 25 ++++ 27 files changed, 394 insertions(+), 96 deletions(-) create mode 100644 .changeset/23972-installed-package-source.md create mode 100644 packages/objectql/src/package-source-stamp.test.ts diff --git a/.changeset/23972-installed-package-source.md b/.changeset/23972-installed-package-source.md new file mode 100644 index 00000000000..668b2f3a475 --- /dev/null +++ b/.changeset/23972-installed-package-source.md @@ -0,0 +1,42 @@ +--- +'@objectstack/spec': minor +'@objectstack/objectql': minor +'@objectstack/metadata-protocol': patch +'@objectstack/runtime': patch +'@objectstack/service-package': patch +'@objectstack/cloud-connection': patch +--- + +feat(spec): every installed-package row declares where it came from, a closed `source: 'code' | 'marketplace' | 'authored'`, and the `GET /packages` rows declare the `writable` verdict they already served (#23972) + +Clause-②: yes (widening) + +**What is new on the wire.** `GET /api/v1/packages` and `GET /api/v1/packages/:id` serve two declared keys on every row: + +- `source` names the package's origin. `code` means the deployment's own code registered it (a `defineStack()` host, a plugin's bundled manifest, a platform package). `marketplace` means the marketplace install-local plugin installed it, and that holds after a restart too, when the plugin rehydrates it from its ledger. `authored` means it is a database-authored package (a Studio base, a duplicate, an HTTP install), including after the restart that rehydrates it from `sys_packages`. The door that registers the package stamps the value. No reader derives it. +- `writable` is the server's own writability verdict (ADR-0070 D2), served on these rows since #14375 and now declared. It is computed per read and never stored, so it is declared on the served row (`InstalledPackageAtEitherStageSchema`) and not on the registry record. + +FROM → TO for a reader of these rows: + +| you did | do instead | +| --- | --- | +| treated `writable: true` as authored, and looked up each non-writable row in the marketplace's installed list (the control plane's, else install-local's) to tell marketplace from code | read `row.source` | +| read the undeclared `writable` | read the declared `writable` (unchanged value) | + +The two keys are separate statements: `source` says where the package came from, and `writable` says what this server lets an org do to it. + +**`InstalledPackageSchema` requires `source`.** It is `InstalledPackageSourceSchema`, exported from `@objectstack/spec/kernel`. A row without it, or with a value outside the closed set, is refused at the `source` key. + +**API change for code that registers packages (`@objectstack/objectql`).** The registry records the origin and never infers it: + +| you wrote | write instead | +| --- | --- | +| `registry.installPackage(manifest)` | `registry.installPackage(manifest, 'code')` (or `'marketplace'` / `'authored'`, whichever is where the package came from) | +| `registry.installPackage(manifest, settings, scope)` | `registry.installPackage(manifest, source, settings, scope)` | +| `ctx.getService('manifest').register(artifact)` for a marketplace install | `ctx.getService('manifest').register(artifact, { source: 'marketplace' })` | + +`SchemaRegistry.installPackage` throws a `TypeError` naming the closed set when `source` is missing or unknown, before anything is recorded. `ObjectQL.registerApp(body, scope?, source?)` and the `manifest` service record `'code'` when no source is given. They accept `'code'` or `'marketplace'` and refuse `'authored'`. The `IObjectQLEngine` registry contract (`@objectstack/spec/contracts`) declares the same `installPackage(manifest, source, settings?)`. + +**The one-line fix:** pass the package's origin as `installPackage`'s second argument. + +Readers: objectui's Setup → Packages Source column and its `?source=` filter (it switches from the two-read join to `source` in its own follow-up), and the client SDK's `packages.list()` / `packages.get()` row types. diff --git a/packages/client/src/packages-write-envelope.test.ts b/packages/client/src/packages-write-envelope.test.ts index f1cb9d8ad5d..f53f39310b7 100644 --- a/packages/client/src/packages-write-envelope.test.ts +++ b/packages/client/src/packages-write-envelope.test.ts @@ -178,7 +178,7 @@ describe('#12034 — the packages write verbs answer the bare row, not `{ packag it('enable resolves to the row, carrying the flag it just set', async () => { const registry = new SchemaRegistry(); - registry.installPackage(MANIFEST as any); + registry.installPackage(MANIFEST as any, 'authored'); registry.disablePackage(MANIFEST.id); const { client, seen } = dispatcherBackedClient(registry); @@ -195,7 +195,7 @@ describe('#12034 — the packages write verbs answer the bare row, not `{ packag it('disable resolves to the row, carrying the flag it just cleared', async () => { const registry = new SchemaRegistry(); - registry.installPackage(MANIFEST as any); + registry.installPackage(MANIFEST as any, 'authored'); const { client, seen } = dispatcherBackedClient(registry); const row = await client.packages.disable(MANIFEST.id); @@ -216,7 +216,7 @@ describe('#12034 — the packages write verbs answer the bare row, not `{ packag */ it('the dispatcher wraps the row exactly once, and the client strips exactly that', async () => { const registry = new SchemaRegistry(); - registry.installPackage(MANIFEST as any); + registry.installPackage(MANIFEST as any, 'authored'); const { client, dispatcher } = dispatcherBackedClient(registry); const raw = await dispatcher.handlePackages('/com.acme.crm/enable', 'PATCH', undefined, {}, CONTEXT()); diff --git a/packages/cloud-connection/src/marketplace-install-local-hot-loaded.test.ts b/packages/cloud-connection/src/marketplace-install-local-hot-loaded.test.ts index f245a64ece8..09c925f263b 100644 --- a/packages/cloud-connection/src/marketplace-install-local-hot-loaded.test.ts +++ b/packages/cloud-connection/src/marketplace-install-local-hot-loaded.test.ts @@ -63,7 +63,7 @@ async function mount(dir: string, opts: { controlPlaneUrl?: string; failRegister const rawApp = makeRawApp(); const hooks = new Map(); const registry: any[] = []; - const register = vi.fn(async (m: any) => { + const register = vi.fn(async (m: any, _options?: { source?: string }) => { if (opts.failRegisterOf !== undefined && m?.id === opts.failRegisterOf) { throw new Error(REGISTER_FAILURE); } @@ -95,6 +95,9 @@ async function mount(dir: string, opts: { controlPlaneUrl?: string; failRegister logger, /** The manifest ids the install ASKED to register, whether or not the register resolved. */ installRegisterAttempts: () => register.mock.calls.slice(registeredAtBoot).map((call) => call[0]?.id), + /** Every register call as `[manifest id, source stated]`, boot included. */ + registerSources: () => register.mock.calls.map((call: any[]) => [call[0]?.id, call[1]?.source]), + registeredAtBoot, }; } @@ -192,3 +195,24 @@ describe('install-local answers hotLoaded from what the hot-register did', () => expect(readdirSync(dir)).toEqual([]); }); }); + +// [#23972] The registry row of a marketplace install says where it came from: +// the plugin states `source: 'marketplace'` on the hot-register at install AND +// on the boot rehydrate from its ledger, so a restart never reads it back as +// code. Its own UI bundle is the plugin's code and states nothing (`'code'`). +describe('install-local registers its packages as source marketplace', () => { + it('states marketplace on the install register and on the rehydrate after a restart', async () => { + const first = await mount(dir); + const res = await first.install({ manifest: MANIFEST }); + expect(res.status, JSON.stringify(res.payload)).toBe(200); + expect(first.registerSources().slice(first.registeredAtBoot)).toEqual([[APP_ID, 'marketplace']]); + + // A second boot over the same ledger: the rehydrate is the restart half. + const second = await mount(dir); + const atBoot = second.registerSources().slice(0, second.registeredAtBoot); + expect(atBoot.filter(([id]) => id === APP_ID)).toEqual([[APP_ID, 'marketplace']]); + // CONTROL: the plugin's own UI bundle registered at boot without a source. + expect(atBoot.filter(([id]) => id !== APP_ID).every(([, source]) => source === undefined)).toBe(true); + expect(atBoot.length).toBeGreaterThan(1); + }); +}); diff --git a/packages/metadata-protocol/src/durable-package.test.ts b/packages/metadata-protocol/src/durable-package.test.ts index af28eb91926..e91c967891c 100644 --- a/packages/metadata-protocol/src/durable-package.test.ts +++ b/packages/metadata-protocol/src/durable-package.test.ts @@ -78,7 +78,7 @@ function makeImpl(overrides?: { }; const engine = { registry, find: overrides?.find ?? (async () => []) }; // Seeded through the registry's own verb, exactly as the boot hydration does. - for (const manifest of overrides?.hydrate ?? []) registry.installPackage(manifest); + for (const manifest of overrides?.hydrate ?? []) registry.installPackage(manifest, 'authored'); registryCalls.length = 0; // hydration is the fixture, not an observation const publish = vi.fn(overrides?.publish ?? (async () => ({ success: true }))); diff --git a/packages/metadata-protocol/src/protocol.package-delete-refusal.test.ts b/packages/metadata-protocol/src/protocol.package-delete-refusal.test.ts index a7a677ac296..35d3a26b279 100644 --- a/packages/metadata-protocol/src/protocol.package-delete-refusal.test.ts +++ b/packages/metadata-protocol/src/protocol.package-delete-refusal.test.ts @@ -129,7 +129,7 @@ function boot(world: World, storeDelete: StoreDelete = landed, opts: { uninstall }, }; // The boot hydration: every stored row is registered. - for (const manifest of world.sysPackages.values()) registry.installPackage(manifest); + for (const manifest of world.sysPackages.values()) registry.installPackage(manifest, 'authored'); const engine = { registry, diff --git a/packages/objectql/src/package-source-stamp.test.ts b/packages/objectql/src/package-source-stamp.test.ts new file mode 100644 index 00000000000..5909a9645dd --- /dev/null +++ b/packages/objectql/src/package-source-stamp.test.ts @@ -0,0 +1,114 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * Every installed-package row carries `source` — where the package came from — + * stamped by the door that registered it (`InstalledPackageSchema.source`, + * #23972; the closed set is ruling B on #23892: code / marketplace / authored). + * + * This file holds the engine's two doors onto the package table: + * + * - the `manifest` service → `ObjectQL.registerApp`: the deployment's own code + * registers through it, so a caller that says nothing records `'code'`; the + * marketplace install-local plugin states `{ source: 'marketplace' }`, and + * every package of a multi-package artifact it hands over carries it; + * - `SchemaRegistry.installPackage`, the record seam itself: it takes the + * origin as a required argument and refuses a call without one, or with a + * value outside the set, before anything is recorded. + * + * The authored door (`installPackage` in `@objectstack/metadata-protocol`) is + * pinned beside it, in `protocol-install-package-enable-on-install.test.ts`. + */ + +import { describe, it, expect, afterEach } from 'vitest'; +import { ObjectKernel, type Plugin, type PluginContext } from '@objectstack/core'; +import { ObjectQLPlugin } from './plugin.js'; +import type { ObjectQL } from './engine.js'; +import { SchemaRegistry } from './registry.js'; + +type ManifestService = { register(m: unknown, options?: { source?: 'code' | 'marketplace' }): void | Promise }; + +const CODE_ID = 'com.example.source.code'; +const MARKET_ID = 'com.example.source.market'; +const BUNDLE_A = 'com.example.source.bundle'; +const BUNDLE_B = 'com.example.source.bundle.extra'; + +const body = (id: string, namespace: string) => ({ id, name: namespace, namespace, version: '1.0.0', type: 'app' }); + +const kernels: ObjectKernel[] = []; + +/** Boot a kernel whose one plugin registers `steps` through the `manifest` service in `init()`. */ +async function boot(steps: Array<[unknown, { source?: 'code' | 'marketplace' } | undefined]>): Promise { + const kernel = new ObjectKernel({ logger: { level: 'silent' }, gracefulShutdown: false }); + kernels.push(kernel); + await kernel.use(new ObjectQLPlugin()); + await kernel.use({ + name: 'test.package-source.registrar', + dependencies: ['com.objectstack.engine.objectql'], + init: async (ctx: PluginContext) => { + const manifest = ctx.getService('manifest'); + for (const [artifact, options] of steps) await manifest.register(artifact, options); + }, + } as Plugin); + await kernel.bootstrap(); + return kernel.getService('objectql'); +} + +const sourceOf = (ql: ObjectQL, id: string) => (ql.registry.getPackage(id) as { source?: unknown } | undefined)?.source; + +afterEach(async () => { + while (kernels.length) { + const kernel = kernels.pop()!; + if (kernel.getState() === 'running') await kernel.shutdown(); + } +}); + +describe('the manifest service stamps the registering caller\'s source (#23972)', () => { + it('a package registered by code, with no source stated, records source code', async () => { + const ql = await boot([[body(CODE_ID, 'srccode'), undefined]]); + expect(sourceOf(ql, CODE_ID)).toBe('code'); + }); + + it('a package the marketplace registers records source marketplace', async () => { + const ql = await boot([ + [body(CODE_ID, 'srccode'), undefined], + [body(MARKET_ID, 'srcmarket'), { source: 'marketplace' }], + ]); + expect(sourceOf(ql, MARKET_ID)).toBe('marketplace'); + // CONTROL: the code package registered beside it keeps its own origin. + expect(sourceOf(ql, CODE_ID)).toBe('code'); + }); + + it('every package of a multi-package marketplace artifact records source marketplace', async () => { + const artifact = { + ...body(BUNDLE_A, 'srcbundle'), + packages: [{ manifest: body(BUNDLE_A, 'srcbundle') }, { manifest: body(BUNDLE_B, 'srcbundlex') }], + }; + const ql = await boot([[artifact, { source: 'marketplace' }]]); + expect([sourceOf(ql, BUNDLE_A), sourceOf(ql, BUNDLE_B)]).toEqual(['marketplace', 'marketplace']); + }); + + it('registerApp refuses `authored` — an authored package is installed, not registered here — and records nothing', async () => { + const ql = await boot([]); + expect(() => ql.registerApp(body(MARKET_ID, 'srcmarket'), undefined, 'authored' as never)).toThrow(TypeError); + expect(ql.registry.getPackage(MARKET_ID)).toBeUndefined(); + }); +}); + +describe('SchemaRegistry.installPackage requires the origin and refuses an unknown one (#23972)', () => { + it('records the source it is handed on the row', () => { + const registry = new SchemaRegistry({ logLevel: 'silent' } as never); + for (const [id, source] of [['com.a.one', 'code'], ['com.a.two', 'marketplace'], ['com.a.three', 'authored']] as const) { + expect(registry.installPackage({ id, name: id, version: '1.0.0', type: 'app' } as never, source).source).toBe(source); + } + }); + + it('refuses a call that states no source, or a value outside the closed set, and records no row', () => { + const registry = new SchemaRegistry({ logLevel: 'silent' } as never); + const manifest = { id: 'com.a.refused', name: 'refused', namespace: 'refused', version: '1.0.0', type: 'app' }; + expect(() => registry.installPackage(manifest as never, undefined as never)).toThrow(TypeError); + expect(() => registry.installPackage(manifest as never, 'cloud' as never)).toThrow(TypeError); + expect(registry.getPackage('com.a.refused')).toBeUndefined(); + // Refused ahead of every mutation: the namespace was not claimed either. + expect(registry.getNamespaceOwners('refused')).toEqual([]); + }); +}); diff --git a/packages/objectql/src/protocol-boot-hydration-scoped.test.ts b/packages/objectql/src/protocol-boot-hydration-scoped.test.ts index b82baec221e..c218b792eef 100644 --- a/packages/objectql/src/protocol-boot-hydration-scoped.test.ts +++ b/packages/objectql/src/protocol-boot-hydration-scoped.test.ts @@ -316,10 +316,10 @@ describe('security catalog read — a name two packages ship (ADR-0131 D4, ruled // its in-memory registrar runs (Phase 2, `AppPlugin.start`). const stack = (id: string, label: string) => ({ id, name: id, version: '1.0.0', type: 'app', positions: [{ name: 'regional_manager', label }] }) as never; - registry.installPackage(stack(PKG_A, 'first stack')); + registry.installPackage(stack(PKG_A, 'first stack'), 'code'); metadata.registerInMemory('position', 'regional_manager', { name: 'regional_manager', label: 'first stack' }); - const refusal = refusalOf(() => registry.installPackage(stack(PKG_B, 'second stack'))); + const refusal = refusalOf(() => registry.installPackage(stack(PKG_B, 'second stack'), 'code')); expect(refusal?.code).toBe(NAMESPACE_CONFLICT_CODE); expect(refusal?.status).toBe(422); expect(refusal?.incomingPackageId).toBe(PKG_B); diff --git a/packages/objectql/src/protocol-install-package-enable-on-install.test.ts b/packages/objectql/src/protocol-install-package-enable-on-install.test.ts index 5e544f40223..1978caf2225 100644 --- a/packages/objectql/src/protocol-install-package-enable-on-install.test.ts +++ b/packages/objectql/src/protocol-install-package-enable-on-install.test.ts @@ -283,3 +283,16 @@ describe('#19277 — a BOOT-SEEDED disable, the durable state this seam can see' expect(registry.getPackage('com.acme.seeded.control')?.enabled).toBe(true); }); }); + +// [#23972] The protocol primitive is the authored door: a Studio base, a +// duplicate, an HTTP install. Every row it installs says `source: 'authored'`, +// on the row it returns and on the registry row the read doors serve. +describe('#23972 — the in-process install door stamps source authored', () => { + it('installs the row with source authored, in both records', async () => { + const registry = freshRegistry(); + const { protocol } = makeDoor(registry); + const res = await install(protocol, { manifest: manifest('com.acme.authored', 'authored') }); + expect(res.package.source, 'the row this primitive RETURNED').toBe('authored'); + expect(registry.getPackage('com.acme.authored')?.source, 'the registry the next read serves from').toBe('authored'); + }); +}); diff --git a/packages/objectql/src/registry-artifact-co-ownership.test.ts b/packages/objectql/src/registry-artifact-co-ownership.test.ts index 55a91323757..3e9a71dcd89 100644 --- a/packages/objectql/src/registry-artifact-co-ownership.test.ts +++ b/packages/objectql/src/registry-artifact-co-ownership.test.ts @@ -266,7 +266,7 @@ describe('ADR-0130 D1 — the relaxation does not widen the same-id reinstall ex manifest: { id: string; namespace: string; objects?: Array<{ name: string }> }, scope?: { packageIds: string[] }, ) => { - registry.installPackage(manifest as never, undefined, scope); + registry.installPackage(manifest as never, 'code', undefined, scope); for (const obj of manifest.objects ?? []) { registry.registerObject(obj as never, manifest.id, manifest.namespace, 'own'); } @@ -293,7 +293,7 @@ describe('ADR-0130 D1 — the relaxation does not widen the same-id reinstall ex let caught: Envelope | undefined; try { - registry.installPackage(crmBillingColliding() as never, undefined, scope); + registry.installPackage(crmBillingColliding() as never, 'code', undefined, scope); } catch (e) { caught = e as Envelope; } expect(caught?.code).toBe('DUPLICATE_ARTIFACT_OBJECT_NAME'); @@ -306,9 +306,9 @@ describe('ADR-0130 D1 — the relaxation does not widen the same-id reinstall ex // id, so same package, so a reload — including a reload arriving from a // different artifact, which is what a version upgrade is. install(crmCore(), scopeOf('com.acme.crm')); - expect(() => registry.installPackage(crmCore() as never, undefined, scopeOf('com.acme.crm'))).not.toThrow(); - expect(() => registry.installPackage(crmCore() as never)).not.toThrow(); - expect(() => registry.installPackage(crmCore() as never, undefined, scopeOf('com.acme.crm', 'com.acme.crm.billing'))) + expect(() => registry.installPackage(crmCore() as never, 'code', undefined, scopeOf('com.acme.crm'))).not.toThrow(); + expect(() => registry.installPackage(crmCore() as never, 'code')).not.toThrow(); + expect(() => registry.installPackage(crmCore() as never, 'code', undefined, scopeOf('com.acme.crm', 'com.acme.crm.billing'))) .not.toThrow(); }); @@ -323,7 +323,7 @@ describe('ADR-0130 D1 — the relaxation does not widen the same-id reinstall ex let caught: Envelope | undefined; try { - registry.installPackage(stranger as never, undefined, scopeOf('com.other.suite')); + registry.installPackage(stranger as never, 'code', undefined, scopeOf('com.other.suite')); } catch (e) { caught = e as Envelope; } expect(caught).toBeUndefined(); @@ -340,12 +340,12 @@ describe('ADR-0130 D1 — the relaxation does not widen the same-id reinstall ex warnReg.logLevel = 'silent'; const scope = scopeOf('com.acme.crm', 'com.acme.crm.billing'); const base = crmCore(); - warnReg.installPackage(base as never, undefined, scope); + warnReg.installPackage(base as never, 'code', undefined, scope); for (const obj of base.objects) warnReg.registerObject(obj as never, base.id, base.namespace, 'own'); let caught: Envelope | undefined; try { - warnReg.installPackage(crmBillingColliding() as never, undefined, scope); + warnReg.installPackage(crmBillingColliding() as never, 'code', undefined, scope); } catch (e) { caught = e as Envelope; } expect(caught?.code).toBe('DUPLICATE_ARTIFACT_OBJECT_NAME'); diff --git a/packages/objectql/src/registry-default-record-page.test.ts b/packages/objectql/src/registry-default-record-page.test.ts index 65bf531f565..a6cf4bdb920 100644 --- a/packages/objectql/src/registry-default-record-page.test.ts +++ b/packages/objectql/src/registry-default-record-page.test.ts @@ -165,7 +165,7 @@ describe('a second package registering a default record page for an object anoth it('a package installed with no pages registered still holds what it declared (its claim)', async () => { const { registry, register } = await boot(); // A bare install — `POST /api/v1/packages` stores the manifest alone. - registry.installPackage(pkg('com.acme.auth', [recordPage('acme_org_detail', 'acme_org')]) as any); + registry.installPackage(pkg('com.acme.auth', [recordPage('acme_org_detail', 'acme_org')]) as any, 'code'); expect(registry.listItems('page')).toEqual([]); const err = await register(pkg('com.acme.tenant', [recordPage('tenant_org_detail', 'acme_org')])); expectRefusal(err, 'acme_org', pkgPage('com.acme.tenant', 'tenant_org_detail'), pkgPage('com.acme.auth', 'acme_org_detail')); @@ -301,8 +301,8 @@ describe('controls — what is not a second default', () => { it('`collisionPolicy: \'warn\'` does not downgrade it', async () => { const registry = new SchemaRegistry({ collisionPolicy: 'warn' }); registry.logLevel = 'silent'; - registry.installPackage(pkg('com.acme.auth', [recordPage('acme_org_detail', 'acme_org')]) as never); - const err = await caught(() => registry.installPackage(pkg('com.acme.tenant', [recordPage('tenant_org_detail', 'acme_org')]) as never)); + registry.installPackage(pkg('com.acme.auth', [recordPage('acme_org_detail', 'acme_org')]) as never, 'code'); + const err = await caught(() => registry.installPackage(pkg('com.acme.tenant', [recordPage('tenant_org_detail', 'acme_org')]) as never, 'code')); expectRefusal(err, 'acme_org', pkgPage('com.acme.tenant', 'tenant_org_detail'), pkgPage('com.acme.auth', 'acme_org_detail')); }); }); diff --git a/packages/objectql/src/registry-install-preserves-lifecycle.test.ts b/packages/objectql/src/registry-install-preserves-lifecycle.test.ts index 670b2a2a47e..4ddea17bc81 100644 --- a/packages/objectql/src/registry-install-preserves-lifecycle.test.ts +++ b/packages/objectql/src/registry-install-preserves-lifecycle.test.ts @@ -46,12 +46,12 @@ describe('#18877 — installPackage preserves an existing row\'s lifecycle state }); it('a re-install carries `enabled` / `status` / `statusChangedAt` over from the existing row', () => { - registry.installPackage(manifest('com.acme.carry')); + registry.installPackage(manifest('com.acme.carry'), 'code'); const disabled = registry.disablePackage('com.acme.carry'); const stampedAt = disabled?.statusChangedAt; expect(typeof stampedAt, 'precondition: the disable stamped a time').toBe('string'); - const again = registry.installPackage(manifest('com.acme.carry')); + const again = registry.installPackage(manifest('com.acme.carry'), 'code'); expect(again.enabled, 'the row the registry RETURNED').toBe(false); expect(again.status).toBe('disabled'); @@ -65,8 +65,8 @@ describe('#18877 — installPackage preserves an existing row\'s lifecycle state it('⛔ the control leg — a re-install does not disable an ENABLED row either', () => { // Without this, «preserve» is satisfiable by a registry that simply // stopped enabling anything. - registry.installPackage(manifest('com.acme.stayon')); - const again = registry.installPackage(manifest('com.acme.stayon')); + registry.installPackage(manifest('com.acme.stayon'), 'code'); + const again = registry.installPackage(manifest('com.acme.stayon'), 'code'); expect(again.enabled).toBe(true); expect(again.status).toBe('installed'); @@ -76,7 +76,7 @@ describe('#18877 — installPackage preserves an existing row\'s lifecycle state it('the boot seed still decides for an id that has NO row yet', () => { registry.setInitialDisabledPackageIds(['com.acme.seeded']); - const first = registry.installPackage(manifest('com.acme.seeded')); + const first = registry.installPackage(manifest('com.acme.seeded'), 'code'); expect(first.enabled, 'boot hydration must still replay a persisted disable').toBe(false); expect(first.status).toBe('disabled'); @@ -87,10 +87,10 @@ describe('#18877 — installPackage preserves an existing row\'s lifecycle state // The card's own defect, at this layer. Before #18877 the second // install read the seed again and re-landed the package disabled. registry.setInitialDisabledPackageIds(['com.acme.seeded.enabled']); - registry.installPackage(manifest('com.acme.seeded.enabled')); + registry.installPackage(manifest('com.acme.seeded.enabled'), 'code'); registry.enablePackage('com.acme.seeded.enabled'); - const again = registry.installPackage(manifest('com.acme.seeded.enabled')); + const again = registry.installPackage(manifest('com.acme.seeded.enabled'), 'code'); expect(again.enabled, 'the most recent explicit lifecycle action stands').toBe(true); expect(again.status).toBe('installed'); @@ -99,7 +99,7 @@ describe('#18877 — installPackage preserves an existing row\'s lifecycle state it('an id the seed never named still lands at the declared default', () => { registry.setInitialDisabledPackageIds(['com.acme.someone.else']); - const fresh = registry.installPackage(manifest('com.acme.unseeded')); + const fresh = registry.installPackage(manifest('com.acme.unseeded'), 'code'); expect(fresh.enabled).toBe(true); expect(fresh.status).toBe('installed'); }); @@ -128,11 +128,11 @@ describe('#18877 item 3 — uninstallPackage forgets the id from the boot seed', it('a package deleted and re-installed in the same boot is a FRESH install', () => { registry.setInitialDisabledPackageIds(['com.acme.gone']); - expect(registry.installPackage(manifest('com.acme.gone')).enabled, 'precondition: the seed applied').toBe(false); + expect(registry.installPackage(manifest('com.acme.gone'), 'code').enabled, 'precondition: the seed applied').toBe(false); expect(registry.uninstallPackage('com.acme.gone')).toBe(true); - const reinstalled = registry.installPackage(manifest('com.acme.gone')); + const reinstalled = registry.installPackage(manifest('com.acme.gone'), 'code'); expect( reinstalled.enabled, 'a row that no longer exists has no lifecycle state — and the seed must not supply one', @@ -142,10 +142,10 @@ describe('#18877 item 3 — uninstallPackage forgets the id from the boot seed', it('⛔ and it forgets only the id it was given', () => { registry.setInitialDisabledPackageIds(['com.acme.gone2', 'com.acme.neighbour']); - registry.installPackage(manifest('com.acme.gone2')); + registry.installPackage(manifest('com.acme.gone2'), 'code'); registry.uninstallPackage('com.acme.gone2'); - const neighbour = registry.installPackage(manifest('com.acme.neighbour')); + const neighbour = registry.installPackage(manifest('com.acme.neighbour'), 'code'); expect(neighbour.enabled, 'the neighbour\'s persisted disable is untouched').toBe(false); }); @@ -155,7 +155,7 @@ describe('#18877 item 3 — uninstallPackage forgets the id from the boot seed', // anyway, or a refused uninstall would silently re-enable the package // on its next registration. registry.setInitialDisabledPackageIds(['com.acme.extended']); - registry.installPackage(manifest('com.acme.extended', 'ext')); + registry.installPackage(manifest('com.acme.extended', 'ext'), 'code'); registry.registerObject({ name: 'account', fields: {} } as any, 'com.acme.extended', 'ext', 'own'); registry.registerObject({ name: 'account', fields: {} } as any, 'com.acme.extender', undefined, 'extend'); @@ -169,7 +169,7 @@ describe('#18877 item 3 — uninstallPackage forgets the id from the boot seed', registry.unregisterObjectsByPackage('com.acme.extender'); expect(registry.uninstallPackage('com.acme.extended'), 'now it really goes').toBe(true); expect( - registry.installPackage(manifest('com.acme.extended', 'ext')).enabled, + registry.installPackage(manifest('com.acme.extended', 'ext'), 'code').enabled, 'and only THEN is the id forgotten', ).toBe(true); }); diff --git a/packages/objectql/src/registry-log-level.test.ts b/packages/objectql/src/registry-log-level.test.ts index 2f27e0ecd43..fb25983293c 100644 --- a/packages/objectql/src/registry-log-level.test.ts +++ b/packages/objectql/src/registry-log-level.test.ts @@ -60,15 +60,15 @@ describe('SchemaRegistry log-level gating (#3420)', () => { const info = new SchemaRegistry({ multiTenant: false }); expect(info.logLevel).toBe('info'); - info.installPackage(manifest); - info.installPackage(manifest); // same-package reload → "Overwriting package" + info.installPackage(manifest, 'code'); + info.installPackage(manifest, 'code'); // same-package reload → "Overwriting package" expect(warn).not.toHaveBeenCalledWith(expect.stringContaining('Overwriting package')); expect(debug).not.toHaveBeenCalledWith(expect.stringContaining('Overwriting package')); debug.mockClear(); const dbg = new SchemaRegistry({ multiTenant: false, logLevel: 'debug' }); - dbg.installPackage(manifest); - dbg.installPackage(manifest); + dbg.installPackage(manifest, 'code'); + dbg.installPackage(manifest, 'code'); expect(debug).toHaveBeenCalledWith(expect.stringContaining('Overwriting package: com.test')); }); diff --git a/packages/objectql/src/registry-namespace-install-gate.test.ts b/packages/objectql/src/registry-namespace-install-gate.test.ts index 584f3eae2ac..cfab96e9dfa 100644 --- a/packages/objectql/src/registry-namespace-install-gate.test.ts +++ b/packages/objectql/src/registry-namespace-install-gate.test.ts @@ -30,16 +30,16 @@ describe('SchemaRegistry — namespace install gate (ADR-0048 Phase 1)', () => { }); it('refuses a package whose namespace is already owned by a different package', () => { - registry.installPackage(manifest('com.acme.crm', 'crm') as any); + registry.installPackage(manifest('com.acme.crm', 'crm') as any, 'code'); expect(() => - registry.installPackage(manifest('com.beta.crm', 'crm') as any), + registry.installPackage(manifest('com.beta.crm', 'crm') as any, 'code'), ).toThrowError(NamespaceConflictError); }); it('error names both packages and the namespace', () => { - registry.installPackage(manifest('com.acme.crm', 'crm') as any); + registry.installPackage(manifest('com.acme.crm', 'crm') as any, 'code'); try { - registry.installPackage(manifest('com.beta.crm', 'crm') as any); + registry.installPackage(manifest('com.beta.crm', 'crm') as any, 'code'); throw new Error('expected a namespace conflict error'); } catch (e) { expect(e).toBeInstanceOf(NamespaceConflictError); @@ -66,10 +66,10 @@ describe('SchemaRegistry — namespace install gate (ADR-0048 Phase 1)', () => { // after it. `resolveThrownHttpError` reads exactly these two fields off the // throw, so they are what the door's answer is MADE of — asserting the // class instead asserts something the wire never sees. - registry.installPackage(manifest('com.acme.crm', 'crm') as any); + registry.installPackage(manifest('com.acme.crm', 'crm') as any, 'code'); let caught: (Error & { code?: string; status?: number }) | undefined; try { - registry.installPackage(manifest('com.beta.crm', 'crm') as any); + registry.installPackage(manifest('com.beta.crm', 'crm') as any, 'code'); } catch (e) { caught = e as Error & { code?: string; status?: number }; } expect(caught?.code).toBe('NAMESPACE_CONFLICT'); @@ -80,24 +80,24 @@ describe('SchemaRegistry — namespace install gate (ADR-0048 Phase 1)', () => { }); it('allows the same package to reinstall/reload its own namespace', () => { - registry.installPackage(manifest('com.acme.crm', 'crm') as any); + registry.installPackage(manifest('com.acme.crm', 'crm') as any, 'code'); expect(() => - registry.installPackage(manifest('com.acme.crm', 'crm') as any), + registry.installPackage(manifest('com.acme.crm', 'crm') as any, 'code'), ).not.toThrow(); }); it('allows two packages with distinct namespaces', () => { - registry.installPackage(manifest('com.acme.crm', 'crm') as any); + registry.installPackage(manifest('com.acme.crm', 'crm') as any, 'code'); expect(() => - registry.installPackage(manifest('com.acme.hr', 'hr') as any), + registry.installPackage(manifest('com.acme.hr', 'hr') as any, 'code'), ).not.toThrow(); }); it('exempts shareable platform namespaces (base/system/sys)', () => { for (const ns of ['base', 'system', 'sys']) { - registry.installPackage(manifest(`com.a.${ns}`, ns) as any); + registry.installPackage(manifest(`com.a.${ns}`, ns) as any, 'code'); expect(() => - registry.installPackage(manifest(`com.b.${ns}`, ns) as any), + registry.installPackage(manifest(`com.b.${ns}`, ns) as any, 'code'), ).not.toThrow(); } }); @@ -105,19 +105,19 @@ describe('SchemaRegistry — namespace install gate (ADR-0048 Phase 1)', () => { it('downgrades to a warning under collisionPolicy "warn"', () => { const warnReg = new SchemaRegistry({ multiTenant: false, collisionPolicy: 'warn' }); warnReg.logLevel = 'silent'; - warnReg.installPackage(manifest('com.acme.crm', 'crm') as any); + warnReg.installPackage(manifest('com.acme.crm', 'crm') as any, 'code'); expect(() => - warnReg.installPackage(manifest('com.beta.crm', 'crm') as any), + warnReg.installPackage(manifest('com.beta.crm', 'crm') as any, 'code'), ).not.toThrow(); // Both packages are recorded; the namespace now has two owners. expect(warnReg.getNamespaceOwners('crm').sort()).toEqual(['com.acme.crm', 'com.beta.crm']); }); it('releases the namespace on uninstall, allowing a different package to claim it', () => { - registry.installPackage(manifest('com.acme.crm', 'crm') as any); + registry.installPackage(manifest('com.acme.crm', 'crm') as any, 'code'); registry.uninstallPackage('com.acme.crm'); expect(() => - registry.installPackage(manifest('com.beta.crm', 'crm') as any), + registry.installPackage(manifest('com.beta.crm', 'crm') as any, 'code'), ).not.toThrow(); expect(registry.getNamespaceOwners('crm')).toEqual(['com.beta.crm']); }); diff --git a/packages/objectql/src/registry-package-manifest-serializable.test.ts b/packages/objectql/src/registry-package-manifest-serializable.test.ts index 1f4a8dad473..dffbe2a6cbd 100644 --- a/packages/objectql/src/registry-package-manifest-serializable.test.ts +++ b/packages/objectql/src/registry-package-manifest-serializable.test.ts @@ -82,7 +82,7 @@ describe('SchemaRegistry.installPackage — the record is serializable', () => { it('survives a plugin instance that closes a cycle through the engine', () => { const plugin = new FakeConnectorPlugin(); plugin.init(makeCyclicEngine()); - registry.installPackage(baseManifest({ plugins: [plugin] })); + registry.installPackage(baseManifest({ plugins: [plugin] }), 'code'); // The expression every read door runs. expect(() => JSON.stringify(registry.getPackage('com.example.showcase'))).not.toThrow(); @@ -100,7 +100,7 @@ describe('SchemaRegistry.installPackage — the record is serializable', () => { const manifest = baseManifest({ plugins: [plugin] }); expect(() => JSON.stringify(manifest)).not.toThrow(); - registry.installPackage(manifest); + registry.installPackage(manifest, 'code'); plugin.init(makeCyclicEngine()); expect(() => JSON.stringify(registry.getPackage('com.example.showcase'))).not.toThrow(); @@ -109,7 +109,7 @@ describe('SchemaRegistry.installPackage — the record is serializable', () => { it('survives a reference cycle among PLAIN data in the manifest', () => { const cyclic: Record = { name: 'self' }; cyclic.self = cyclic; - registry.installPackage(baseManifest({ data: { node: cyclic } })); + registry.installPackage(baseManifest({ data: { node: cyclic } }), 'code'); const record = registry.getPackage('com.example.showcase')!; expect(() => JSON.stringify(record)).not.toThrow(); @@ -120,7 +120,7 @@ describe('SchemaRegistry.installPackage — the record is serializable', () => { it('keeps the declarative half of the manifest byte-for-byte', () => { const manifest = baseManifest({ plugins: [new FakeConnectorPlugin()] }); - registry.installPackage(manifest); + registry.installPackage(manifest, 'code'); const stored = registry.getPackage('com.example.showcase')!.manifest as any; for (const key of [ @@ -137,7 +137,7 @@ describe('SchemaRegistry.installPackage — the record is serializable', () => { onEnable: () => undefined, registryHandle: new Map([['a', 1]]), publishedAt, - })); + }), 'code'); const stored = registry.getPackage('com.example.showcase')!.manifest as any; expect(stored.onEnable).toBeUndefined(); @@ -151,7 +151,7 @@ describe('SchemaRegistry.installPackage — the record is serializable', () => { // Projecting must therefore copy, never strip in place. const plugin = new FakeConnectorPlugin(); const manifest = baseManifest({ plugins: [plugin] }); - registry.installPackage(manifest); + registry.installPackage(manifest, 'code'); expect(manifest.plugins).toHaveLength(1); expect(manifest.plugins[0]).toBe(plugin); @@ -159,10 +159,10 @@ describe('SchemaRegistry.installPackage — the record is serializable', () => { }); it('projects on REINSTALL too (rebuild / HMR overwrite)', () => { - registry.installPackage(baseManifest()); + registry.installPackage(baseManifest(), 'code'); const plugin = new FakeConnectorPlugin(); plugin.init(makeCyclicEngine()); - registry.installPackage(baseManifest({ plugins: [plugin] })); + registry.installPackage(baseManifest({ plugins: [plugin] }), 'code'); expect(() => JSON.stringify(registry.getPackage('com.example.showcase'))).not.toThrow(); }); @@ -172,7 +172,7 @@ describe('SchemaRegistry.installPackage — the record is serializable', () => { // caller's whole listing, not just the offending package. const plugin = new FakeConnectorPlugin(); plugin.init(makeCyclicEngine()); - registry.installPackage(baseManifest({ plugins: [plugin] })); + registry.installPackage(baseManifest({ plugins: [plugin] }), 'code'); registry.installPackage({ id: 'com.objectstack.setup', name: 'Setup', namespace: 'setup', version: '9.3.0', } as any); @@ -183,7 +183,7 @@ describe('SchemaRegistry.installPackage — the record is serializable', () => { it('still records the namespace and the lifecycle state it always did', () => { // The projection must not cost the record its non-manifest half. - registry.installPackage(baseManifest({ plugins: [new FakeConnectorPlugin()] })); + registry.installPackage(baseManifest({ plugins: [new FakeConnectorPlugin()] }), 'code'); const record = registry.getPackage('com.example.showcase')!; expect(record.status).toBe('installed'); @@ -227,7 +227,7 @@ describe('SchemaRegistry.installPackage — the record reports every declared fu }); it('records BOTH spellings — the bare one as a handler-less declaration', () => { - registry.installPackage(baseManifest({ functions: showcaseFunctions() })); + registry.installPackage(baseManifest({ functions: showcaseFunctions() }), 'code'); const stored = registry.getPackage('com.example.showcase')!.manifest as any; expect(Object.keys(stored.functions).sort()).toEqual(['summarizeCompletedTask', 'sweepProjectHealth']); @@ -240,7 +240,7 @@ describe('SchemaRegistry.installPackage — the record reports every declared fu }); it('⛔ mints no handler ref — an absent `handler` is the honest statement', () => { - registry.installPackage(baseManifest({ functions: showcaseFunctions() })); + registry.installPackage(baseManifest({ functions: showcaseFunctions() }), 'code'); const stored = registry.getPackage('com.example.showcase')!.manifest as any; for (const entry of Object.values(stored.functions) as Array>) { @@ -253,7 +253,7 @@ describe('SchemaRegistry.installPackage — the record reports every declared fu // no runtime module re-attached the callables: a bare ref and a lowered // record. Neither is a callable, so neither is normalised. const lowered = { bare: 'bare', declared: { handler: 'declared', effect: 'writes' } }; - registry.installPackage(baseManifest({ functions: lowered })); + registry.installPackage(baseManifest({ functions: lowered }), 'code'); expect((registry.getPackage('com.example.showcase')!.manifest as any).functions).toEqual(lowered); }); @@ -264,13 +264,13 @@ describe('SchemaRegistry.installPackage — the record reports every declared fu // default, so ⛔ nothing is written where the schema declares nothing. registry.installPackage(baseManifest({ functions: [{ name: 'syncBilling', handler: () => 'billed', effect: 'writes' }], - })); + }), 'code'); const stored = registry.getPackage('com.example.showcase')!.manifest as any; expect(stored.functions).toEqual([{ name: 'syncBilling', effect: 'writes' }]); const bare = new SchemaRegistry({ multiTenant: false, collisionPolicy: 'error' }); bare.logLevel = 'silent'; - bare.installPackage(baseManifest({ functions: [{ name: 'syncBilling', handler: () => 'billed' }] })); + bare.installPackage(baseManifest({ functions: [{ name: 'syncBilling', handler: () => 'billed' }] }), 'code'); expect((bare.getPackage('com.example.showcase')!.manifest as any).functions) .toEqual([{ name: 'syncBilling' }]); }); @@ -280,7 +280,7 @@ describe('SchemaRegistry.installPackage — the record reports every declared fu // object. Normalising must copy, exactly as the projection does. const functions = showcaseFunctions(); const manifest = baseManifest({ functions }); - registry.installPackage(manifest); + registry.installPackage(manifest, 'code'); expect(typeof manifest.functions.summarizeCompletedTask).toBe('function'); expect(manifest.functions).toBe(functions); @@ -292,7 +292,7 @@ describe('SchemaRegistry.installPackage — the record reports every declared fu // declared, must reach the projection as the SAME object — the normaliser // copies only when it really rewrote an entry. const untouched = baseManifest(); - registry.installPackage(untouched); + registry.installPackage(untouched, 'code'); const stored = registry.getPackage('com.example.showcase')!.manifest as any; expect('functions' in stored).toBe(false); expect(stored.objects).toEqual(untouched.objects); @@ -307,7 +307,7 @@ describe('SchemaRegistry.installPackage — the record reports every declared fu { name: 'on_insert', object: 'invoice', events: ['beforeInsert'], handler: () => 'hooked' }, { name: 'on_update', object: 'invoice', events: ['beforeUpdate'], handler: 'on_update' }, ], - })); + }), 'code'); const stored = registry.getPackage('com.example.showcase')!.manifest as any; expect(stored.hooks).toEqual([ diff --git a/packages/objectql/src/registry-security-catalog-names-held-by.test.ts b/packages/objectql/src/registry-security-catalog-names-held-by.test.ts index 0706dbcb409..075fb4984df 100644 --- a/packages/objectql/src/registry-security-catalog-names-held-by.test.ts +++ b/packages/objectql/src/registry-security-catalog-names-held-by.test.ts @@ -21,22 +21,22 @@ describe('SchemaRegistry.securityCatalogNamesHeldBy', () => { }); it('answers the permission-set names a package declared, and no other package\'s', () => { - registry.installPackage(manifest('com.acme.tasks', ['tasks_user', 'tasks_admin'])); - registry.installPackage(manifest('com.acme.other', ['other_user'])); + registry.installPackage(manifest('com.acme.tasks', ['tasks_user', 'tasks_admin']), 'code'); + registry.installPackage(manifest('com.acme.other', ['other_user']), 'code'); expect(registry.securityCatalogNamesHeldBy('com.acme.tasks', 'permission')).toEqual(['tasks_admin', 'tasks_user']); expect(registry.securityCatalogNamesHeldBy('com.acme.tasks', 'position')).toEqual([]); }); it('a disabled package still holds its names', () => { - registry.installPackage(manifest('com.acme.tasks', ['tasks_user'])); + registry.installPackage(manifest('com.acme.tasks', ['tasks_user']), 'code'); registry.disablePackage('com.acme.tasks'); expect(registry.securityCatalogNamesHeldBy('com.acme.tasks', 'permission')).toEqual(['tasks_user']); }); it('nothing once the package is uninstalled — which is why the doors read it first', () => { - registry.installPackage(manifest('com.acme.tasks', ['tasks_user'])); + registry.installPackage(manifest('com.acme.tasks', ['tasks_user']), 'code'); registry.uninstallPackage('com.acme.tasks'); expect(registry.securityCatalogNamesHeldBy('com.acme.tasks', 'permission')).toEqual([]); diff --git a/packages/objectql/src/registry-security-catalog-namespace.test.ts b/packages/objectql/src/registry-security-catalog-namespace.test.ts index 7122b94af11..67a3e31da32 100644 --- a/packages/objectql/src/registry-security-catalog-namespace.test.ts +++ b/packages/objectql/src/registry-security-catalog-namespace.test.ts @@ -252,8 +252,8 @@ describe('the refusal reports, and only refuses, what the ruling covers', () => it('collisionPolicy "warn" does not downgrade it', async () => { const registry = new SchemaRegistry({ collisionPolicy: 'warn' }); registry.logLevel = 'silent'; - registry.installPackage(pkg('com.acme.first', { position: ['p1'] }) as never); - const err = await caught(() => registry.installPackage(pkg('com.acme.second', { position: ['p1'] }) as never)); + registry.installPackage(pkg('com.acme.first', { position: ['p1'] }) as never, 'code'); + const err = await caught(() => registry.installPackage(pkg('com.acme.second', { position: ['p1'] }) as never, 'code')); expectRefusal(err, 'com.acme.second', { kind: 'package', packageId: 'com.acme.first' }); }); diff --git a/packages/objectql/src/registry.test.ts b/packages/objectql/src/registry.test.ts index 29e979e8407..358cceba279 100644 --- a/packages/objectql/src/registry.test.ts +++ b/packages/objectql/src/registry.test.ts @@ -476,7 +476,7 @@ describe('SchemaRegistry', () => { describe('Package Management', () => { it('should install package with namespace', () => { const manifest = { id: 'com.test', name: 'Test', namespace: 'test', version: '1.0.0' }; - const pkg = registry.installPackage(manifest as any); + const pkg = registry.installPackage(manifest as any, 'code'); expect(pkg.status).toBe('installed'); expect(registry.getNamespaceOwner('test')).toBe('com.test'); @@ -484,7 +484,7 @@ describe('SchemaRegistry', () => { it('should uninstall package and release namespace', () => { const manifest = { id: 'com.test', name: 'Test', namespace: 'test', version: '1.0.0' }; - registry.installPackage(manifest as any); + registry.installPackage(manifest as any, 'code'); registry.uninstallPackage('com.test'); expect(registry.getPackage('com.test')).toBeUndefined(); diff --git a/packages/runtime/src/app-plugin.disabled-seed.test.ts b/packages/runtime/src/app-plugin.disabled-seed.test.ts index da4d35a7ee3..65dfd66a4f3 100644 --- a/packages/runtime/src/app-plugin.disabled-seed.test.ts +++ b/packages/runtime/src/app-plugin.disabled-seed.test.ts @@ -114,7 +114,7 @@ describe('empty-env boot seeds persisted package disable-state (#5047)', () => { const { kernel, registry } = await bootEmptyEnv(); // Nothing came from the (empty) artifact; this is the post-boot // registration every package in such an env goes through. - registry.installPackage(manifestFor(DISABLED_ID)); + registry.installPackage(manifestFor(DISABLED_ID), 'code'); expect(registry.getPackage(DISABLED_ID)).toMatchObject({ status: 'disabled', @@ -128,7 +128,7 @@ describe('empty-env boot seeds persisted package disable-state (#5047)', () => { setPackageDisabled(ENVIRONMENT_ID, DISABLED_ID, true); const { kernel, registry } = await bootEmptyEnv(); - registry.installPackage(manifestFor(ENABLED_ID)); + registry.installPackage(manifestFor(ENABLED_ID), 'code'); expect(registry.getPackage(ENABLED_ID)).toMatchObject({ status: 'installed', @@ -170,7 +170,7 @@ describe('empty-env boot seeds persisted package disable-state (#5047)', () => { setPackageDisabled(ENVIRONMENT_ID, DISABLED_ID, false); const { kernel, registry } = await bootEmptyEnv(); - registry.installPackage(manifestFor(DISABLED_ID)); + registry.installPackage(manifestFor(DISABLED_ID), 'code'); expect(registry.getPackage(DISABLED_ID)).toMatchObject({ status: 'installed', @@ -182,7 +182,7 @@ describe('empty-env boot seeds persisted package disable-state (#5047)', () => { it('boots an empty env with no persisted state at all (nothing to seed)', async () => { const { kernel, registry } = await bootEmptyEnv(); - registry.installPackage(manifestFor(DISABLED_ID)); + registry.installPackage(manifestFor(DISABLED_ID), 'code'); expect(registry.getPackage(DISABLED_ID)).toMatchObject({ enabled: true }); diff --git a/packages/runtime/src/domains/packages-read-delete-response-conformance.test.ts b/packages/runtime/src/domains/packages-read-delete-response-conformance.test.ts index 82debf602e4..628c939eef4 100644 --- a/packages/runtime/src/domains/packages-read-delete-response-conformance.test.ts +++ b/packages/runtime/src/domains/packages-read-delete-response-conformance.test.ts @@ -167,7 +167,7 @@ function matchesWhere(row: any, where: any): boolean { function dispatcher(manifests: any[], protocol?: unknown): HttpDispatcher { const registry = new SchemaRegistry({ multiTenant: false, collisionPolicy: 'error' }); (registry as any).logLevel = 'silent'; - for (const m of manifests) registry.installPackage(m as any); + for (const m of manifests) registry.installPackage(m as any, 'code'); const ql = bindCatalogFromTables({ registry, diff --git a/packages/runtime/src/domains/packages-readonly-gate.test.ts b/packages/runtime/src/domains/packages-readonly-gate.test.ts index 4ae194c18dd..ca0b04dd6b8 100644 --- a/packages/runtime/src/domains/packages-readonly-gate.test.ts +++ b/packages/runtime/src/domains/packages-readonly-gate.test.ts @@ -82,10 +82,10 @@ function manifest(id: string, scope?: 'system' | 'cloud' | 'project') { */ function make() { const registry = new SchemaRegistry({ logLevel: 'silent' } as any); - registry.installPackage(manifest(PLATFORM_SCOPED, 'system')); - registry.installPackage(manifest(CLOUD_SCOPED, 'cloud')); - registry.installPackage(manifest(CODE_LOADED, 'project')); - registry.installPackage(manifest(WRITABLE, 'project')); + registry.installPackage(manifest(PLATFORM_SCOPED, 'system'), 'code'); + registry.installPackage(manifest(CLOUD_SCOPED, 'cloud'), 'code'); + registry.installPackage(manifest(CODE_LOADED, 'project'), 'code'); + registry.installPackage(manifest(WRITABLE, 'project'), 'authored'); // Only the code-loaded package booted from an artifact. const manifests = new Map([[CODE_LOADED, manifest(CODE_LOADED)]]); @@ -106,7 +106,7 @@ function make() { // that can never go red. With the mint in place, deleting the gate makes // those cases fail on the listing, not merely on the status. duplicatePackage: vi.fn(async (req: any) => { - registry.installPackage(manifest(req.targetPackageId)); + registry.installPackage(manifest(req.targetPackageId), 'authored'); // The exact empty-success body the card measured on a running code // package: HTTP 200, `success: false`, nothing copied, nothing named // as failed. diff --git a/packages/runtime/src/domains/packages-writable-verdict.test.ts b/packages/runtime/src/domains/packages-writable-verdict.test.ts index 983faa35ccf..99fe4086a2d 100644 --- a/packages/runtime/src/domains/packages-writable-verdict.test.ts +++ b/packages/runtime/src/domains/packages-writable-verdict.test.ts @@ -37,6 +37,8 @@ import { describe, it, expect } from 'vitest'; import { SchemaRegistry } from '@objectstack/objectql'; import { HttpDispatcher } from '../http-dispatcher.js'; +import { InstalledPackageSourceSchema } from '@objectstack/spec/kernel'; +import { GetInstalledPackageResponseSchema, ListInstalledPackagesResponseSchema } from '@objectstack/spec/api-assembled'; // ── the four shapes ────────────────────────────────────────────────────────── /** Pin 1 — booted code package, explicit `scope: 'project'` (today's hotcrm shape). */ @@ -76,12 +78,12 @@ function manifest(id: string, extra: Record = {}) { */ function make() { const registry = new SchemaRegistry({ logLevel: 'silent' } as any); - registry.installPackage(manifest(CODE_PROJECT, { scope: 'project', type: 'app' })); - registry.installPackage(manifest(CODE_MODULE, { type: 'module' })); - registry.installPackage(manifest(SYSTEM_SCOPED, { scope: 'system' })); - registry.installPackage(manifest(CLOUD_SCOPED, { scope: 'cloud' })); - registry.installPackage(manifest(DB_BASE)); - registry.installPackage(manifest(DB_PROJECT, { scope: 'project' })); + registry.installPackage(manifest(CODE_PROJECT, { scope: 'project', type: 'app' }), 'code'); + registry.installPackage(manifest(CODE_MODULE, { type: 'module' }), 'marketplace'); + registry.installPackage(manifest(SYSTEM_SCOPED, { scope: 'system' }), 'code'); + registry.installPackage(manifest(CLOUD_SCOPED, { scope: 'cloud' }), 'code'); + registry.installPackage(manifest(DB_BASE), 'authored'); + registry.installPackage(manifest(DB_PROJECT, { scope: 'project' }), 'authored'); // Only the two code packages are booted — this is what `ObjectQL.registerApp` // records, both for every package of a loaded artifact (which is CODE_PROJECT, @@ -227,3 +229,35 @@ describe('GET /packages — the verdict is additive and computed, never stored ( expect(r.response?.body?.data?.packages).toHaveLength(6); }); }); + +// ══════════════════════════════════════════════════════════════════════════════ +// 4. The row's origin — `source`, read off the record the registering door wrote +// ══════════════════════════════════════════════════════════════════════════════ + +describe('GET /packages — every row carries the `source` its registering door stamped (#23972)', () => { + it('serves each row\'s source as recorded, on the list and the detail door', async () => { + const { dispatcher } = make(); + const rows = await list(dispatcher); + expect(Object.fromEntries(rows.map((row) => [row.manifest.id, row.source]))).toEqual({ + [CODE_PROJECT]: 'code', + [CODE_MODULE]: 'marketplace', + [SYSTEM_SCOPED]: 'code', + [CLOUD_SCOPED]: 'code', + [DB_BASE]: 'authored', + [DB_PROJECT]: 'authored', + }); + const r = await dispatcher.handlePackages(`/${CODE_MODULE}`, 'GET', undefined, {}, reader()); + expect(r.response?.body?.data?.source).toBe('marketplace'); + }); + + it('the served body parses through its declared contract, `source` and `writable` kept', async () => { + const r = await make().dispatcher.handlePackages('/', 'GET', undefined, {}, reader()); + const parsed = ListInstalledPackagesResponseSchema.parse(r.response?.body); + for (const row of parsed.data.packages) { + expect(InstalledPackageSourceSchema.options).toContain(row.source); + expect(typeof row.writable).toBe('boolean'); + } + const detail = await make().dispatcher.handlePackages(`/${DB_BASE}`, 'GET', undefined, {}, reader()); + expect(GetInstalledPackageResponseSchema.parse(detail.response?.body).data).toMatchObject({ source: 'authored', writable: true }); + }); +}); diff --git a/packages/services/service-package/src/hydration.test.ts b/packages/services/service-package/src/hydration.test.ts index 7f28aa3ee3b..41ebfbd4a92 100644 --- a/packages/services/service-package/src/hydration.test.ts +++ b/packages/services/service-package/src/hydration.test.ts @@ -57,9 +57,25 @@ describe('PackageServicePlugin boot hydration (ADR-0033 consolidation)', () => { expect(registry.installPackage).toHaveBeenCalledTimes(1); expect(registry.installPackage).toHaveBeenCalledWith( expect.objectContaining({ id: 'app.seed', version: '1.0.0' }), + 'authored', ); }); + // [#23972] The `sys_packages` rehydrate is the restart half of an authored + // install: the row it puts back must say `source: 'authored'`, the origin the + // install stamped, never fall to the registry's other doors' values. + it('rehydrates a sys_packages row as source authored', async () => { + const registry = { + installPackage: vi.fn(), + getPackage: vi.fn(() => undefined), + }; + const { ctx } = makeCtx(registry); + + await new PackageServicePlugin().start(ctx); + + expect(registry.installPackage.mock.calls.map((call) => call[1])).toEqual(['authored']); + }); + it('does NOT overwrite a package already registered (e.g. from the filesystem)', async () => { const registry = { installPackage: vi.fn(), diff --git a/packages/spec/authorable-surface/api.json b/packages/spec/authorable-surface/api.json index 884e04b86d1..5c3e9ec1940 100644 --- a/packages/spec/authorable-surface/api.json +++ b/packages/spec/authorable-surface/api.json @@ -200,6 +200,7 @@ "api/AssembledInstalledPackage:previousVersion", "api/AssembledInstalledPackage:registeredNamespaces", "api/AssembledInstalledPackage:settings", + "api/AssembledInstalledPackage:source", "api/AssembledInstalledPackage:status", "api/AssembledInstalledPackage:statusChangedAt", "api/AssembledInstalledPackage:updatedAt", diff --git a/packages/spec/authorable-surface/kernel.json b/packages/spec/authorable-surface/kernel.json index 0bf45f41313..7b150841a34 100644 --- a/packages/spec/authorable-surface/kernel.json +++ b/packages/spec/authorable-surface/kernel.json @@ -232,6 +232,7 @@ "kernel/InstalledPackage:previousVersion", "kernel/InstalledPackage:registeredNamespaces", "kernel/InstalledPackage:settings", + "kernel/InstalledPackage:source", "kernel/InstalledPackage:status", "kernel/InstalledPackage:statusChangedAt", "kernel/InstalledPackage:updatedAt", diff --git a/packages/spec/json-schema.manifest/kernel.json b/packages/spec/json-schema.manifest/kernel.json index fd6dcedbd8d..2ab9ed9e83a 100644 --- a/packages/spec/json-schema.manifest/kernel.json +++ b/packages/spec/json-schema.manifest/kernel.json @@ -51,6 +51,7 @@ "kernel/InstallPackageRequest", "kernel/InstallPackageResponse", "kernel/InstalledPackage", + "kernel/InstalledPackageSource", "kernel/KernelContext", "kernel/KernelSecurityPolicy", "kernel/ListPackagesRequest", diff --git a/packages/spec/src/api/package-api.test.ts b/packages/spec/src/api/package-api.test.ts index 8c3c5347aa5..7fa47707858 100644 --- a/packages/spec/src/api/package-api.test.ts +++ b/packages/spec/src/api/package-api.test.ts @@ -636,7 +636,10 @@ describe('package-rollback-response retirement (ruled: it described the wrong op * Everything else is held equal on purpose: what separates them has to be the * stage, so a failure below can only be about the stage. */ -const LIFECYCLE = { status: 'installed', enabled: true } as const; +// `source` is the record's origin (every row carries one, #23972); `writable` +// is the served row's verdict, which the record schemas do not declare and +// strip, so one fixture serves both. +const LIFECYCLE = { source: 'code', status: 'installed', enabled: true, writable: false } as const; const MANIFEST_BASE = { id: 'com.acme.stage', namespace: 'stage', version: '1.0.0', type: 'app', scope: 'project', name: 'Stage Fixture', @@ -682,6 +685,30 @@ describe('the two declared manifest stages are DISTINCT, not two names for one s }); }); +// [#23972] The read doors serve the server's writability verdict on every row; +// it is declared on the SERVED row (both stage branches) and on neither record +// schema, because the registry never stores it. +describe('the served installed-package row declares `writable`', () => { + it('requires a boolean `writable` on either stage', () => { + for (const row of [GLOB_ROW, ASSEMBLED_ROW]) { + const { writable: _omitted, ...bare } = row; + const verdict = InstalledPackageAtEitherStageSchema.safeParse(bare); + expect(verdict.success).toBe(false); + expect(InstalledPackageAtEitherStageSchema.safeParse({ ...row, writable: 'yes' }).success).toBe(false); + } + }); + + it('keeps `writable` and `source` on the parsed served row', () => { + const parsed = InstalledPackageAtEitherStageSchema.parse({ ...GLOB_ROW, source: 'authored', writable: true }); + expect(parsed).toMatchObject({ source: 'authored', writable: true }); + }); + + it('is not part of the registry RECORD: the record schemas strip it', () => { + expect(InstalledPackageSchema.parse(GLOB_ROW)).not.toHaveProperty('writable'); + expect(AssembledInstalledPackageSchema.parse(ASSEMBLED_ROW)).not.toHaveProperty('writable'); + }); +}); + describe('`InstalledPackageAtEitherStageSchema` admits both stages and NOTHING else', () => { it('parses the authoring row', () => { expect(InstalledPackageAtEitherStageSchema.safeParse(GLOB_ROW).success).toBe(true); diff --git a/packages/spec/src/kernel/package-registry.test.ts b/packages/spec/src/kernel/package-registry.test.ts index 842b40a715a..18c9f1b940a 100644 --- a/packages/spec/src/kernel/package-registry.test.ts +++ b/packages/spec/src/kernel/package-registry.test.ts @@ -2,6 +2,7 @@ import { describe, it, expect } from 'vitest'; import { PackageStatusEnum, InstalledPackageSchema, + InstalledPackageSourceSchema, ListPackagesRequestSchema, ListPackagesResponseSchema, GetPackageRequestSchema, @@ -39,6 +40,30 @@ describe('PackageStatusEnum', () => { }); }); +// [#23972] Every row says where it came from, from a closed set; the set is +// the ruling's (code / marketplace / authored) and a row without it, or with a +// value outside it, is refused — no reader is left to reconstruct it. +describe('InstalledPackageSchema.source', () => { + it('accepts each of the three closed origins', () => { + for (const source of ['code', 'marketplace', 'authored'] as const) { + expect(InstalledPackageSchema.safeParse({ manifest: validManifest, source }).success, source).toBe(true); + } + expect(InstalledPackageSourceSchema.options).toEqual(['code', 'marketplace', 'authored']); + }); + + it('refuses a value outside the closed set, at the source key', () => { + const verdict = InstalledPackageSchema.safeParse({ manifest: validManifest, source: 'cloud' }); + expect(verdict.success).toBe(false); + expect(verdict.error?.issues.map((issue) => [issue.code, issue.path.join('.')])).toEqual([['invalid_value', 'source']]); + }); + + it('refuses a row that does not say where it came from', () => { + const verdict = InstalledPackageSchema.safeParse({ manifest: validManifest }); + expect(verdict.success).toBe(false); + expect(verdict.error?.issues.map((issue) => issue.path.join('.'))).toEqual(['source']); + }); +}); + describe('InstalledPackageSchema', () => { it('should accept minimal installed package', () => { const pkg = { manifest: validManifest }; From 790c4c2c5263761424767a541714a9646e4fd037 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 17:37:14 +0000 Subject: [PATCH 03/10] test(packages): registry fixtures state the package source; client served-row pins carry writable Claude-Session: https://claude.ai/code/session_01RGdjFAX2oq1VdGNT5WGdKN Co-authored-by: Claude --- packages/client/src/return-type-precision.test.ts | 12 +++++++----- .../metadata-protocol/src/durable-package.test.ts | 10 +++++----- .../src/package-unmanaged-install.test.ts | 2 +- .../src/protocol.package-delete-refusal.test.ts | 4 ++-- .../src/protocol.package-persist-failure.test.ts | 10 ++++++---- .../objectql/src/package-disable-enforcement.test.ts | 2 +- .../src/protocol-lock-artifact-package-axis.test.ts | 2 +- .../src/protocol-object-overlay-layer.test.ts | 2 +- .../src/protocol-packaged-object-base.test.ts | 2 +- ...blish-package-drafts-response-conformance.test.ts | 2 +- .../registry-assert-package-uninstallable.test.ts | 2 +- .../objectql/src/registry-i18n-bundle-key.test.ts | 2 +- .../src/registry-install-preserves-lifecycle.test.ts | 4 ++-- .../registry-package-manifest-serializable.test.ts | 2 +- .../src/registry-unregister-items-by-package.test.ts | 6 +++--- packages/objectql/src/registry.test.ts | 10 +++++----- .../runtime/src/app-plugin.disabled-seed.test.ts | 2 +- .../automation-flow-door-package-address.test.ts | 4 ++-- .../src/domains/dispatcher-query-arity.test.ts | 4 ++-- .../domains/package-door-producer-key-carry.test.ts | 6 +++--- .../src/domains/packages-get-version-scope.test.ts | 2 +- .../src/domains/packages-list-enabled-filter.test.ts | 2 +- .../packages-orgless-grants-capability-gate.test.ts | 2 +- .../domains/packages-serializable-response.test.ts | 4 ++-- .../runtime/src/domains/packages-single-door.test.ts | 2 +- .../packages-uninstall-refuse-before-mutate.test.ts | 2 +- .../src/domains/packages-write-flag-arity.test.ts | 2 +- .../src/package-door-namespace-conflict-code.test.ts | 2 +- ...e-revert-code-shipped-members.integration.test.ts | 6 +++--- 29 files changed, 59 insertions(+), 55 deletions(-) diff --git a/packages/client/src/return-type-precision.test.ts b/packages/client/src/return-type-precision.test.ts index dae16a6812b..e5819f272ff 100644 --- a/packages/client/src/return-type-precision.test.ts +++ b/packages/client/src/return-type-precision.test.ts @@ -307,9 +307,9 @@ export function searchResultIsNotTheGlobalSearchShape(): void { export async function returnTypePrecisionPins11925(): Promise { // ── bound 1/3: both surfaces agree on this envelope ────────────────── // `runtime`'s `/packages` domain and `rest`'s `GET {base}/packages` both - // send `{ packages, total: packages.length }`. The REST rows also carry a - // `source` discriminator the dispatcher rows lack, so it stays undeclared - // — the same treatment #8140 gave the scoped sibling directly above. + // send `{ packages, total: packages.length }`. (The REST twin's + // `source: 'registry' | 'database' | 'both'` stamp left with its routes in + // #14503; the `source` the rows carry now is the declared origin, #23972.) // ⚠️ [#17536] The ELEMENT moved, the envelope did not — this pin is still // #11925's statement that both mounted surfaces agree on // `{ packages, total }`, and that half is unchanged. @@ -645,8 +645,10 @@ export async function returnTypePrecisionPins12034(): Promise { void (await scoped.packages.get('com.acme.crm')).package; } -declare const authoringRow: InstalledPackage; -declare const assembledRow: AssembledInstalledPackage; +// A SERVED row is the registry record plus the read door's own verdict, +// `writable` (#23972 declared it on the served row, not on the record). +declare const authoringRow: InstalledPackage & { writable: boolean }; +declare const assembledRow: AssembledInstalledPackage & { writable: boolean }; /** * [#17536] The four `/packages` READ members declare the stage the DOOR is diff --git a/packages/metadata-protocol/src/durable-package.test.ts b/packages/metadata-protocol/src/durable-package.test.ts index e91c967891c..f2f354266a9 100644 --- a/packages/metadata-protocol/src/durable-package.test.ts +++ b/packages/metadata-protocol/src/durable-package.test.ts @@ -51,19 +51,19 @@ function makeImpl(overrides?: { /** * Manifests to replay into the fresh registry before the test runs — a * stand-in for the boot hydration `PackageServicePlugin.start()` performs - * (`for (const rec of await packageService.list()) registry.installPackage(rec.manifest)`, + * (`for (const rec of await packageService.list()) registry.installPackage(rec.manifest, 'authored')`, * `packages/services/service-package/src/index.ts`). A host composed with * the rows a previous host persisted IS that process after a restart; a host * composed with none is a restart that found `sys_packages` empty. */ hydrate?: readonly { id: string; [key: string]: unknown }[]; }) { - const registryCalls: Array<{ manifest: any; settings: any }> = []; + const registryCalls: Array<{ manifest: any; source: string; settings: any }> = []; const installed = new Map(); const registry = { - installPackage: (manifest: any, settings?: any) => { - registryCalls.push({ manifest, settings }); - const pkg = { manifest, status: 'installed', enabled: true }; + installPackage: (manifest: any, source: string, settings?: any) => { + registryCalls.push({ manifest, source, settings }); + const pkg = { manifest, source, status: 'installed', enabled: true }; installed.set(manifest.id, pkg); return pkg; }, diff --git a/packages/metadata-protocol/src/package-unmanaged-install.test.ts b/packages/metadata-protocol/src/package-unmanaged-install.test.ts index afa8f55f6b1..5c8a34122d7 100644 --- a/packages/metadata-protocol/src/package-unmanaged-install.test.ts +++ b/packages/metadata-protocol/src/package-unmanaged-install.test.ts @@ -824,7 +824,7 @@ describe('importUnmanagedPackage — the verb', () => { it('refuses a target id that names an installed package — never into an existing base', async () => { const { protocol, engine, writes } = makeProtocol(); - engine.registry.installPackage({ id: 'com.example.mine', name: 'Mine' }); + engine.registry.installPackage({ id: 'com.example.mine', name: 'Mine' }, 'code'); const err = await refusalOf(() => protocol.importUnmanagedPackage({ manifest: crmBody(), targetPackageId: 'com.example.mine', })); diff --git a/packages/metadata-protocol/src/protocol.package-delete-refusal.test.ts b/packages/metadata-protocol/src/protocol.package-delete-refusal.test.ts index 35d3a26b279..14ddf38831b 100644 --- a/packages/metadata-protocol/src/protocol.package-delete-refusal.test.ts +++ b/packages/metadata-protocol/src/protocol.package-delete-refusal.test.ts @@ -110,8 +110,8 @@ const thrownDatabaseError: StoreDelete = async () => { function boot(world: World, storeDelete: StoreDelete = landed, opts: { uninstallRefusal?: Error } = {}) { const rows = new Map; status: string; enabled: boolean }>(); const registry = { - installPackage(manifest: Record) { - const row = { manifest: { ...manifest }, status: 'installed', enabled: true }; + installPackage(manifest: Record, source: string) { + const row = { manifest: { ...manifest }, source, status: 'installed', enabled: true }; rows.set(String(manifest.id), row); return row; }, diff --git a/packages/metadata-protocol/src/protocol.package-persist-failure.test.ts b/packages/metadata-protocol/src/protocol.package-persist-failure.test.ts index e7c2da04170..b570f087b10 100644 --- a/packages/metadata-protocol/src/protocol.package-persist-failure.test.ts +++ b/packages/metadata-protocol/src/protocol.package-persist-failure.test.ts @@ -46,6 +46,7 @@ import { ObjectStackProtocolImplementation } from './index.js'; interface Row { manifest: Record; + source: string; status: string; enabled: boolean; installedAt: string; @@ -61,10 +62,11 @@ function makeRegistry() { return { rows, namespaces, - installPackage(manifest: any, settings?: unknown): Row { + installPackage(manifest: any, source: string, settings?: unknown): Row { const existing = rows.get(manifest.id); const row: Row = { manifest: { ...manifest }, + source, status: existing?.status ?? 'installed', enabled: existing?.enabled ?? true, installedAt: stamp(), @@ -171,7 +173,7 @@ describe('#21243 installPackage — a refused sys_packages write fails the insta it('a re-install over an existing row puts the PRIOR row back, lifecycle included', async () => { const { impl, registry } = makeImpl(returnedDriverFault); - const prior = registry.installPackage({ id: 'com.example.leave', name: 'Leave v1', version: '1.0.0', namespace: 'leave' }); + const prior = registry.installPackage({ id: 'com.example.leave', name: 'Leave v1', version: '1.0.0', namespace: 'leave' }, 'authored'); registry.disablePackage('com.example.leave'); const priorContent = JSON.parse(JSON.stringify(registry.getPackage('com.example.leave'))); @@ -221,7 +223,7 @@ describe('#21243 updatePackage — a refused sys_packages write fails the edit a ['thrown DATABASE_ERROR', 'DATABASE_ERROR', thrownDatabaseError], ] as const)('%s → 500 %s, the prior manifest back in place', async (_label, code, publish) => { const { impl, registry } = makeImpl(publish); - const row = registry.installPackage({ id: 'com.example.leave', name: 'Leave', version: '1.0.0' }); + const row = registry.installPackage({ id: 'com.example.leave', name: 'Leave', version: '1.0.0' }, 'authored'); const manifestObject = row.manifest; const priorContent = JSON.parse(JSON.stringify(row)); @@ -243,7 +245,7 @@ describe('#21243 updatePackage — a refused sys_packages write fails the edit a it('CONTROL — a landed write answers the edited row', async () => { const { impl, registry } = makeImpl(async () => ({ success: true })); - registry.installPackage({ id: 'com.example.leave', name: 'Leave', version: '1.0.0' }); + registry.installPackage({ id: 'com.example.leave', name: 'Leave', version: '1.0.0' }, 'authored'); const res = await impl.updatePackage({ packageId: 'com.example.leave', patch: { description: 'patched' } }); diff --git a/packages/objectql/src/package-disable-enforcement.test.ts b/packages/objectql/src/package-disable-enforcement.test.ts index 5048415d737..f26100cf360 100644 --- a/packages/objectql/src/package-disable-enforcement.test.ts +++ b/packages/objectql/src/package-disable-enforcement.test.ts @@ -174,7 +174,7 @@ async function seed(protocol: any, registry: SchemaRegistry) { [PKG, 'demo_widget', 'demo_app'], [OTHER_PKG, 'healthy_widget', 'healthy_app'], ] as const) { - registry.installPackage({ id: pkg, name: pkg, version: '1.0.0' } as any); + registry.installPackage({ id: pkg, name: pkg, version: '1.0.0' } as any, 'code'); await protocol.saveMetaItem({ type: 'object', name: obj, packageId: pkg, item: objectBody(obj) }); // `app` rather than `view` as the non-object specimen: a `view` is saved as // an ADR-0017 CONTAINER and only becomes independently listable once the diff --git a/packages/objectql/src/protocol-lock-artifact-package-axis.test.ts b/packages/objectql/src/protocol-lock-artifact-package-axis.test.ts index 46b5404e429..375ee3d7ef6 100644 --- a/packages/objectql/src/protocol-lock-artifact-package-axis.test.ts +++ b/packages/objectql/src/protocol-lock-artifact-package-axis.test.ts @@ -55,7 +55,7 @@ function protocolFor(arrangement: Arrangement, order: readonly string[]) { } as Record, 'name' as never, packageId); } for (const packageId of arrangement.disabled ?? []) { - registry.installPackage({ id: packageId, name: packageId, version: '1.0.0', type: 'app' } as never); + registry.installPackage({ id: packageId, name: packageId, version: '1.0.0', type: 'app' } as never, 'code'); registry.disablePackage(packageId); } const rows = arrangement.rowLock === undefined ? [] : [{ diff --git a/packages/objectql/src/protocol-object-overlay-layer.test.ts b/packages/objectql/src/protocol-object-overlay-layer.test.ts index 01a2a774ce1..d29c012aa85 100644 --- a/packages/objectql/src/protocol-object-overlay-layer.test.ts +++ b/packages/objectql/src/protocol-object-overlay-layer.test.ts @@ -218,7 +218,7 @@ async function persistOverlayRow(name: string, packageId?: string): Promise - registry.installPackage({ id, name: 'My App', version: '1.0.0' } as any); + registry.installPackage({ id, name: 'My App', version: '1.0.0' } as any, 'code'); const ownerPackageId = (registry: SchemaRegistry, name: string) => registry.getObjectOwner(name)?.packageId; diff --git a/packages/objectql/src/protocol-packaged-object-base.test.ts b/packages/objectql/src/protocol-packaged-object-base.test.ts index 5f7b1c8daea..94c45ebc7c7 100644 --- a/packages/objectql/src/protocol-packaged-object-base.test.ts +++ b/packages/objectql/src/protocol-packaged-object-base.test.ts @@ -120,7 +120,7 @@ function makeSession() { /** The packaged object, as a code package registers it. */ function registerPackaged(registry: SchemaRegistry) { - registry.installPackage({ id: PKG, name: 'Showcase', version: '1.0.0' } as any); + registry.installPackage({ id: PKG, name: 'Showcase', version: '1.0.0' } as any, 'code'); registry.registerObject( { name: OBJ, diff --git a/packages/objectql/src/publish-package-drafts-response-conformance.test.ts b/packages/objectql/src/publish-package-drafts-response-conformance.test.ts index 0292e9ed854..e80fad1d2b7 100644 --- a/packages/objectql/src/publish-package-drafts-response-conformance.test.ts +++ b/packages/objectql/src/publish-package-drafts-response-conformance.test.ts @@ -141,7 +141,7 @@ async function makeProtocol() { // The base every draft here is bound to is INSTALLED: a flow may be saved // only into a package the registry holds (#20863). Its manifest declares // no namespace, so the ADR-0028 prefix pre-flight is unchanged. - engine.registry.installPackage({ id: PKG, name: 'Edu', version: '1.0.0' } as never); + engine.registry.installPackage({ id: PKG, name: 'Edu', version: '1.0.0' } as never, 'code'); return new ObjectStackProtocolImplementation(engine); } diff --git a/packages/objectql/src/registry-assert-package-uninstallable.test.ts b/packages/objectql/src/registry-assert-package-uninstallable.test.ts index bce59b5ca1b..89c790a6af8 100644 --- a/packages/objectql/src/registry-assert-package-uninstallable.test.ts +++ b/packages/objectql/src/registry-assert-package-uninstallable.test.ts @@ -139,7 +139,7 @@ describe('#21276 SchemaRegistry.assertPackageUninstallable', () => { expect(ask).toHaveBeenCalledWith('com.owner'); expect(registry.getObject('free')).toBeDefined(); - registry.installPackage({ id: 'com.owner', name: 'Owner', version: '1.0.0' } as any); + registry.installPackage({ id: 'com.owner', name: 'Owner', version: '1.0.0' } as any, 'code'); ask.mockClear(); const viaUninstall = thrownBy(() => registry.uninstallPackage('com.owner')); expect(ask).toHaveBeenCalledTimes(1); diff --git a/packages/objectql/src/registry-i18n-bundle-key.test.ts b/packages/objectql/src/registry-i18n-bundle-key.test.ts index 7296706aac8..430201f29f4 100644 --- a/packages/objectql/src/registry-i18n-bundle-key.test.ts +++ b/packages/objectql/src/registry-i18n-bundle-key.test.ts @@ -304,7 +304,7 @@ describe('SchemaRegistry — i18n bundle keys (#7730)', () => { it('hides the whole bundle when its owning package is disabled', () => { // `listItems` filters by each item's `_packageId` (PR #7700). Re-keying // must not let a bundle member slip past that filter. - registry.installPackage({ id: 'com.acme.crm', name: 'CRM', version: '1.0.0' } as any); + registry.installPackage({ id: 'com.acme.crm', name: 'CRM', version: '1.0.0' } as any, 'code'); registry.registerItem('email_template', tpl('auth.welcome', 'en-US'), 'name', 'com.acme.crm'); registry.registerItem('email_template', tpl('auth.welcome', 'zh-CN'), 'name', 'com.acme.crm'); expect(registry.listItems('email_template')).toHaveLength(2); diff --git a/packages/objectql/src/registry-install-preserves-lifecycle.test.ts b/packages/objectql/src/registry-install-preserves-lifecycle.test.ts index 4ddea17bc81..371529c2dd4 100644 --- a/packages/objectql/src/registry-install-preserves-lifecycle.test.ts +++ b/packages/objectql/src/registry-install-preserves-lifecycle.test.ts @@ -108,10 +108,10 @@ describe('#18877 — installPackage preserves an existing row\'s lifecycle state // ⛔ The scope leg: 「preserve」 is about lifecycle state only. A // re-install is still an install, and the row it writes is still the // new manifest's. - registry.installPackage({ ...manifest('com.acme.scope'), version: '1.0.0' } as any); + registry.installPackage({ ...manifest('com.acme.scope'), version: '1.0.0' } as any, 'code'); registry.disablePackage('com.acme.scope'); - const again = registry.installPackage({ ...manifest('com.acme.scope'), version: '2.0.0' } as any); + const again = registry.installPackage({ ...manifest('com.acme.scope'), version: '2.0.0' } as any, 'code'); expect(again.manifest.version, 'the re-install really replaced the manifest').toBe('2.0.0'); expect(again.enabled, 'and did so without touching the lifecycle state').toBe(false); diff --git a/packages/objectql/src/registry-package-manifest-serializable.test.ts b/packages/objectql/src/registry-package-manifest-serializable.test.ts index dffbe2a6cbd..95f521a82af 100644 --- a/packages/objectql/src/registry-package-manifest-serializable.test.ts +++ b/packages/objectql/src/registry-package-manifest-serializable.test.ts @@ -175,7 +175,7 @@ describe('SchemaRegistry.installPackage — the record is serializable', () => { registry.installPackage(baseManifest({ plugins: [plugin] }), 'code'); registry.installPackage({ id: 'com.objectstack.setup', name: 'Setup', namespace: 'setup', version: '9.3.0', - } as any); + } as any, 'code'); expect(() => JSON.stringify(registry.getAllPackages())).not.toThrow(); expect(registry.getAllPackages()).toHaveLength(2); diff --git a/packages/objectql/src/registry-unregister-items-by-package.test.ts b/packages/objectql/src/registry-unregister-items-by-package.test.ts index ddc484c926b..d5a034505bc 100644 --- a/packages/objectql/src/registry-unregister-items-by-package.test.ts +++ b/packages/objectql/src/registry-unregister-items-by-package.test.ts @@ -192,7 +192,7 @@ describe('#7221 — SchemaRegistry.unregisterItemsByPackage', () => { describe('#7221 — SchemaRegistry.uninstallPackage closes the same gap', () => { it('removes the package’s generic items, not just its objects and record', () => { const reg = quiet(); - reg.installPackage({ id: 'crm', name: 'CRM', version: '1.0.0' } as any); + reg.installPackage({ id: 'crm', name: 'CRM', version: '1.0.0' } as any, 'code'); reg.registerItem('page', { name: 'home' }, 'name', 'crm'); reg.registerItem('flow', { name: 'onboard' }, 'name', 'crm'); reg.registerObject(objectBody('contact'), 'crm'); @@ -209,7 +209,7 @@ describe('#7221 — SchemaRegistry.uninstallPackage closes the same gap', () => it('removes NOTHING when the object half refuses (ADR-0029 extenders)', () => { const reg = quiet(); - reg.installPackage({ id: 'crm', name: 'CRM', version: '1.0.0' } as any); + reg.installPackage({ id: 'crm', name: 'CRM', version: '1.0.0' } as any, 'code'); reg.registerItem('page', { name: 'home' }, 'name', 'crm'); reg.registerObject(objectBody('contact'), 'crm'); reg.registerObject(objectBody('contact', 'extra'), 'analytics', undefined, 'extend'); @@ -224,7 +224,7 @@ describe('#7221 — SchemaRegistry.uninstallPackage closes the same gap', () => it('keeps the package’s own `package` record addressable by its bare id', () => { const reg = quiet(); - reg.installPackage({ id: 'crm', name: 'CRM', version: '1.0.0' } as any); + reg.installPackage({ id: 'crm', name: 'CRM', version: '1.0.0' } as any, 'code'); // The sweep is prefix-scoped to `crm:`, so the bare-keyed package record is // not collateral — `uninstallPackage` still owns removing it, and reports // `true` because it found it. diff --git a/packages/objectql/src/registry.test.ts b/packages/objectql/src/registry.test.ts index 358cceba279..90a2b41ab9a 100644 --- a/packages/objectql/src/registry.test.ts +++ b/packages/objectql/src/registry.test.ts @@ -504,7 +504,7 @@ describe('SchemaRegistry', () => { * Latent by grade: no in-tree caller reaches the refusal path today. */ it('[#7970] a refused uninstall leaves the namespace still resolving', () => { - registry.installPackage({ id: 'com.crm', name: 'CRM', namespace: 'crm', version: '1.0.0' } as any); + registry.installPackage({ id: 'com.crm', name: 'CRM', namespace: 'crm', version: '1.0.0' } as any, 'code'); registry.registerObject({ name: 'contact', fields: {} }, 'com.crm', 'crm', 'own'); registry.registerObject({ name: 'contact', fields: {} }, 'com.analytics', undefined, 'extend'); @@ -520,7 +520,7 @@ describe('SchemaRegistry', () => { }); it('[#7970] a refused uninstall leaves the whole package intact, not just the namespace', () => { - registry.installPackage({ id: 'com.crm', name: 'CRM', namespace: 'crm', version: '1.0.0' } as any); + registry.installPackage({ id: 'com.crm', name: 'CRM', namespace: 'crm', version: '1.0.0' } as any, 'code'); // Registered ahead of the extended object, so the object walk reaches // this one before it can refuse. registry.registerObject({ name: 'account', fields: {} }, 'com.crm', 'crm', 'own'); @@ -539,7 +539,7 @@ describe('SchemaRegistry', () => { }); it('[#7970] the successful path still releases the namespace after the object verb', () => { - registry.installPackage({ id: 'com.crm', name: 'CRM', namespace: 'crm', version: '1.0.0' } as any); + registry.installPackage({ id: 'com.crm', name: 'CRM', namespace: 'crm', version: '1.0.0' } as any, 'code'); registry.registerObject({ name: 'contact', fields: {} }, 'com.crm', 'crm', 'own'); expect(registry.uninstallPackage('com.crm')).toBe(true); @@ -550,7 +550,7 @@ describe('SchemaRegistry', () => { }); it('updatePackageManifest merges editable fields, preserving lifecycle state', () => { - registry.installPackage({ id: 'com.test', name: 'Old', version: '1.0.0' } as any); + registry.installPackage({ id: 'com.test', name: 'Old', version: '1.0.0' } as any, 'code'); registry.disablePackage('com.test'); // lifecycle state that must survive an edit const updated = registry.updatePackageManifest('com.test', { @@ -568,7 +568,7 @@ describe('SchemaRegistry', () => { }); it('updatePackageManifest only overwrites the fields present in the patch', () => { - registry.installPackage({ id: 'com.test', name: 'Keep', version: '1.0.0' } as any); + registry.installPackage({ id: 'com.test', name: 'Keep', version: '1.0.0' } as any, 'code'); const updated = registry.updatePackageManifest('com.test', { description: 'only this' }); expect(updated?.manifest.name).toBe('Keep'); expect(updated?.manifest.version).toBe('1.0.0'); diff --git a/packages/runtime/src/app-plugin.disabled-seed.test.ts b/packages/runtime/src/app-plugin.disabled-seed.test.ts index 65dfd66a4f3..cc46def8338 100644 --- a/packages/runtime/src/app-plugin.disabled-seed.test.ts +++ b/packages/runtime/src/app-plugin.disabled-seed.test.ts @@ -43,7 +43,7 @@ interface InstalledPackageView { enabled?: boolean; } interface TestRegistry { - installPackage(manifest: Record): unknown; + installPackage(manifest: Record, source: 'code' | 'marketplace' | 'authored'): unknown; getPackage(id: string): InstalledPackageView | undefined; } diff --git a/packages/runtime/src/domains/automation-flow-door-package-address.test.ts b/packages/runtime/src/domains/automation-flow-door-package-address.test.ts index 3aad0426e25..599b75ec1df 100644 --- a/packages/runtime/src/domains/automation-flow-door-package-address.test.ts +++ b/packages/runtime/src/domains/automation-flow-door-package-address.test.ts @@ -135,8 +135,8 @@ async function boot() { engine.registry.registerObject(SysMetadataAuditObject as any, TEST_PACKAGE_ID); await engine.syncSchemas(); // Two installed, writable bases: no read-only manifest scope, and no code package booted them. - engine.registry.installPackage({ id: ALPHA, name: ALPHA, namespace: 'alpha', version: '1.0.0', type: 'app' } as any); - engine.registry.installPackage({ id: BETA, name: BETA, namespace: 'beta', version: '1.0.0', type: 'app' } as any); + engine.registry.installPackage({ id: ALPHA, name: ALPHA, namespace: 'alpha', version: '1.0.0', type: 'app' } as any, 'code'); + engine.registry.installPackage({ id: BETA, name: BETA, namespace: 'beta', version: '1.0.0', type: 'app' } as any, 'code'); const protocol = new ObjectStackProtocolImplementation(engine as any); const flows = new Map(); diff --git a/packages/runtime/src/domains/dispatcher-query-arity.test.ts b/packages/runtime/src/domains/dispatcher-query-arity.test.ts index be53fa13fff..338f916c7e9 100644 --- a/packages/runtime/src/domains/dispatcher-query-arity.test.ts +++ b/packages/runtime/src/domains/dispatcher-query-arity.test.ts @@ -94,8 +94,8 @@ function boot() { function bootPackages() { const registry = new SchemaRegistry({ multiTenant: false, collisionPolicy: 'error' }); (registry as any).logLevel = 'silent'; - registry.installPackage({ id: 'com.acme.app', name: 'com.acme.app', namespace: 'app', version: '1.0.0', type: 'app' } as any); - registry.installPackage({ id: 'com.acme.plugin', name: 'com.acme.plugin', namespace: 'plugin', version: '1.0.0', type: 'plugin' } as any); + registry.installPackage({ id: 'com.acme.app', name: 'com.acme.app', namespace: 'app', version: '1.0.0', type: 'app' } as any, 'code'); + registry.installPackage({ id: 'com.acme.plugin', name: 'com.acme.plugin', namespace: 'plugin', version: '1.0.0', type: 'plugin' } as any, 'code'); (registry.getPackage('com.acme.plugin') as any).status = 'error'; const getAllPackages = vi.spyOn(registry, 'getAllPackages'); const get = (n: string) => (n === 'objectql' ? { registry } : null); diff --git a/packages/runtime/src/domains/package-door-producer-key-carry.test.ts b/packages/runtime/src/domains/package-door-producer-key-carry.test.ts index 44cf0789827..d1e2d74d5ad 100644 --- a/packages/runtime/src/domains/package-door-producer-key-carry.test.ts +++ b/packages/runtime/src/domains/package-door-producer-key-carry.test.ts @@ -201,7 +201,7 @@ function realRegistry(): SchemaRegistry { objects: [{ name: 'invoice', fields: { total: { type: 'currency' } } }], apps: [{ name: 'showcase', label: 'Showcase' }], plugins: [plugin], - } as never); + } as never, 'code'); // Init AFTER install — the measured ordering: the manifest serialised // cleanly during boot and only became cyclic once the plugins came up. plugin.init(cyclicEngine()); @@ -209,11 +209,11 @@ function realRegistry(): SchemaRegistry { registry.installPackage({ id: SYSTEM_SCOPED, name: 'Setup', namespace: 'setup', version: '9.3.0', type: 'plugin', scope: 'system', - } as never); + } as never, 'code'); registry.installPackage({ id: DB_BASE, name: 'My Base', namespace: 'mybase', version: '1.0.0', type: 'app', - } as never); + } as never, 'code'); // Every declared field carries a value from here on — see // `seatDeclaredFields` for the measurement that made this necessary. diff --git a/packages/runtime/src/domains/packages-get-version-scope.test.ts b/packages/runtime/src/domains/packages-get-version-scope.test.ts index 4cbd50798aa..2a9d6f547af 100644 --- a/packages/runtime/src/domains/packages-get-version-scope.test.ts +++ b/packages/runtime/src/domains/packages-get-version-scope.test.ts @@ -82,7 +82,7 @@ const ABSENT = '99.0.0'; function make() { const registry = new SchemaRegistry({ logLevel: 'silent' } as any); - registry.installPackage({ id: PKG, name: PKG, version: INSTALLED, scope: 'project', type: 'app' } as any); + registry.installPackage({ id: PKG, name: PKG, version: INSTALLED, scope: 'project', type: 'app' } as any, 'code'); const objectql = { registry, manifests: new Map() }; const kernel: any = { context: { getService: (name: string) => (name === 'objectql' ? objectql : null) }, diff --git a/packages/runtime/src/domains/packages-list-enabled-filter.test.ts b/packages/runtime/src/domains/packages-list-enabled-filter.test.ts index e4de12442a0..67a8d31cc6b 100644 --- a/packages/runtime/src/domains/packages-list-enabled-filter.test.ts +++ b/packages/runtime/src/domains/packages-list-enabled-filter.test.ts @@ -93,7 +93,7 @@ function fixtureRegistry(): SchemaRegistry { const install = (id: string, type: string) => registry.installPackage({ id, name: id, namespace: id.split('.').pop(), version: '1.0.0', type, - } as any); + } as any, 'code'); install(ENABLED_APP, 'app'); install(DISABLED_APP, 'app'); diff --git a/packages/runtime/src/domains/packages-orgless-grants-capability-gate.test.ts b/packages/runtime/src/domains/packages-orgless-grants-capability-gate.test.ts index 33e92db132f..a1830e86a35 100644 --- a/packages/runtime/src/domains/packages-orgless-grants-capability-gate.test.ts +++ b/packages/runtime/src/domains/packages-orgless-grants-capability-gate.test.ts @@ -133,7 +133,7 @@ const SESSIONS: Record o.name).find((n: string) => n.endsWith('crm_lead'))!; diff --git a/packages/runtime/src/domains/packages-write-flag-arity.test.ts b/packages/runtime/src/domains/packages-write-flag-arity.test.ts index d6bc24f7009..248142f027f 100644 --- a/packages/runtime/src/domains/packages-write-flag-arity.test.ts +++ b/packages/runtime/src/domains/packages-write-flag-arity.test.ts @@ -38,7 +38,7 @@ const AUTHOR = { userId: 'u_author', isSystem: false, systemPermissions: ['manag function boot() { const registry = new SchemaRegistry({ multiTenant: false, collisionPolicy: 'error' }); (registry as any).logLevel = 'silent'; - registry.installPackage({ id: PKG, name: PKG, namespace: 'app', version: '1.0.0', type: 'app' } as any); + registry.installPackage({ id: PKG, name: PKG, namespace: 'app', version: '1.0.0', type: 'app' } as any, 'code'); const getPackage = vi.spyOn(registry, 'getPackage'); const protocol = { deletePackage: vi.fn(async (_r: any) => ({ success: true })), diff --git a/packages/runtime/src/package-door-namespace-conflict-code.test.ts b/packages/runtime/src/package-door-namespace-conflict-code.test.ts index 55c1fb815fc..921cb27c3d5 100644 --- a/packages/runtime/src/package-door-namespace-conflict-code.test.ts +++ b/packages/runtime/src/package-door-namespace-conflict-code.test.ts @@ -39,7 +39,7 @@ * * Doubled: only the kernel's service lookup, which hands the door that * registry. There is no protocol service, so the install takes the documented - * fallback limb (`registry.installPackage(manifest, settings)`) — the same + * fallback limb (`registry.installPackage(manifest, 'authored', settings)`) — the same * primitive the protocol limb calls underneath. * * ## Reverse verification — direction predicted BEFORE running diff --git a/packages/runtime/src/package-revert-code-shipped-members.integration.test.ts b/packages/runtime/src/package-revert-code-shipped-members.integration.test.ts index 00adda4e572..f592a71053f 100644 --- a/packages/runtime/src/package-revert-code-shipped-members.integration.test.ts +++ b/packages/runtime/src/package-revert-code-shipped-members.integration.test.ts @@ -101,13 +101,13 @@ async function boot() { // A platform package that ships objects, as a platform plugin's manifest // installs it: `scope: 'system'`, and the four `sys_metadata*` objects // registered under it stand in for its objects. - engine.registry.installPackage({ id: OBJECTS_PKG, name: OBJECTS_PKG, version: '1.0.0', scope: 'system' } as any); + engine.registry.installPackage({ id: OBJECTS_PKG, name: OBJECTS_PKG, version: '1.0.0', scope: 'system' } as any, 'code'); for (const o of sysObjects) engine.registry.registerObject(o, OBJECTS_PKG); // A platform package that ships no objects — nothing of it reaches the // metadata service. - engine.registry.installPackage({ id: REGISTRY_ONLY_PKG, name: REGISTRY_ONLY_PKG, version: '1.0.0', scope: 'system' } as any); + engine.registry.installPackage({ id: REGISTRY_ONLY_PKG, name: REGISTRY_ONLY_PKG, version: '1.0.0', scope: 'system' } as any, 'code'); // A writable base: project-scoped, not booted. - engine.registry.installPackage({ id: STAMPED_BASE, name: STAMPED_BASE, version: '1.0.0', scope: 'project' } as any); + engine.registry.installPackage({ id: STAMPED_BASE, name: STAMPED_BASE, version: '1.0.0', scope: 'project' } as any, 'code'); const protocol = new ObjectStackProtocolImplementation( engine as any, undefined, undefined, 'package-author', From 8372d93c7306bcd9be333a98aa7978147f88f59e Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 17:41:23 +0000 Subject: [PATCH 04/10] wip: spec fixtures carry source; contract spells the origin union Claude-Session: https://claude.ai/code/session_01RGdjFAX2oq1VdGNT5WGdKN Co-authored-by: Claude --- .changeset/23972-installed-package-source.md | 1 + packages/client/src/index.ts | 20 ++++++++----- packages/spec/src/api/package-api.test.ts | 2 ++ .../src/contracts/objectql-engine.test.ts | 11 ++++++- .../spec/src/contracts/objectql-engine.ts | 9 ++++-- .../spec/src/kernel/package-registry.test.ts | 30 +++++++++---------- 6 files changed, 46 insertions(+), 27 deletions(-) diff --git a/.changeset/23972-installed-package-source.md b/.changeset/23972-installed-package-source.md index 668b2f3a475..d1ccc5d9706 100644 --- a/.changeset/23972-installed-package-source.md +++ b/.changeset/23972-installed-package-source.md @@ -5,6 +5,7 @@ '@objectstack/runtime': patch '@objectstack/service-package': patch '@objectstack/cloud-connection': patch +'@objectstack/client': patch --- feat(spec): every installed-package row declares where it came from, a closed `source: 'code' | 'marketplace' | 'authored'`, and the `GET /packages` rows declare the `writable` verdict they already served (#23972) diff --git a/packages/client/src/index.ts b/packages/client/src/index.ts index 9e5f5301958..9fb7dea55de 100644 --- a/packages/client/src/index.ts +++ b/packages/client/src/index.ts @@ -2582,11 +2582,16 @@ export class ObjectStackClient { * SAME envelope for this route, which is what makes it bindable: * `runtime`'s `/packages` domain sends `success({ packages, total: * packages.length })` and `rest`'s `GET {base}/packages` sends - * `sendOk(res, { packages, total: packages.length })`. The REST rows carry - * an extra `source: 'database' | 'registry' | 'both'` discriminator that is - * deliberately NOT declared here — the dispatcher rows have no such key, so - * declaring it would be false on that surface. #8140 bound the identical - * scoped sibling (`ScopedEnvironmentClient.packages.list`) the same way. + * `sendOk(res, { packages, total: packages.length })`. #8140 bound the + * identical scoped sibling (`ScopedEnvironmentClient.packages.list`) the + * same way. (The REST rows' `source: 'database' | 'registry' | 'both'` + * stamp left with those routes in #14503.) + * + * [#23972] Every row carries two declared keys besides the record's own: + * `source` (`'code' | 'marketplace' | 'authored'`, where the package came + * from, stamped by the door that registered it) and `writable` (the + * server's writability verdict, computed per read). Read the origin from + * `source`; ⛔ do not reconstruct it from `writable` or from a second list. * * ⚠️ [#17536] The ROW type moved, the envelope did not. #11925 bound the * element to `InstalledPackage` — the AUTHORING stage — and PR #17517 then @@ -2697,9 +2702,8 @@ export class ObjectStackClient { * manifest stages. It was a COMMENT and never a binding, which is why the * disagreement it describes could sit here unread by any gate. * - * `source` stays undeclared because there is no longer anything that - * emits it on this route; `writable` stays undeclared for the reason - * `list` leaves it undeclared. + * [#23972] The row carries the declared `source` and `writable`, as on + * `list`. * * ⚠️ Clause-② narrowing (#12034). `{ package: any }` is what let * `(await client.packages.get(id)).package` compile, and on the only diff --git a/packages/spec/src/api/package-api.test.ts b/packages/spec/src/api/package-api.test.ts index 7fa47707858..abaa47ce6f7 100644 --- a/packages/spec/src/api/package-api.test.ts +++ b/packages/spec/src/api/package-api.test.ts @@ -206,6 +206,7 @@ describe('PackageInstallResponseSchema', () => { data: { package: { manifest: { id: 'com.acme.crm', name: 'acme_crm', version: '1.0.0', type: 'plugin' }, + source: 'authored', status: 'installed', enabled: true, }, @@ -231,6 +232,7 @@ describe('PackageInstallResponseSchema', () => { data: { package: { manifest: { id: 'com.acme.crm', name: 'acme_crm', version: '1.0.0', type: 'plugin' }, + source: 'authored', status: 'installed', enabled: true, }, diff --git a/packages/spec/src/contracts/objectql-engine.test.ts b/packages/spec/src/contracts/objectql-engine.test.ts index 0b2f4db77e3..a29377042a6 100644 --- a/packages/spec/src/contracts/objectql-engine.test.ts +++ b/packages/spec/src/contracts/objectql-engine.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from 'vitest'; +import { describe, it, expect, expectTypeOf } from 'vitest'; import type { EngineFilterJudgement, EngineFilterJudgementOptions, @@ -8,6 +8,7 @@ import type { import type { ServiceObject } from '../data/object.zod'; import type { FilterCondition } from '../data/filter.zod'; import type { ExecutionContext } from '../kernel/execution-context.zod'; +import type { InstalledPackageSource } from '../kernel/package-registry.zod'; /** * `getObject` — typed on the contract, not re-declared by consumers @@ -211,3 +212,11 @@ describe('judgeFilter contract — the engine judges a filter without running it expect(same).toBe('same'); }); }); + +// [#23972] The registry contract spells the origin union rather than importing +// it (see its docblock); this holds the spelling equal to the declaration. +describe('EngineSchemaRegistryView.installPackage', () => { + it('takes the declared InstalledPackageSource as its source', () => { + expectTypeOf[1]>().toEqualTypeOf(); + }); +}); diff --git a/packages/spec/src/contracts/objectql-engine.ts b/packages/spec/src/contracts/objectql-engine.ts index 82c89b2d63a..ad4b467a697 100644 --- a/packages/spec/src/contracts/objectql-engine.ts +++ b/packages/spec/src/contracts/objectql-engine.ts @@ -56,7 +56,6 @@ import type { IDataDriver } from './data-driver'; import type { FlowFunctionEffect, FlowFunctionEntry } from '../automation/flow-function.zod'; import type { ServiceObject } from '../data/object.zod'; import type { BaseEngineOptions, EngineQueryOptions } from '../data/data-engine.zod'; -import type { InstalledPackageSource } from '../kernel/package-registry.zod'; /** * The engine's schema-registry view — the members reached through the @@ -114,9 +113,13 @@ export interface EngineSchemaRegistryView { /** * Register a package manifest in the in-memory registry. `source` is the * caller's statement of where the package came from, stamped on the row; a - * value outside the closed set is refused. + * value outside the closed set is refused. The union is + * `InstalledPackageSource` (`kernel/package-registry.zod`), spelled here + * rather than imported so this contract module does not pull the manifest + * schema graph into its importers; `objectql-engine.test.ts` + * holds the two equal. */ - installPackage(manifest: unknown, source: InstalledPackageSource, settings?: Record): unknown; + installPackage(manifest: unknown, source: 'code' | 'marketplace' | 'authored', settings?: Record): unknown; /** Drop a package from the registry; `false` when no package had that id. */ uninstallPackage(id: string): boolean; /** Flip a package to enabled; `undefined` when no package had that id. */ diff --git a/packages/spec/src/kernel/package-registry.test.ts b/packages/spec/src/kernel/package-registry.test.ts index 18c9f1b940a..80878285a54 100644 --- a/packages/spec/src/kernel/package-registry.test.ts +++ b/packages/spec/src/kernel/package-registry.test.ts @@ -66,19 +66,19 @@ describe('InstalledPackageSchema.source', () => { describe('InstalledPackageSchema', () => { it('should accept minimal installed package', () => { - const pkg = { manifest: validManifest }; + const pkg = { manifest: validManifest, source: 'code' }; expect(() => InstalledPackageSchema.parse(pkg)).not.toThrow(); }); it('should apply default values', () => { - const parsed = InstalledPackageSchema.parse({ manifest: validManifest }); + const parsed = InstalledPackageSchema.parse({ manifest: validManifest, source: 'code' }); expect(parsed.status).toBe('installed'); expect(parsed.enabled).toBe(true); }); it('should accept full installed package', () => { const pkg = { - manifest: validManifest, + manifest: validManifest, source: 'code', status: 'disabled' as const, enabled: false, installedAt: '2025-01-15T10:00:00Z', @@ -96,7 +96,7 @@ describe('InstalledPackageSchema', () => { it('should accept installed package with upgrade history', () => { const pkg = { - manifest: validManifest, + manifest: validManifest, source: 'code', upgradeHistory: [ { fromVersion: '1.0.0', @@ -121,7 +121,7 @@ describe('InstalledPackageSchema', () => { it('should accept upgrade history with failed/rolled_back status', () => { const pkg = { - manifest: validManifest, + manifest: validManifest, source: 'code', upgradeHistory: [ { fromVersion: '1.0.0', @@ -145,7 +145,7 @@ describe('InstalledPackageSchema', () => { it('should reject invalid datetime for installedAt', () => { expect(() => InstalledPackageSchema.parse({ - manifest: validManifest, + manifest: validManifest, source: 'code', installedAt: 'not-a-date', })).toThrow(); }); @@ -177,7 +177,7 @@ describe('ListPackagesRequestSchema', () => { describe('ListPackagesResponseSchema', () => { it('should accept valid response', () => { const response = { - packages: [{ manifest: validManifest }], + packages: [{ manifest: validManifest, source: 'code' }], total: 1, }; const parsed = ListPackagesResponseSchema.parse(response); @@ -204,7 +204,7 @@ describe('GetPackageRequestSchema', () => { describe('GetPackageResponseSchema', () => { it('should accept valid response', () => { - const response = { package: { manifest: validManifest } }; + const response = { package: { manifest: validManifest, source: 'code' } }; expect(() => GetPackageResponseSchema.parse(response)).not.toThrow(); }); }); @@ -256,7 +256,7 @@ describe('InstallPackageRequestSchema', () => { describe('InstallPackageResponseSchema', () => { it('should accept valid response', () => { const response = { - package: { manifest: validManifest }, + package: { manifest: validManifest, source: 'code' }, message: 'Installed successfully', }; expect(() => InstallPackageResponseSchema.parse(response)).not.toThrow(); @@ -301,7 +301,7 @@ describe('EnablePackageRequestSchema', () => { describe('EnablePackageResponseSchema', () => { it('should accept valid response', () => { const response = { - package: { manifest: validManifest }, + package: { manifest: validManifest, source: 'code' }, message: 'Enabled', }; expect(() => EnablePackageResponseSchema.parse(response)).not.toThrow(); @@ -318,7 +318,7 @@ describe('DisablePackageRequestSchema', () => { describe('DisablePackageResponseSchema', () => { it('should accept valid response', () => { const response = { - package: { manifest: validManifest }, + package: { manifest: validManifest, source: 'code' }, message: 'Disabled', }; expect(() => DisablePackageResponseSchema.parse(response)).not.toThrow(); @@ -394,7 +394,7 @@ describe('NamespaceConflictErrorSchema', () => { describe('InstalledPackageSchema - registeredNamespaces', () => { it('should accept package with registered namespaces', () => { const pkg = { - manifest: validManifest, + manifest: validManifest, source: 'code', registeredNamespaces: ['crm', 'crm_ext'], }; const parsed = InstalledPackageSchema.parse(pkg); @@ -402,7 +402,7 @@ describe('InstalledPackageSchema - registeredNamespaces', () => { }); it('should accept package without registered namespaces (backward compatible)', () => { - const pkg = { manifest: validManifest }; + const pkg = { manifest: validManifest, source: 'code' }; const parsed = InstalledPackageSchema.parse(pkg); expect(parsed.registeredNamespaces).toBeUndefined(); }); @@ -411,7 +411,7 @@ describe('InstalledPackageSchema - registeredNamespaces', () => { describe('InstallPackageResponseSchema - dependencyResolution', () => { it('should accept response with dependency resolution', () => { const response = { - package: { manifest: validManifest }, + package: { manifest: validManifest, source: 'code' }, message: 'Installed successfully', dependencyResolution: { dependencies: [ @@ -433,7 +433,7 @@ describe('InstallPackageResponseSchema - dependencyResolution', () => { it('should accept response without dependency resolution (backward compatible)', () => { const response = { - package: { manifest: validManifest }, + package: { manifest: validManifest, source: 'code' }, }; const parsed = InstallPackageResponseSchema.parse(response); expect(parsed.dependencyResolution).toBeUndefined(); From 9f60c1806411f6441a818d892096c87c609a8355 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 17:44:20 +0000 Subject: [PATCH 05/10] chore(spec): regenerate api-surface, export-origins, declaration-map and reference docs Claude-Session: https://claude.ai/code/session_01RGdjFAX2oq1VdGNT5WGdKN Co-authored-by: Claude --- .../references/api/package-api-assembled.mdx | 21 ++++++------ content/docs/references/api/package-api.mdx | 2 +- content/docs/references/api/protocol.mdx | 15 ++++++--- content/docs/references/index.mdx | 10 +++--- .../references/kernel/package-registry.mdx | 33 +++++++++++++++---- packages/spec/api-surface/kernel.json | 2 ++ packages/spec/declaration-map/kernel.json | 2 ++ packages/spec/export-origins/kernel.json | 2 ++ 8 files changed, 59 insertions(+), 28 deletions(-) diff --git a/content/docs/references/api/package-api-assembled.mdx b/content/docs/references/api/package-api-assembled.mdx index a9b2fb305ab..a60394e2249 100644 --- a/content/docs/references/api/package-api-assembled.mdx +++ b/content/docs/references/api/package-api-assembled.mdx @@ -67,6 +67,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, at the stage the registry records it | +| **source** | `Enum<'code' \| 'marketplace' \| 'authored'>` | ✅ | Where the installed package came from: code (registered by the deployment's own code), marketplace (installed from the marketplace), or authored (a database-authored package) | | **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 | @@ -166,7 +167,7 @@ Get installed package response | **success** | `boolean` | ✅ | Operation success status | | **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>; declaredCode?: string; message: string; userMessage?: string; … }` | optional | Error details if success is false | | **meta** | `{ timestamp: string; duration?: integer; requestId?: string; traceId?: string }` | optional | Response metadata | -| **data** | `{ manifest: object; status?: Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>; enabled?: boolean; installedAt?: string; … } \| … +1 more` | ✅ | Installed package details | +| **data** | `{ manifest: object; source: Enum<'code' \| 'marketplace' \| 'authored'>; status?: Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>; enabled?: boolean; … } \| … +1 more` | ✅ | Installed package details | ### Nested Shape: `GetInstalledPackageResponse.error` @@ -193,11 +194,10 @@ Get installed package response ### Nested Shape: `GetInstalledPackageResponse.data[option 1]` -Installed package with runtime lifecycle state - | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **manifest** | `{ id: string; namespace?: string; defaultDatasource?: string; version: string; … }` | ✅ | Package manifest at the AUTHORING stage; a row installed by a `defineStack()` host carries the assembled body instead — see `AssembledInstalledPackageSchema` / `InstalledPackageAtEitherStageSchema` | +| **source** | `Enum<'code' \| 'marketplace' \| 'authored'>` | ✅ | Where the installed package came from: code (registered by the deployment's own code), marketplace (installed from the marketplace), or authored (a database-authored package) | | **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 | @@ -209,14 +209,14 @@ Installed package with runtime lifecycle state | **settings** | `Record` | optional | User-provided configuration settings | | **upgradeHistory** | `{ fromVersion: string; toVersion: string; upgradedAt: string; status: Enum<'success' \| 'failed' \| 'rolled_back'>; … }[]` | optional | Version upgrade history | | **registeredNamespaces** | `string[]` | optional | Namespace prefixes registered by this package | +| **writable** | `boolean` | ✅ | Whether this server lets an org author into and manage this package (a writable base); false for a read-only package. Computed per read by the server's own predicate, never stored | ### Nested Shape: `GetInstalledPackageResponse.data[option 2]` -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, at the stage the registry records it | +| **source** | `Enum<'code' \| 'marketplace' \| 'authored'>` | ✅ | Where the installed package came from: code (registered by the deployment's own code), marketplace (installed from the marketplace), or authored (a database-authored package) | | **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 | @@ -228,6 +228,7 @@ Installed package row whose manifest is the assembled package body | **settings** | `Record` | optional | User-provided configuration settings | | **upgradeHistory** | `{ fromVersion: string; toVersion: string; upgradedAt: string; status: Enum<'success' \| 'failed' \| 'rolled_back'>; … }[]` | optional | Version upgrade history | | **registeredNamespaces** | `string[]` | optional | Namespace prefixes registered by this package | +| **writable** | `boolean` | ✅ | Whether this server lets an org author into and manage this package (a writable base); false for a read-only package. Computed per read by the server's own predicate, never stored | --- @@ -242,13 +243,12 @@ This schema accepts one of the following structures: #### Option 1 -Installed package with runtime lifecycle state - ### Properties | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **manifest** | `{ id: string; namespace?: string; defaultDatasource?: string; version: string; … }` | ✅ | Package manifest at the AUTHORING stage; a row installed by a `defineStack()` host carries the assembled body instead — see `AssembledInstalledPackageSchema` / `InstalledPackageAtEitherStageSchema` | +| **source** | `Enum<'code' \| 'marketplace' \| 'authored'>` | ✅ | Where the installed package came from: code (registered by the deployment's own code), marketplace (installed from the marketplace), or authored (a database-authored package) | | **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 | @@ -260,6 +260,7 @@ Installed package with runtime lifecycle state | **settings** | `Record` | optional | User-provided configuration settings | | **upgradeHistory** | `{ fromVersion: string; toVersion: string; upgradedAt: string; status: Enum<'success' \| 'failed' \| 'rolled_back'>; … }[]` | optional | Version upgrade history | | **registeredNamespaces** | `string[]` | optional | Namespace prefixes registered by this package | +| **writable** | `boolean` | ✅ | Whether this server lets an org author into and manage this package (a writable base); false for a read-only package. Computed per read by the server's own predicate, never stored | ### Nested Shape: `InstalledPackageAtEitherStage[option 1].manifest` @@ -306,13 +307,12 @@ Installed package with runtime lifecycle state #### Option 2 -Installed package row whose manifest is the assembled package body - ### Properties | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **manifest** | `{ id: string; namespace?: string; defaultDatasource?: string; version: string; … }` | ✅ | The ASSEMBLED package body this row carries, at the stage the registry records it | +| **source** | `Enum<'code' \| 'marketplace' \| 'authored'>` | ✅ | Where the installed package came from: code (registered by the deployment's own code), marketplace (installed from the marketplace), or authored (a database-authored package) | | **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 | @@ -324,6 +324,7 @@ Installed package row whose manifest is the assembled package body | **settings** | `Record` | optional | User-provided configuration settings | | **upgradeHistory** | `{ fromVersion: string; toVersion: string; upgradedAt: string; status: Enum<'success' \| 'failed' \| 'rolled_back'>; … }[]` | optional | Version upgrade history | | **registeredNamespaces** | `string[]` | optional | Namespace prefixes registered by this package | +| **writable** | `boolean` | ✅ | Whether this server lets an org author into and manage this package (a writable base); false for a read-only package. Computed per read by the server's own predicate, never stored | ### Nested Shape: `InstalledPackageAtEitherStage[option 2].manifest` @@ -443,7 +444,7 @@ List installed packages response | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **packages** | `({ manifest: object; status?: Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>; enabled?: boolean; installedAt?: string; … } \| … +1 more)[]` | ✅ | Installed packages | +| **packages** | `({ manifest: object; source: Enum<'code' \| 'marketplace' \| 'authored'>; status?: Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>; enabled?: boolean; … } \| … +1 more)[]` | ✅ | Installed packages | | **total** | `integer` | optional | Total matching packages | | **nextCursor** | `string` | optional | Cursor for the next page | | **hasMore** | `boolean` | ✅ | Whether more packages are available — this door serves one page, so always `false` | diff --git a/content/docs/references/api/package-api.mdx b/content/docs/references/api/package-api.mdx index 3e1807d279f..4bd03bc6a4c 100644 --- a/content/docs/references/api/package-api.mdx +++ b/content/docs/references/api/package-api.mdx @@ -383,7 +383,7 @@ Install package response | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **package** | `{ manifest: object; status?: Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>; enabled?: boolean; installedAt?: string; … }` | ✅ | Installed package details | +| **package** | `{ manifest: object; source: Enum<'code' \| 'marketplace' \| 'authored'>; status?: Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>; enabled?: boolean; … }` | ✅ | Installed package details | | **dependencyResolution** | `{ dependencies: object[]; canProceed: boolean; requiredActions: object[]; installOrder: string[]; … }` | optional | Dependency resolution result | | **namespaceConflicts** | `{ type: 'namespace_conflict'; requestedNamespace: string; conflictingPackageId: string; conflictingPackageName: string; … }[]` | optional | Namespace conflicts detected | | **message** | `string` | optional | Installation status message | diff --git a/content/docs/references/api/protocol.mdx b/content/docs/references/api/protocol.mdx index 15b9df0e4c9..84428a0c50a 100644 --- a/content/docs/references/api/protocol.mdx +++ b/content/docs/references/api/protocol.mdx @@ -816,7 +816,7 @@ Disable package response | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **package** | `{ manifest: object; status?: Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>; enabled?: boolean; installedAt?: string; … }` | ✅ | Disabled package details | +| **package** | `{ manifest: object; source: Enum<'code' \| 'marketplace' \| 'authored'>; status?: Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>; enabled?: boolean; … }` | ✅ | Disabled package details | | **message** | `string` | optional | Disable status message | ### Nested Shape: `DisablePackageResponse.package` @@ -824,6 +824,7 @@ Disable package response | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **manifest** | `{ id: string; namespace?: string; defaultDatasource?: string; version: string; … }` | ✅ | Package manifest at the AUTHORING stage; a row installed by a `defineStack()` host carries the assembled body instead — see `AssembledInstalledPackageSchema` / `InstalledPackageAtEitherStageSchema` | +| **source** | `Enum<'code' \| 'marketplace' \| 'authored'>` | ✅ | Where the installed package came from: code (registered by the deployment's own code), marketplace (installed from the marketplace), or authored (a database-authored package) | | **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 | @@ -860,7 +861,7 @@ Enable package response | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **package** | `{ manifest: object; status?: Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>; enabled?: boolean; installedAt?: string; … }` | ✅ | Enabled package details | +| **package** | `{ manifest: object; source: Enum<'code' \| 'marketplace' \| 'authored'>; status?: Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>; enabled?: boolean; … }` | ✅ | Enabled package details | | **message** | `string` | optional | Enable status message | ### Nested Shape: `EnablePackageResponse.package` @@ -868,6 +869,7 @@ Enable package response | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **manifest** | `{ id: string; namespace?: string; defaultDatasource?: string; version: string; … }` | ✅ | Package manifest at the AUTHORING stage; a row installed by a `defineStack()` host carries the assembled body instead — see `AssembledInstalledPackageSchema` / `InstalledPackageAtEitherStageSchema` | +| **source** | `Enum<'code' \| 'marketplace' \| 'authored'>` | ✅ | Where the installed package came from: code (registered by the deployment's own code), marketplace (installed from the marketplace), or authored (a database-authored package) | | **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 | @@ -1669,13 +1671,14 @@ Get package response | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **package** | `{ manifest: object; status?: Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>; enabled?: boolean; installedAt?: string; … }` | ✅ | Package details | +| **package** | `{ manifest: object; source: Enum<'code' \| 'marketplace' \| 'authored'>; status?: Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>; enabled?: boolean; … }` | ✅ | Package details | ### Nested Shape: `GetPackageResponse.package` | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **manifest** | `{ id: string; namespace?: string; defaultDatasource?: string; version: string; … }` | ✅ | Package manifest at the AUTHORING stage; a row installed by a `defineStack()` host carries the assembled body instead — see `AssembledInstalledPackageSchema` / `InstalledPackageAtEitherStageSchema` | +| **source** | `Enum<'code' \| 'marketplace' \| 'authored'>` | ✅ | Where the installed package came from: code (registered by the deployment's own code), marketplace (installed from the marketplace), or authored (a database-authored package) | | **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 | @@ -2105,7 +2108,7 @@ Install package response | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **package** | `{ manifest: object; status?: Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>; enabled?: boolean; installedAt?: string; … }` | ✅ | Installed package details | +| **package** | `{ manifest: object; source: Enum<'code' \| 'marketplace' \| 'authored'>; status?: Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>; enabled?: boolean; … }` | ✅ | Installed package details | | **message** | `string` | optional | Installation status message | | **dependencyResolution** | `{ dependencies: object[]; canProceed: boolean; requiredActions: object[]; installOrder: string[]; … }` | optional | Dependency resolution result from install analysis | @@ -2114,6 +2117,7 @@ Install package response | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **manifest** | `{ id: string; namespace?: string; defaultDatasource?: string; version: string; … }` | ✅ | Package manifest at the AUTHORING stage; a row installed by a `defineStack()` host carries the assembled body instead — see `AssembledInstalledPackageSchema` / `InstalledPackageAtEitherStageSchema` | +| **source** | `Enum<'code' \| 'marketplace' \| 'authored'>` | ✅ | Where the installed package came from: code (registered by the deployment's own code), marketplace (installed from the marketplace), or authored (a database-authored package) | | **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 | @@ -2308,7 +2312,7 @@ List packages response | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **packages** | `{ manifest: object; status?: Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>; enabled?: boolean; installedAt?: string; … }[]` | ✅ | List of installed packages | +| **packages** | `{ manifest: object; source: Enum<'code' \| 'marketplace' \| 'authored'>; status?: Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>; enabled?: boolean; … }[]` | ✅ | List of installed packages | | **total** | `number` | ✅ | Total package count | ### Nested Shape: `ListPackagesResponse.packages[number]` @@ -2318,6 +2322,7 @@ Installed package with runtime lifecycle state | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **manifest** | `{ id: string; namespace?: string; defaultDatasource?: string; version: string; … }` | ✅ | Package manifest at the AUTHORING stage; a row installed by a `defineStack()` host carries the assembled body instead — see `AssembledInstalledPackageSchema` / `InstalledPackageAtEitherStageSchema` | +| **source** | `Enum<'code' \| 'marketplace' \| 'authored'>` | ✅ | Where the installed package came from: code (registered by the deployment's own code), marketplace (installed from the marketplace), or authored (a database-authored package) | | **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 | diff --git a/content/docs/references/index.mdx b/content/docs/references/index.mdx index 6fddd3beadd..56b5643955b 100644 --- a/content/docs/references/index.mdx +++ b/content/docs/references/index.mdx @@ -1,7 +1,7 @@ --- title: Protocol reference — every schema by module navTitle: Protocol Reference -description: Every schema published by @objectstack/spec — 1537 schemas across 14 protocol modules +description: Every schema published by @objectstack/spec — 1538 schemas across 14 protocol modules --- {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} @@ -26,7 +26,7 @@ counts are sums of the rows they head. Regenerate with | [Data Protocol](/docs/references/data) | 29 | 176 | Objects, fields, queries, filters, datasources and drivers — the ObjectQL layer. | | [Identity Protocol](/docs/references/identity) | 5 | 27 | Users and accounts, organizations, positions, SCIM provisioning. | | [Integration Protocol](/docs/references/integration) | 1 | 12 | The single connector protocol (ADR-0097) — catalog descriptors and provider-bound instances. | -| [Kernel Protocol](/docs/references/kernel) | 30 | 160 | Plugin lifecycle and manifests, capabilities and security, metadata loading, service registry. | +| [Kernel Protocol](/docs/references/kernel) | 30 | 161 | Plugin lifecycle and manifests, capabilities and security, metadata loading, service registry. | | [Marketplace Protocol](/docs/references/marketplace) | 4 | 30 | The package & marketplace format — package identity and versions, listing, publish, review, search, install, template manifests. | | [QA Protocol](/docs/references/qa) | 1 | 8 | Declarative test suites — scenarios, steps, actions and assertions. | | [Security Protocol](/docs/references/security) | 5 | 30 | Permission sets, row-level security, sharing rules, tenancy posture. | @@ -34,7 +34,7 @@ counts are sums of the rows they head. Regenerate with | [Studio Protocol](/docs/references/studio) | 3 | 35 | Studio designer metadata — the authoring surfaces for the protocols above. | | [System Protocol](/docs/references/system) | 36 | 284 | The runtime environment — logging, jobs, cache, metrics, notifications, i18n and compliance. | | [UI Protocol](/docs/references/ui) | 16 | 169 | Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. | -| **Total** | **197** | **1537** | 14 protocol modules | +| **Total** | **197** | **1538** | 14 protocol modules | --- @@ -198,7 +198,7 @@ The single connector protocol (ADR-0097) — catalog descriptors and provider-bo ## Kernel Protocol -**Source:** `packages/spec/src/kernel/` · **Import:** `@objectstack/spec/kernel` · **30 pages, 160 schemas** +**Source:** `packages/spec/src/kernel/` · **Import:** `@objectstack/spec/kernel` · **30 pages, 161 schemas** Plugin lifecycle and manifests, capabilities and security, metadata loading, service registry. @@ -220,7 +220,7 @@ Plugin lifecycle and manifests, capabilities and security, metadata loading, ser | [`metadata-plugin.zod.ts`](/docs/references/kernel/metadata-plugin) | `MetadataBulkResult`, `MetadataDependency`, `MetadataPluginConfig`, `MetadataPluginManifest`, `MetadataQuery`, `MetadataQueryResult`, `MetadataType`, `MetadataTypeRegistryEntry`, `MetadataValidationResult` | | [`metadata-protection.zod.ts`](/docs/references/kernel/metadata-protection) | `MetadataLock`, `MetadataLockSource`, `MetadataProvenance` | | [`package-artifact.zod.ts`](/docs/references/kernel/package-artifact) | `ArtifactChecksum`, `ArtifactFileEntry`, `ArtifactSignature`, `MetadataCategoryEnum`, `PackageArtifact` | -| [`package-registry.zod.ts`](/docs/references/kernel/package-registry) | `DisablePackageRequest`, `DisablePackageResponse`, `EnablePackageRequest`, `EnablePackageResponse`, `GetPackageRequest`, `GetPackageResponse`, `InstallPackageRequest`, `InstallPackageResponse`, `InstalledPackage`, `ListPackagesRequest`, `ListPackagesResponse`, `NamespaceConflictError`, `NamespaceRegistryEntry`, `PackageStatusEnum`, `UninstallPackageRequest`, `UninstallPackageResponse` | +| [`package-registry.zod.ts`](/docs/references/kernel/package-registry) | `DisablePackageRequest`, `DisablePackageResponse`, `EnablePackageRequest`, `EnablePackageResponse`, `GetPackageRequest`, `GetPackageResponse`, `InstallPackageRequest`, `InstallPackageResponse`, `InstalledPackage`, `InstalledPackageSource`, `ListPackagesRequest`, `ListPackagesResponse`, `NamespaceConflictError`, `NamespaceRegistryEntry`, `PackageStatusEnum`, `UninstallPackageRequest`, `UninstallPackageResponse` | | [`package-upgrade.zod.ts`](/docs/references/kernel/package-upgrade) | `MetadataChangeType`, `MetadataDiffItem`, `RollbackPackageRequest`, `RollbackPackageResponse`, `UpgradeImpactLevel`, `UpgradePackageRequest`, `UpgradePackageResponse`, `UpgradePhase`, `UpgradePlan`, `UpgradeSnapshot` | | [`plugin.zod.ts`](/docs/references/kernel/plugin) | `Plugin` | | [`plugin-capability.zod.ts`](/docs/references/kernel/plugin-capability) | `CapabilityConformanceLevel`, `ExtensionPoint`, `PluginCapability`, `PluginCapabilityManifest`, `PluginDependency`, `PluginInterface`, `ProtocolFeature`, `ProtocolReference`, `ProtocolVersion` | diff --git a/content/docs/references/kernel/package-registry.mdx b/content/docs/references/kernel/package-registry.mdx index d88a0b885f3..866ee3c7834 100644 --- a/content/docs/references/kernel/package-registry.mdx +++ b/content/docs/references/kernel/package-registry.mdx @@ -35,8 +35,8 @@ Defines the runtime state and lifecycle operations for installed packages. ## TypeScript Usage ```typescript -import { DisablePackageRequestSchema, DisablePackageResponseSchema, EnablePackageRequestSchema, EnablePackageResponseSchema, GetPackageRequestSchema, GetPackageResponseSchema, InstallPackageRequestSchema, InstallPackageResponseSchema, InstalledPackageSchema, ListPackagesRequestSchema, ListPackagesResponseSchema, NamespaceConflictErrorSchema, NamespaceRegistryEntrySchema, PackageStatusEnum, UninstallPackageRequestSchema, UninstallPackageResponseSchema } from '@objectstack/spec/kernel'; -import type { DisablePackageRequest, DisablePackageResponse, EnablePackageRequest, EnablePackageResponse, GetPackageRequest, GetPackageResponse, InstallPackageRequest, InstallPackageResponse, InstalledPackage, ListPackagesRequest, ListPackagesResponse, NamespaceConflictError, NamespaceRegistryEntry, UninstallPackageRequest, UninstallPackageResponse } from '@objectstack/spec/kernel'; +import { DisablePackageRequestSchema, DisablePackageResponseSchema, EnablePackageRequestSchema, EnablePackageResponseSchema, GetPackageRequestSchema, GetPackageResponseSchema, InstallPackageRequestSchema, InstallPackageResponseSchema, InstalledPackageSchema, InstalledPackageSourceSchema, ListPackagesRequestSchema, ListPackagesResponseSchema, NamespaceConflictErrorSchema, NamespaceRegistryEntrySchema, PackageStatusEnum, UninstallPackageRequestSchema, UninstallPackageResponseSchema } from '@objectstack/spec/kernel'; +import type { DisablePackageRequest, DisablePackageResponse, EnablePackageRequest, EnablePackageResponse, GetPackageRequest, GetPackageResponse, InstallPackageRequest, InstallPackageResponse, InstalledPackage, InstalledPackageSource, ListPackagesRequest, ListPackagesResponse, NamespaceConflictError, NamespaceRegistryEntry, UninstallPackageRequest, UninstallPackageResponse } from '@objectstack/spec/kernel'; // Validate data const result = DisablePackageRequestSchema.parse(data); @@ -65,7 +65,7 @@ Disable package response | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **package** | `{ manifest: object; status?: Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>; enabled?: boolean; installedAt?: string; … }` | ✅ | Disabled package details | +| **package** | `{ manifest: object; source: Enum<'code' \| 'marketplace' \| 'authored'>; status?: Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>; enabled?: boolean; … }` | ✅ | Disabled package details | | **message** | `string` | optional | Disable status message | ### Nested Shape: `DisablePackageResponse.package` @@ -73,6 +73,7 @@ Disable package response | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **manifest** | `{ id: string; namespace?: string; defaultDatasource?: string; version: string; … }` | ✅ | Package manifest at the AUTHORING stage; a row installed by a `defineStack()` host carries the assembled body instead — see `AssembledInstalledPackageSchema` / `InstalledPackageAtEitherStageSchema` | +| **source** | `Enum<'code' \| 'marketplace' \| 'authored'>` | ✅ | Where the installed package came from: code (registered by the deployment's own code), marketplace (installed from the marketplace), or authored (a database-authored package) | | **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 | @@ -109,7 +110,7 @@ Enable package response | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **package** | `{ manifest: object; status?: Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>; enabled?: boolean; installedAt?: string; … }` | ✅ | Enabled package details | +| **package** | `{ manifest: object; source: Enum<'code' \| 'marketplace' \| 'authored'>; status?: Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>; enabled?: boolean; … }` | ✅ | Enabled package details | | **message** | `string` | optional | Enable status message | ### Nested Shape: `EnablePackageResponse.package` @@ -117,6 +118,7 @@ Enable package response | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **manifest** | `{ id: string; namespace?: string; defaultDatasource?: string; version: string; … }` | ✅ | Package manifest at the AUTHORING stage; a row installed by a `defineStack()` host carries the assembled body instead — see `AssembledInstalledPackageSchema` / `InstalledPackageAtEitherStageSchema` | +| **source** | `Enum<'code' \| 'marketplace' \| 'authored'>` | ✅ | Where the installed package came from: code (registered by the deployment's own code), marketplace (installed from the marketplace), or authored (a database-authored package) | | **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 | @@ -153,13 +155,14 @@ Get package response | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **package** | `{ manifest: object; status?: Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>; enabled?: boolean; installedAt?: string; … }` | ✅ | Package details | +| **package** | `{ manifest: object; source: Enum<'code' \| 'marketplace' \| 'authored'>; status?: Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>; enabled?: boolean; … }` | ✅ | Package details | ### Nested Shape: `GetPackageResponse.package` | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **manifest** | `{ id: string; namespace?: string; defaultDatasource?: string; version: string; … }` | ✅ | Package manifest at the AUTHORING stage; a row installed by a `defineStack()` host carries the assembled body instead — see `AssembledInstalledPackageSchema` / `InstalledPackageAtEitherStageSchema` | +| **source** | `Enum<'code' \| 'marketplace' \| 'authored'>` | ✅ | Where the installed package came from: code (registered by the deployment's own code), marketplace (installed from the marketplace), or authored (a database-authored package) | | **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 | @@ -230,7 +233,7 @@ Install package response | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **package** | `{ manifest: object; status?: Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>; enabled?: boolean; installedAt?: string; … }` | ✅ | Installed package details | +| **package** | `{ manifest: object; source: Enum<'code' \| 'marketplace' \| 'authored'>; status?: Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>; enabled?: boolean; … }` | ✅ | Installed package details | | **message** | `string` | optional | Installation status message | | **dependencyResolution** | `{ dependencies: object[]; canProceed: boolean; requiredActions: object[]; installOrder: string[]; … }` | optional | Dependency resolution result from install analysis | @@ -239,6 +242,7 @@ Install package response | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **manifest** | `{ id: string; namespace?: string; defaultDatasource?: string; version: string; … }` | ✅ | Package manifest at the AUTHORING stage; a row installed by a `defineStack()` host carries the assembled body instead — see `AssembledInstalledPackageSchema` / `InstalledPackageAtEitherStageSchema` | +| **source** | `Enum<'code' \| 'marketplace' \| 'authored'>` | ✅ | Where the installed package came from: code (registered by the deployment's own code), marketplace (installed from the marketplace), or authored (a database-authored package) | | **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 | @@ -273,6 +277,7 @@ Installed package with runtime lifecycle state | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **manifest** | `{ id: string; namespace?: string; defaultDatasource?: string; version: string; … }` | ✅ | Package manifest at the AUTHORING stage; a row installed by a `defineStack()` host carries the assembled body instead — see `AssembledInstalledPackageSchema` / `InstalledPackageAtEitherStageSchema` | +| **source** | `Enum<'code' \| 'marketplace' \| 'authored'>` | ✅ | Where the installed package came from: code (registered by the deployment's own code), marketplace (installed from the marketplace), or authored (a database-authored package) | | **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 | @@ -327,6 +332,19 @@ Installed package with runtime lifecycle state | **migrationLog** | `string[]` | optional | Migration step logs | +--- + +## InstalledPackageSource + +Where the installed package came from: code (registered by the deployment's own code), marketplace (installed from the marketplace), or authored (a database-authored package) + +### Allowed Values + +* `code` +* `marketplace` +* `authored` + + --- ## ListPackagesRequest @@ -352,7 +370,7 @@ List packages response | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **packages** | `{ manifest: object; status?: Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>; enabled?: boolean; installedAt?: string; … }[]` | ✅ | List of installed packages | +| **packages** | `{ manifest: object; source: Enum<'code' \| 'marketplace' \| 'authored'>; status?: Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>; enabled?: boolean; … }[]` | ✅ | List of installed packages | | **total** | `number` | ✅ | Total package count | ### Nested Shape: `ListPackagesResponse.packages[number]` @@ -362,6 +380,7 @@ Installed package with runtime lifecycle state | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **manifest** | `{ id: string; namespace?: string; defaultDatasource?: string; version: string; … }` | ✅ | Package manifest at the AUTHORING stage; a row installed by a `defineStack()` host carries the assembled body instead — see `AssembledInstalledPackageSchema` / `InstalledPackageAtEitherStageSchema` | +| **source** | `Enum<'code' \| 'marketplace' \| 'authored'>` | ✅ | Where the installed package came from: code (registered by the deployment's own code), marketplace (installed from the marketplace), or authored (a database-authored package) | | **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 | diff --git a/packages/spec/api-surface/kernel.json b/packages/spec/api-surface/kernel.json index d94ff4da222..dde89b4d2b2 100644 --- a/packages/spec/api-surface/kernel.json +++ b/packages/spec/api-surface/kernel.json @@ -145,6 +145,8 @@ "InstalledPackage (type)", "InstalledPackageParsed (type)", "InstalledPackageSchema (const)", + "InstalledPackageSource (type)", + "InstalledPackageSourceSchema (const)", "KernelContext (type)", "KernelContextParsed (type)", "KernelContextSchema (const)", diff --git a/packages/spec/declaration-map/kernel.json b/packages/spec/declaration-map/kernel.json index ecf4b9f886d..960d01a361f 100644 --- a/packages/spec/declaration-map/kernel.json +++ b/packages/spec/declaration-map/kernel.json @@ -97,6 +97,8 @@ "InstallPackageResponseSchema": "kernel/InstallPackageResponse", "InstalledPackage": "kernel/InstalledPackage", "InstalledPackageSchema": "kernel/InstalledPackage", + "InstalledPackageSource": "kernel/InstalledPackageSource", + "InstalledPackageSourceSchema": "kernel/InstalledPackageSource", "KernelContext": "kernel/KernelContext", "KernelContextSchema": "kernel/KernelContext", "KernelSecurityPolicy": "kernel/KernelSecurityPolicy", diff --git a/packages/spec/export-origins/kernel.json b/packages/spec/export-origins/kernel.json index 0275dfd4f21..db2f33dd37e 100644 --- a/packages/spec/export-origins/kernel.json +++ b/packages/spec/export-origins/kernel.json @@ -144,6 +144,8 @@ "InstalledPackage": "src/kernel/package-registry.zod.ts#InstalledPackage (type)", "InstalledPackageParsed": "src/kernel/package-registry.zod.ts#InstalledPackageParsed (type)", "InstalledPackageSchema": "src/kernel/package-registry.zod.ts#InstalledPackageSchema (const)", + "InstalledPackageSource": "src/kernel/package-registry.zod.ts#InstalledPackageSource (type)", + "InstalledPackageSourceSchema": "src/kernel/package-registry.zod.ts#InstalledPackageSourceSchema (const)", "KernelContext": "src/kernel/context.zod.ts#KernelContext (type)", "KernelContextParsed": "src/kernel/context.zod.ts#KernelContextParsed (type)", "KernelContextSchema": "src/kernel/context.zod.ts#KernelContextSchema (const)", From ff283ff1fdd9eb03e81d3d53ce3f6e7391b3a5d8 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 17:54:25 +0000 Subject: [PATCH 06/10] test(objectql): the protocol install fixture reads the authored source argument Claude-Session: https://claude.ai/code/session_01RGdjFAX2oq1VdGNT5WGdKN Co-authored-by: Claude --- packages/objectql/src/protocol-install-package.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/objectql/src/protocol-install-package.test.ts b/packages/objectql/src/protocol-install-package.test.ts index 8ac578b498d..bd3afc2beb2 100644 --- a/packages/objectql/src/protocol-install-package.test.ts +++ b/packages/objectql/src/protocol-install-package.test.ts @@ -63,6 +63,7 @@ describe('protocol.installPackage (ADR-0033 consolidation)', () => { // from the id (`app.demo` → `demo`) since none was declared (see below). expect(registry.installPackage).toHaveBeenCalledWith( { ...manifest, namespace: 'demo' }, + 'authored', undefined, ); expect(publish).toHaveBeenCalledTimes(1); @@ -93,7 +94,7 @@ describe('protocol.installPackage (ADR-0033 consolidation)', () => { const { protocol, registry } = makeProtocol(); const manifest = { id: 'app.s', name: 'S', version: '1.0.0', type: 'application' }; await protocol.installPackage({ manifest, settings: { theme: 'dark' } } as never); - expect(registry.installPackage).toHaveBeenCalledWith(manifest, { theme: 'dark' }); + expect(registry.installPackage).toHaveBeenCalledWith(manifest, 'authored', { theme: 'dark' }); }); it('still registers in-memory when no package service is present (non-fatal)', async () => { From ac543ec65fa0f3e625942e0e3b3bc47b16a09f1b Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 18:14:55 +0000 Subject: [PATCH 07/10] test(runtime): the read-door pins read the declared source and writable Claude-Session: https://claude.ai/code/session_01RGdjFAX2oq1VdGNT5WGdKN Co-authored-by: Claude --- .../packages-install-manifest-id.test.ts | 2 +- .../packages-install-manifest-version.test.ts | 2 +- ...s-read-delete-response-conformance.test.ts | 21 ++++++++-------- .../src/domains/packages-single-door.test.ts | 16 +++++++++---- .../domains/packages-writable-verdict.test.ts | 24 ++++++++++++++----- packages/runtime/src/http-dispatcher.test.ts | 2 +- 6 files changed, 43 insertions(+), 24 deletions(-) diff --git a/packages/runtime/src/domains/packages-install-manifest-id.test.ts b/packages/runtime/src/domains/packages-install-manifest-id.test.ts index 422f9fda11f..291dd31513c 100644 --- a/packages/runtime/src/domains/packages-install-manifest-id.test.ts +++ b/packages/runtime/src/domains/packages-install-manifest-id.test.ts @@ -333,7 +333,7 @@ describe('§2 a conforming id still installs', () => { // The gate sits ahead of BOTH limbs, so the fallback limb is reached by // a conforming manifest exactly as before this card. expect(r.response?.status).toBe(201); - expect(registryInstall).toHaveBeenCalledWith(WELL_FORMED, undefined); + expect(registryInstall).toHaveBeenCalledWith(WELL_FORMED, 'authored', undefined); }); }); diff --git a/packages/runtime/src/domains/packages-install-manifest-version.test.ts b/packages/runtime/src/domains/packages-install-manifest-version.test.ts index 4641c36991d..f1c921ef1fa 100644 --- a/packages/runtime/src/domains/packages-install-manifest-version.test.ts +++ b/packages/runtime/src/domains/packages-install-manifest-version.test.ts @@ -288,7 +288,7 @@ describe('§2 a well-formed manifest still installs', () => { // The gate sits ahead of BOTH limbs, so the fallback limb is reached by // a well-formed manifest exactly as before this card. expect(r.response?.status).toBe(201); - expect(registryInstall).toHaveBeenCalledWith(WELL_FORMED, undefined); + expect(registryInstall).toHaveBeenCalledWith(WELL_FORMED, 'authored', undefined); }); }); diff --git a/packages/runtime/src/domains/packages-read-delete-response-conformance.test.ts b/packages/runtime/src/domains/packages-read-delete-response-conformance.test.ts index 628c939eef4..49f8c0415ae 100644 --- a/packages/runtime/src/domains/packages-read-delete-response-conformance.test.ts +++ b/packages/runtime/src/domains/packages-read-delete-response-conformance.test.ts @@ -298,18 +298,16 @@ describe('#16781 — GET /packages: the F2 gap is closed on EVERY authoring path }); /** - * ⚠️ The declaration is a strict SUBSET of the wire on this row too, and - * the residue is named rather than hidden — the disposition its `DELETE` - * sibling already carries below. + * [#23972] The declaration now covers the whole row: `writable` (this + * door's own computed verdict, #14375) is declared on the served row and + * `source` on the record, so a declared parse drops nothing. Before, the + * residue here was exactly `['writable']`. */ - it('the UNDECLARED per-row residue is exactly `writable`', async () => { + it('no per-row residue: `source` and `writable` are both declared', async () => { const r = await send('GET', `${PREFIX}/packages`, [CODE_PKG]); const parsed: any = ListInstalledPackagesResponseSchema.parse(r.body); - // `writable` is this door's own computed verdict (#14375), not a - // declared record field, so a declared parse drops it. Deleting it from - // the wire is a payload removal; declaring it is a separate decision - // about what the response promises. - expect(strippedKeys(r.body.data.packages[0], parsed.data.packages[0])).toEqual(['writable']); + expect(strippedKeys(r.body.data.packages[0], parsed.data.packages[0])).toEqual([]); + expect(parsed.data.packages[0]).toMatchObject({ source: 'code', writable: expect.any(Boolean) }); }); }); @@ -340,10 +338,11 @@ describe('#17431 — GET /packages/:id serves the same row, and parses on BOTH a expect(verdict.error!.issues.map((i) => i.path.join('.'))).toEqual(['manifest.objects.0']); }); - it('the UNDECLARED residue is exactly `writable` — named, not hidden', async () => { + it('no residue: `source` and `writable` are both declared (#23972)', async () => { const r = await get(CODE_PKG); const parsed: any = GetInstalledPackageResponseSchema.parse(r.body); - expect(strippedKeys(r.body.data, parsed.data)).toEqual(['writable']); + expect(strippedKeys(r.body.data, parsed.data)).toEqual([]); + expect(parsed.data).toMatchObject({ source: 'code', writable: expect.any(Boolean) }); }); }); diff --git a/packages/runtime/src/domains/packages-single-door.test.ts b/packages/runtime/src/domains/packages-single-door.test.ts index 20af0e2387c..59e16958635 100644 --- a/packages/runtime/src/domains/packages-single-door.test.ts +++ b/packages/runtime/src/domains/packages-single-door.test.ts @@ -177,6 +177,9 @@ describe('/packages — one implementation, and its 404 wording says which (#145 } }); + /** The retired REST door's row stamp — its values, never this door's. */ + const RETIRED_REST_SOURCE_STAMPS = ['registry', 'database', 'both']; + for (const [label, base] of [['unscoped', UNSCOPED], ['environment-scoped', SCOPED]] as const) { it(`${label} GET /packages/:id for a missing package answers the dispatcher's spelling, never the retired REST one`, async () => { const r = await send('GET', `${base}/${MISSING}`); @@ -195,13 +198,17 @@ describe('/packages — one implementation, and its 404 wording says which (#145 expect(r.body?.error?.message).toBe(DISPATCHER_SPELLING); }); - it(`${label} GET /packages/:id for an installed package answers the BARE row under data — no { package } wrapper, no source stamp`, async () => { + it(`${label} GET /packages/:id for an installed package answers the BARE row under data — no { package } wrapper, no REST source stamp`, async () => { const r = await send('GET', `${base}/${PKG_ID}`); expect(r.status).toBe(200); expect(r.body?.success).toBe(true); expect(r.body?.data?.manifest?.id).toBe(PKG_ID); expect(r.body?.data?.package).toBeUndefined(); - expect('source' in (r.body?.data ?? {})).toBe(false); + // [#23972] The row's `source` is the declared origin the registering + // door stamped, ⛔ never the retired REST door's + // `'registry' | 'database' | 'both'` stamp. + expect(r.body?.data?.source).toBe('code'); + expect(RETIRED_REST_SOURCE_STAMPS).not.toContain(r.body?.data?.source); // The dispatcher envelope, measured: `data` + `meta` beside the flag. // The retired REST door answered `{ data, success }` with the row // one level down under `data.package` — the key set is part of @@ -209,7 +216,7 @@ describe('/packages — one implementation, and its 404 wording says which (#145 expect(Object.keys(r.body).sort()).toEqual(['data', 'meta', 'success']); }); - it(`${label} GET /packages answers { packages, total, hasMore } and its rows carry no source stamp`, async () => { + it(`${label} GET /packages answers { packages, total, hasMore } and its rows carry no REST source stamp`, async () => { const r = await send('GET', base); expect(r.status).toBe(200); expect(r.body?.success).toBe(true); @@ -218,7 +225,8 @@ describe('/packages — one implementation, and its 404 wording says which (#145 expect(r.body?.data?.hasMore).toBe(false); expect(r.body?.data?.packages).toHaveLength(1); expect(r.body?.data?.packages[0]?.manifest?.id).toBe(PKG_ID); - expect('source' in r.body.data.packages[0]).toBe(false); + expect(r.body.data.packages[0].source).toBe('code'); + expect(RETIRED_REST_SOURCE_STAMPS).not.toContain(r.body.data.packages[0].source); }); } diff --git a/packages/runtime/src/domains/packages-writable-verdict.test.ts b/packages/runtime/src/domains/packages-writable-verdict.test.ts index 99fe4086a2d..5bdfa1164d1 100644 --- a/packages/runtime/src/domains/packages-writable-verdict.test.ts +++ b/packages/runtime/src/domains/packages-writable-verdict.test.ts @@ -251,13 +251,25 @@ describe('GET /packages — every row carries the `source` its registering door }); it('the served body parses through its declared contract, `source` and `writable` kept', async () => { - const r = await make().dispatcher.handlePackages('/', 'GET', undefined, {}, reader()); + // Complete manifests, one per origin: the file's shared fixture leaves + // `type` off, which the declared manifest refuses on its own account. + const registry = new SchemaRegistry({ logLevel: 'silent' } as any); + const full = (id: string) => manifest(id, { type: 'app' }); + registry.installPackage(full(CODE_PROJECT), 'code'); + registry.installPackage(full(CODE_MODULE), 'marketplace'); + registry.installPackage(full(DB_BASE), 'authored'); + const objectql = { registry, manifests: new Map([[CODE_PROJECT, full(CODE_PROJECT)], [CODE_MODULE, full(CODE_MODULE)]]) }; + const dispatcher = new HttpDispatcher({ context: { getService: (name: string) => (name === 'objectql' ? objectql : null) } } as any); + + const r = await dispatcher.handlePackages('/', 'GET', undefined, {}, reader()); const parsed = ListInstalledPackagesResponseSchema.parse(r.response?.body); - for (const row of parsed.data.packages) { - expect(InstalledPackageSourceSchema.options).toContain(row.source); - expect(typeof row.writable).toBe('boolean'); - } - const detail = await make().dispatcher.handlePackages(`/${DB_BASE}`, 'GET', undefined, {}, reader()); + expect(parsed.data.packages.map((row) => [row.manifest.id, row.source, row.writable])).toEqual([ + [CODE_PROJECT, 'code', false], + [CODE_MODULE, 'marketplace', false], + [DB_BASE, 'authored', true], + ]); + expect(parsed.data.packages.every((row) => InstalledPackageSourceSchema.options.includes(row.source))).toBe(true); + const detail = await dispatcher.handlePackages(`/${DB_BASE}`, 'GET', undefined, {}, reader()); expect(GetInstalledPackageResponseSchema.parse(detail.response?.body).data).toMatchObject({ source: 'authored', writable: true }); }); }); diff --git a/packages/runtime/src/http-dispatcher.test.ts b/packages/runtime/src/http-dispatcher.test.ts index f304e134a26..0b92e0cfc69 100644 --- a/packages/runtime/src/http-dispatcher.test.ts +++ b/packages/runtime/src/http-dispatcher.test.ts @@ -2447,7 +2447,7 @@ describe('HttpDispatcher', () => { const result = await dispatcher.handlePackages('', 'POST', { manifest }, {}, PKG_ADMIN()); expect(result.response?.status).toBe(201); - expect(mockRegistry.installPackage).toHaveBeenCalledWith(manifest, undefined); + expect(mockRegistry.installPackage).toHaveBeenCalledWith(manifest, 'authored', undefined); expect((result.response as any)?.body?.data?.manifest?.id).toBe('app.fb'); }); }); From b9174993a9eef77e0b3231ee9b8df432a34fa38f Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 18:16:38 +0000 Subject: [PATCH 08/10] fix(spec): InstalledPackageSource names the author state (ADR-0122), isomorphism pinned Claude-Session: https://claude.ai/code/session_01RGdjFAX2oq1VdGNT5WGdKN Co-authored-by: Claude --- packages/spec/src/kernel/package-registry.zod.ts | 2 +- packages/spec/src/type-alias-convention.pin.test.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/spec/src/kernel/package-registry.zod.ts b/packages/spec/src/kernel/package-registry.zod.ts index 4ef3510b253..a783ebd9f94 100644 --- a/packages/spec/src/kernel/package-registry.zod.ts +++ b/packages/spec/src/kernel/package-registry.zod.ts @@ -69,7 +69,7 @@ export const InstalledPackageSourceSchema = z.enum([ 'marketplace', 'authored', ]).describe('Where the installed package came from: code (registered by the deployment\'s own code), marketplace (installed from the marketplace), or authored (a database-authored package)'); -export type InstalledPackageSource = z.infer; +export type InstalledPackageSource = z.input; /** * Installed Package Schema diff --git a/packages/spec/src/type-alias-convention.pin.test.ts b/packages/spec/src/type-alias-convention.pin.test.ts index 914a55a7216..8c9f6a85640 100644 --- a/packages/spec/src/type-alias-convention.pin.test.ts +++ b/packages/spec/src/type-alias-convention.pin.test.ts @@ -1011,6 +1011,7 @@ export type Iso_kernel_packageArtifact__MetadataCategoryEnum = Assert, z.infer< typeof M91.DisablePackageRequestSchema > >>; export type Iso_kernel_packageRegistry__EnablePackageRequestSchema = Assert, z.infer< typeof M91.EnablePackageRequestSchema > >>; export type Iso_kernel_packageRegistry__GetPackageRequestSchema = Assert, z.infer< typeof M91.GetPackageRequestSchema > >>; +export type Iso_kernel_packageRegistry__InstalledPackageSourceSchema = Assert, z.infer< typeof M91.InstalledPackageSourceSchema > >>; export type Iso_kernel_packageRegistry__ListPackagesRequestSchema = Assert, z.infer< typeof M91.ListPackagesRequestSchema > >>; export type Iso_kernel_packageRegistry__NamespaceConflictErrorSchema = Assert, z.infer< typeof M91.NamespaceConflictErrorSchema > >>; export type Iso_kernel_packageRegistry__NamespaceRegistryEntrySchema = Assert, z.infer< typeof M91.NamespaceRegistryEntrySchema > >>; From 410897be9de2e223fc3aa550c6180ae3fb9bbbbe Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 18:26:48 +0000 Subject: [PATCH 09/10] test(spec): count the InstalledPackageSource isomorphism pin (788 -> 789) Claude-Session: https://claude.ai/code/session_01RGdjFAX2oq1VdGNT5WGdKN Co-authored-by: Claude --- packages/spec/src/type-alias-convention.pin.test.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/spec/src/type-alias-convention.pin.test.ts b/packages/spec/src/type-alias-convention.pin.test.ts index 8c9f6a85640..fd7f3f4b42e 100644 --- a/packages/spec/src/type-alias-convention.pin.test.ts +++ b/packages/spec/src/type-alias-convention.pin.test.ts @@ -277,7 +277,7 @@ import type * as M189 from './system/fate-supplement.zod.js'; import type * as M190 from './kernel/execution-context.zod.js'; // --------------------------------------------------------------------------- -// 788 isomorphic aliases: `z.input` === `z.infer`, so no `XParsed` is declared. +// 789 isomorphic aliases: `z.input` === `z.infer`, so no `XParsed` is declared. // // That number is machine-checked, not hand-kept. The runtime companion at the // bottom of this file recomputes the pin count from the source and asserts that @@ -1703,7 +1703,7 @@ describe('ADR-0122 type-alias convention', () => { // this title and the section header above the pin list — are now asserted // against the recomputed count below, so neither can go stale without a red // test naming it. - it('still declares all 788 isomorphic pins', () => { + it('still declares all 789 isomorphic pins', () => { // The truth of each pin is proved by tsc, not here — an `Assert>` // that stops holding is a compile error with the alias named. What tsc // cannot notice is a pin that was DELETED: removing the assertion removes @@ -2536,7 +2536,13 @@ describe('ADR-0122 type-alias convention', () => { // and no transform anywhere in its tree, so it is pinned here // (Iso_ui_component__RecordJsonProps) rather than given a `…Parsed` // synonym. +1 added. - expect(pins).toHaveLength(788); + // + // 788 -> 789 is #23972: `InstalledPackageSourceSchema` + // (`kernel/package-registry.zod.ts`), the installed-package row's closed + // origin — a plain string enum, so it is pinned here + // (Iso_kernel_packageRegistry__InstalledPackageSourceSchema) rather than + // given a `…Parsed` synonym. +1 added. + expect(pins).toHaveLength(789); // The count is stated in PROSE twice as well — this case's title and the // section header above the pin list — and until commit c6b05c76a nothing read either From 7c45b88c05570a1f1c50dd403a860efa14fc75d1 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 19:18:30 +0000 Subject: [PATCH 10/10] feat(spec)!: declare the installed-package source narrowing and register it (ADR-0087 D3) Claude-Session: https://claude.ai/code/session_01RGdjFAX2oq1VdGNT5WGdKN Co-authored-by: Claude --- .changeset/23972-installed-package-source.md | 8 +++-- ...8.installed-package-row-source-required.ts | 36 +++++++++++++++++++ ...egistry-install-package-source-argument.ts | 31 ++++++++++++++++ 3 files changed, 73 insertions(+), 2 deletions(-) create mode 100644 packages/spec/src/migrations/entries/semantic/18.installed-package-row-source-required.ts create mode 100644 packages/spec/src/migrations/entries/semantic/18.registry-install-package-source-argument.ts diff --git a/.changeset/23972-installed-package-source.md b/.changeset/23972-installed-package-source.md index d1ccc5d9706..6365d0bd9df 100644 --- a/.changeset/23972-installed-package-source.md +++ b/.changeset/23972-installed-package-source.md @@ -8,9 +8,11 @@ '@objectstack/client': patch --- -feat(spec): every installed-package row declares where it came from, a closed `source: 'code' | 'marketplace' | 'authored'`, and the `GET /packages` rows declare the `writable` verdict they already served (#23972) +feat(spec)!: every installed-package row declares where it came from, a closed `source: 'code' | 'marketplace' | 'authored'`, and the `GET /packages` rows declare the `writable` verdict they already served (#23972) -Clause-②: yes (widening) +Clause-②: yes (narrowing — the served row gains two declared keys, and the record schema, the served-row union and the registry's installPackage signature each refuse what they accepted before) + +**BREAKING** — three published accept sets narrow. `InstalledPackageSchema` refuses a row without `source`, or with a value outside `code` / `marketplace` / `authored`. The served-row union `InstalledPackageAtEitherStageSchema` refuses a row without a boolean `writable`. `SchemaRegistry.installPackage` and `EngineSchemaRegistryView.installPackage` take `source` as their second argument and refuse a call without it. The migration is below. None of it is mechanically convertible, because only the door that registers a package knows where it came from. **What is new on the wire.** `GET /api/v1/packages` and `GET /api/v1/packages/:id` serve two declared keys on every row: @@ -41,3 +43,5 @@ The two keys are separate statements: `source` says where the package came from, **The one-line fix:** pass the package's origin as `installPackage`'s second argument. Readers: objectui's Setup → Packages Source column and its `?source=` filter (it switches from the two-read join to `source` in its own follow-up), and the client SDK's `packages.list()` / `packages.get()` row types. + + diff --git a/packages/spec/src/migrations/entries/semantic/18.installed-package-row-source-required.ts b/packages/spec/src/migrations/entries/semantic/18.installed-package-row-source-required.ts new file mode 100644 index 00000000000..b89517612e6 --- /dev/null +++ b/packages/spec/src/migrations/entries/semantic/18.installed-package-row-source-required.ts @@ -0,0 +1,36 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +import type { SemanticMigration } from '../../types.js'; + +// #23972 — a wire narrowing with no authorable key: every installed-package +// row now states where the package came from, and the served read-door row +// declares the writability verdict it already carried. A reader that parses an +// older row through the published schemas is refused now, so it is registered. +export const entry: SemanticMigration = { + id: 'installed-package-row-source-required', + // No backticks in `surface`: build-upgrade-guide.ts renders it inside a + // code span AND a table cell. + surface: + 'InstalledPackageSchema.source (kernel) on every installed-package row, now required and closed to ' + + 'code, marketplace or authored; and writable on the served GET /api/v1/packages and ' + + 'GET /api/v1/packages/:id row (InstalledPackageAtEitherStageSchema, api-assembled), now required', + replacement: + 'Read the origin from row.source and the writability verdict from row.writable, both served on every row ' + + 'by this runtime. Not mechanically convertible for a row held elsewhere: an InstalledPackage value ' + + 'without source cannot be given one by a reader, because only the door that registered the package ' + + 'knows where it came from; re-read the row from GET /api/v1/packages instead of rebuilding it', + reason: + 'The registry stamps source at registration (code for a package the deployment\'s own code registers, ' + + 'marketplace for an install-local install and its ledger rehydrate, authored for an installPackage ' + + 'install and its sys_packages rehydrate), and the read doors have served writable since the server ' + + 'took over the writability verdict. Declaring both makes the published row parse what this runtime ' + + 'serves. A consumer that kept rows from an older runtime, built fixture rows by hand, or derived the ' + + 'origin by joining writable with a marketplace list must decide what each such row is: the old row ' + + 'carries no statement of origin, and the join reads a control-plane install as code wherever the ' + + 'install-local capability is off.', + acceptanceCriteria: + 'Every row the consumer parses through InstalledPackageSchema carries a source from the closed set, and ' + + 'every served row it parses through InstalledPackageAtEitherStageSchema carries a boolean writable. No ' + + 'reader derives the origin from writable or from a second list; a row it cannot attribute is re-read ' + + 'from the runtime, not guessed.', +}; diff --git a/packages/spec/src/migrations/entries/semantic/18.registry-install-package-source-argument.ts b/packages/spec/src/migrations/entries/semantic/18.registry-install-package-source-argument.ts new file mode 100644 index 00000000000..053221172dc --- /dev/null +++ b/packages/spec/src/migrations/entries/semantic/18.registry-install-package-source-argument.ts @@ -0,0 +1,31 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +import type { SemanticMigration } from '../../types.js'; + +// #23972 — a runtime-API narrowing: the registry's package install takes the +// package's origin as its second argument, and a call without one is refused. +// Every earlier call shape breaks, so it is registered with its prescription. +export const entry: SemanticMigration = { + id: 'registry-install-package-source-argument', + // No backticks in `surface`: build-upgrade-guide.ts renders it inside a + // code span AND a table cell. + surface: + 'SchemaRegistry.installPackage(manifest, settings?, scope?) in @objectstack/objectql and ' + + 'EngineSchemaRegistryView.installPackage(manifest, settings?) in @objectstack/spec/contracts, now ' + + 'installPackage(manifest, source, settings?, scope?) with source one of code, marketplace or authored', + replacement: + 'Pass where the package came from as the second argument: installPackage(manifest, \'code\') for a package ' + + 'your own code registers, \'marketplace\' for a marketplace install, \'authored\' for a database-authored ' + + 'package; move settings and scope one position right. Prefer ObjectQL.registerApp (code by default) or ' + + 'the manifest service\'s register(artifact, { source: \'marketplace\' }) where they fit. Not ' + + 'mechanically convertible: only the caller knows the origin, so no codemod can choose it', + reason: + 'The registry records the origin and never infers it, so a call that does not state it, or states a ' + + 'value outside the closed set, throws a TypeError before anything is recorded. An old call compiled ' + + 'against the earlier signature either fails to type-check or, through an untyped caller, refuses at run ' + + 'time; one that passed settings second now has them read as the origin and refused.', + acceptanceCriteria: + 'Every direct installPackage call in the consumer\'s code passes a source naming the package\'s real ' + + 'origin, its settings and scope moved one position right; the rows those calls produce serve that ' + + 'source on GET /api/v1/packages.', +};