diff --git a/.changeset/plugin-manifest-loading-removed.md b/.changeset/plugin-manifest-loading-removed.md new file mode 100644 index 0000000000..be85c62381 --- /dev/null +++ b/.changeset/plugin-manifest-loading-removed.md @@ -0,0 +1,80 @@ +--- +"@objectstack/spec": major +--- + +refactor(spec)!: remove `manifest.loading` — a whole plugin loading block nothing read, including a sandbox that isolated nothing (#4914) + + + +`manifest.loading` and the entire `PluginLoadingConfig` block behind it are +removed under ADR-0049 enforce-or-remove (maintainer ruling 2026-08-04). The +block declared a complete plugin loading policy — `strategy`, `preload`, +`codeSplitting`, `dynamicImport`, `initialization`, `dependencyResolution`, +`hotReload`, `caching`, `sandboxing`, `monitoring` — and **nothing read any of +it**. A bare-name scan of all three repos (objectstack, cloud, objectui, each +with a control probe proving the scan saw the tree) put every reference inside +`packages/spec` itself: the declaration, its own unit tests, the +`Manifest.loading` embed and the generated artifacts. Authoring it parsed +cleanly, entered the manifest, and configured nothing. + +**FROM → TO:** delete the `loading` key from `objectstack.plugin.json`. There is +nothing to re-declare. Plugins are composed at boot — `defineStack` registers +them and the kernel runs `init` then `start` in an order topologically resolved +from each composed plugin's own `dependencies` / `optionalDependencies` +(`resolvePluginOrder`); the set is fixed until the process restarts. + +**⚠️ `loading.sandboxing` is the reason this is a major rather than tidying.** It +declared `isolationLevel: 'process' | 'vm' | 'iframe' | 'web-worker'`, IPC +transports and an `allowedServices` ACL — and applied **none** of it. If you +authored it believing plugins were isolated, they were not, and they never were: +no process/vm/iframe/worker boundary was ever created and `allowedServices` +gated no call. An inert security control is worse than an absent one because it +is believed — the ADR-0033 hazard, since an AI author reads a published +vocabulary as proof of the capability. The enforced surfaces are the plugin +trust tier (`manifest.runtime`, ADR-0025 §3.6) and the manifest permission +declarations. + +**Hot reload converges on one vocabulary.** `PluginHotReloadSchema` was the dead +one of two: `HotReloadManager` (`packages/core/src/hot-reload.ts`) reads +`HotReloadConfigSchema` in `plugin-lifecycle-advanced.zod.ts`, which is **kept**. +`PLUGIN_STANDARDS.md` §5.1 now points at that surviving side and states its real +status — it has an implementation body but no runtime composes one, so it is a +foundation, not a shipped capability. Enforcing it is a separate future decision +and deliberately not part of this change. + +The retirement kit: + +- **`retiredKey()` tombstone** on `Manifest.loading`, not a plain deletion: + `ManifestSchema` is not `.strict()`, so deleting the key would let zod strip it + in silence — trading an inert declaration for an invisible one (the #3726 / + #3733 shape, ADR-0104). The tombstone is audible through `tsc` (input type + `never`) and through the parse, which raises the prescription itself. +- **Eleven whole defs unpublished** with the carrier key, registered in + `RETIRED_DEFS_BY_MAJOR[17]`: `PluginLoadingConfig`, `PluginLoadingStrategy`, + `PluginPreloadConfig`, `PluginCodeSplitting`, `PluginDynamicImport`, + `PluginInitialization`, `PluginDependencyResolution`, `PluginHotReload`, + `PluginCaching`, `PluginSandboxing`, `PluginPerformanceMonitoring`, plus every + type alias. `PluginLoadingEvent` / `PluginLoadingState` survive — the module's + observational half, never embedded in the config block. +- **ADR-0087 D3 `SemanticMigration`** (`plugin-manifest-loading-retired`) and the + `kernel/Manifest:loading` entry in `RETIRED_KEYS_BY_MAJOR[17]`. Deliberately + **no D2 conversion**: the chain walks a normalized stack and + `applyConversionsToStoredItem` maps a metadata type onto one of its + collections, but `PLURAL_TO_SINGULAR` has no `packages` / `plugins` entry — a + manifest is not a stack collection member, and a stored manifest row passes + that seam through unchanged. A conversion would be a transform with no seam + that ever runs. +- **No liveness-ledger change**: the ledger walks `BUILTIN_METADATA_TYPE_SCHEMAS` + (authorable metadata types), and the kernel manifest is not one, so + `manifest.loading` never had a row to keep or orphan. +- Docs rewritten (`PLUGIN_STANDARDS.md` §5.1/§5.2/§5.4, `PROTOCOL_MAP.md`); + generated baselines and reference pages regenerated. + +**Already-installed packages keep working.** Nothing ever read the block, so +removing it removes no behaviour. A stored manifest that still carries `loading` +degrades to a single `[metadata_spec_invalid]` diagnostic at registration — +`Registry.validate()` is deliberately a diagnostic and not a gate, so bad +metadata is never a data outage — and clears when you delete the key from the +source manifest and reinstall. The enforced channel is `os plugin build`, which +runs `ManifestSchema.safeParse` with the author present and exits non-zero +carrying the prescription. diff --git a/content/docs/protocol/kernel/index.mdx b/content/docs/protocol/kernel/index.mdx index 1321400e59..eb504c540c 100644 --- a/content/docs/protocol/kernel/index.mdx +++ b/content/docs/protocol/kernel/index.mdx @@ -540,7 +540,7 @@ const message = i18n.t('slack.button.send', context.locale); Kernel is the **control plane** that orchestrates ObjectStack: - **Lifecycle Management:** Declarative deployment, zero-downtime upgrades, rollback safety -- **Plugin System:** Microkernel architecture with dependency resolution and sandboxing +- **Plugin System:** Microkernel architecture with dependency resolution and trust tiers - **Configuration:** Unified config with merge strategies and tenant isolation - **i18n:** Multi-language support built into the platform diff --git a/content/docs/references/index.mdx b/content/docs/references/index.mdx index 471515fdfc..4a60bf9d6c 100644 --- a/content/docs/references/index.mdx +++ b/content/docs/references/index.mdx @@ -1,6 +1,6 @@ --- title: Protocol Reference -description: Every schema published by @objectstack/spec — 1586 schemas across 14 protocol modules +description: Every schema published by @objectstack/spec — 1575 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,14 +26,14 @@ counts are sums of the rows they head. Regenerate with | [Data Protocol](/docs/references/data) | 30 | 166 | Objects, fields, queries, filters, datasources and drivers — the ObjectQL layer. | | [Identity Protocol](/docs/references/identity) | 5 | 28 | Users and accounts, organizations, positions, API keys, SCIM provisioning. | | [Integration Protocol](/docs/references/integration) | 1 | 27 | The single connector protocol (ADR-0097) — catalog descriptors and provider-bound instances. | -| [Kernel Protocol](/docs/references/kernel) | 31 | 187 | Plugin lifecycle and manifests, capabilities and security, metadata loading, service registry. | +| [Kernel Protocol](/docs/references/kernel) | 31 | 176 | Plugin lifecycle and manifests, capabilities and security, metadata loading, service registry. | | [QA Protocol](/docs/references/qa) | 1 | 8 | Declarative test suites — scenarios, steps, actions and assertions. | | [Security Protocol](/docs/references/security) | 5 | 27 | Permission sets, row-level security, sharing rules, tenancy posture. | | [Shared Protocol](/docs/references/shared) | 8 | 31 | Primitives used across every protocol — identifiers, HTTP, expressions, error maps, enums. | | [Studio Protocol](/docs/references/studio) | 3 | 35 | Studio designer metadata — the authoring surfaces for the protocols above. | | [System Protocol](/docs/references/system) | 37 | 292 | The runtime environment — logging, jobs, cache, metrics, notifications, i18n and compliance. | | [UI Protocol](/docs/references/ui) | 16 | 147 | Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. | -| **Total** | **200** | **1586** | 14 protocol modules | +| **Total** | **200** | **1575** | 14 protocol modules | --- @@ -215,7 +215,7 @@ The single connector protocol (ADR-0097) — catalog descriptors and provider-bo ## Kernel Protocol -**Source:** `packages/spec/src/kernel/` · **Import:** `@objectstack/spec/kernel` · **31 pages, 187 schemas** +**Source:** `packages/spec/src/kernel/` · **Import:** `@objectstack/spec/kernel` · **31 pages, 176 schemas** Plugin lifecycle and manifests, capabilities and security, metadata loading, service registry. @@ -243,7 +243,7 @@ Plugin lifecycle and manifests, capabilities and security, metadata loading, ser | [`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` | | [`plugin-lifecycle-advanced.zod.ts`](/docs/references/kernel/plugin-lifecycle-advanced) | `AdvancedPluginLifecycleConfig`, `DistributedStateConfig`, `GracefulDegradation`, `HotReloadConfig`, `PluginHealthCheck`, `PluginHealthReport`, `PluginHealthStatus`, `PluginStateSnapshot`, `PluginUpdateStrategy` | -| [`plugin-loading.zod.ts`](/docs/references/kernel/plugin-loading) | `PluginCaching`, `PluginCodeSplitting`, `PluginDependencyResolution`, `PluginDynamicImport`, `PluginHotReload`, `PluginInitialization`, `PluginLoadingConfig`, `PluginLoadingEvent`, `PluginLoadingState`, `PluginLoadingStrategy`, `PluginPerformanceMonitoring`, `PluginPreloadConfig`, `PluginSandboxing` | +| [`plugin-loading.zod.ts`](/docs/references/kernel/plugin-loading) | `PluginLoadingEvent`, `PluginLoadingState` | | [`plugin-registry.zod.ts`](/docs/references/kernel/plugin-registry) | `PluginInstallConfig`, `PluginQualityMetrics`, `PluginRegistryEntry`, `PluginSearchFilters`, `PluginStatistics`, `PluginVendor` | | [`plugin-security.zod.ts`](/docs/references/kernel/plugin-security) | `DependencyGraph`, `DependencyGraphNode`, `PackageDependencyConflict`, `PackageDependencyResolutionResult`, `PluginProvenance`, `PluginTrustScore`, `ResolvedPackageDependency`, `SBOM`, `SBOMEntry`, `SecurityPolicy`, `SecurityScanResult`, `SecurityVulnerability`, `VulnerabilitySeverity` | | [`plugin-security-advanced.zod.ts`](/docs/references/kernel/plugin-security-advanced) | `KernelSecurityPolicy`, `KernelSecurityScanResult`, `KernelSecurityVulnerability`, `PermissionAction`, `PermissionScope`, `PluginPermission`, `PluginPermissionSet`, `PluginSecurityManifest`, `PluginTrustLevel`, `ResourceType`, `RuntimeConfig`, `SandboxConfig` | diff --git a/content/docs/references/kernel/manifest.mdx b/content/docs/references/kernel/manifest.mdx index 3b2ee358b1..0e2042251f 100644 --- a/content/docs/references/kernel/manifest.mdx +++ b/content/docs/references/kernel/manifest.mdx @@ -45,7 +45,7 @@ const result = ManifestSchema.parse(data); | **capabilities** | `{ implements?: object[]; provides?: object[]; requires?: object[]; extensionPoints?: object[]; … }` | optional | Plugin capability declarations for interoperability | | **extensions** | `Record` | optional | Extension points and contributions | | **navigationContributions** | `{ app: string; group?: string; priority?: integer; items: (object \| … +8 more)[] }[]` | optional | Navigation items this package contributes into apps owned by other packages | -| **loading** | `{ strategy?: Enum<'eager' \| 'lazy' \| 'parallel' \| 'deferred' \| 'on-demand'>; preload?: object; codeSplitting?: object; dynamicImport?: object; … }` | optional | Plugin loading and runtime behavior configuration | +| **loading** | `never` | optional | [REMOVED] `manifest.loading` was removed in @objectstack/spec 17.0.0 (#4914, ADR-0049 enforce-or-remove) — the entire block (`strategy`, `preload`, `codeSplitting`, `dynamicImport`, `initialization`, `dependencyResolution`, `hotReload`, `caching`, `sandboxing`, `monitoring`) had no runtime reader in any repo, so authoring it configured nothing. Delete the key. Plugins are composed at boot — `defineStack` registers them and the kernel runs `init` then `start` in an order topologically resolved from each composed plugin's own `dependencies` / `optionalDependencies` (`resolvePluginOrder`); the set is fixed until the process restarts. ⚠️ `loading.sandboxing` in particular never isolated anything: it did not run plugins in a process, vm, iframe or web-worker, and `allowedServices` gated no call. If you were relying on it for isolation, you had none — use the plugin trust tier (`manifest.runtime`) and the permission declarations, which are enforced. | | **engine** | `{ objectstack: string }` | optional | Platform compatibility requirements (legacy; superseded by `engines`) | | **engines** | `{ platform?: string; protocol?: string }` | optional | Plugin compatibility ranges (ADR-0025 §3.2; supersedes `engine`) | | **runtime** | `Enum<'node' \| 'sandbox' \| 'worker'>` | optional | Plugin trust tier (ADR-0025 §3.6) | diff --git a/content/docs/references/kernel/plugin-loading.mdx b/content/docs/references/kernel/plugin-loading.mdx index 2355671f3a..c28c150afb 100644 --- a/content/docs/references/kernel/plugin-loading.mdx +++ b/content/docs/references/kernel/plugin-loading.mdx @@ -7,19 +7,14 @@ description: Plugin Loading protocol schemas # Plugin Loading Protocol -Defines the enhanced plugin loading mechanism for the microkernel architecture. -Inspired by industry best practices from: -- Kubernetes CRDs and Operators -- OSGi Dynamic Module System -- Eclipse Plugin Framework -- Webpack Module Federation +What remains of this module is the **observational** half: the lifecycle +event a loader would emit (`PluginLoadingEventSchema`) and the per-plugin +state it would track (`PluginLoadingStateSchema`). -This protocol enables: -- Lazy loading and code splitting -- Dynamic imports and parallel initialization -- Capability-based discovery -- Hot reload in development -- Advanced caching strategies +The **configuration** half — `PluginLoadingConfigSchema` and the ten member +schemas it combined, reached from authored metadata as `manifest.loading` — +was REMOVED in v17 per ADR-0049 enforce-or-remove (#4914). See the block +below. **Source:** `packages/spec/src/kernel/plugin-loading.zod.ts` @@ -28,147 +23,13 @@ This protocol enables: ## TypeScript Usage ```typescript -import { PluginCachingSchema, PluginCodeSplittingSchema, PluginDependencyResolutionSchema, PluginDynamicImportSchema, PluginHotReloadSchema, PluginInitializationSchema, PluginLoadingConfigSchema, PluginLoadingEventSchema, PluginLoadingStateSchema, PluginLoadingStrategySchema, PluginPerformanceMonitoringSchema, PluginPreloadConfigSchema, PluginSandboxingSchema } from '@objectstack/spec/kernel'; -import type { PluginCaching, PluginCodeSplitting, PluginDependencyResolution, PluginDynamicImport, PluginHotReload, PluginInitialization, PluginLoadingConfig, PluginLoadingEvent, PluginLoadingState, PluginLoadingStrategy, PluginPerformanceMonitoring, PluginPreloadConfig, PluginSandboxing } from '@objectstack/spec/kernel'; +import { PluginLoadingEventSchema, PluginLoadingStateSchema } from '@objectstack/spec/kernel'; +import type { PluginLoadingEvent, PluginLoadingState } from '@objectstack/spec/kernel'; // Validate data -const result = PluginCachingSchema.parse(data); +const result = PluginLoadingEventSchema.parse(data); ``` ---- - -## PluginCaching - -Plugin caching configuration - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **enabled** | `boolean` | ✅ | | -| **storage** | `Enum<'memory' \| 'disk' \| 'indexeddb' \| 'hybrid'>` | ✅ | | -| **keyStrategy** | `Enum<'version' \| 'hash' \| 'timestamp'>` | ✅ | | -| **ttl** | `integer` | optional | Time to live in seconds (0 = infinite) | -| **maxSize** | `integer` | optional | Max cache size in MB | -| **invalidateOn** | `Enum<'version-change' \| 'dependency-change' \| 'manual' \| 'error'>[]` | optional | | -| **compression** | `{ enabled: boolean; algorithm: Enum<'gzip' \| 'brotli' \| 'deflate'> }` | optional | | - - ---- - -## PluginCodeSplitting - -Plugin code splitting configuration - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **enabled** | `boolean` | ✅ | | -| **strategy** | `Enum<'route' \| 'feature' \| 'size' \| 'custom'>` | ✅ | | -| **chunkNaming** | `Enum<'hashed' \| 'named' \| 'sequential'>` | ✅ | | -| **maxChunkSize** | `integer` | optional | Max chunk size in KB | -| **sharedDependencies** | `{ enabled: boolean; minChunks: integer }` | optional | | - - ---- - -## PluginDependencyResolution - -Plugin dependency resolution configuration - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **strategy** | `Enum<'strict' \| 'compatible' \| 'latest' \| 'pinned'>` | ✅ | | -| **peerDependencies** | `{ resolve: boolean; onMissing: Enum<'error' \| 'warn' \| 'ignore'>; onMismatch: Enum<'error' \| 'warn' \| 'ignore'> }` | optional | | -| **optionalDependencies** | `{ load: boolean; onFailure: Enum<'warn' \| 'ignore'> }` | optional | | -| **conflictResolution** | `Enum<'fail' \| 'latest' \| 'oldest' \| 'manual'>` | ✅ | | -| **circularDependencies** | `Enum<'error' \| 'warn' \| 'allow'>` | ✅ | | - - ---- - -## PluginDynamicImport - -Plugin dynamic import configuration - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **enabled** | `boolean` | ✅ | | -| **mode** | `Enum<'async' \| 'sync' \| 'eager' \| 'lazy'>` | ✅ | | -| **prefetch** | `boolean` | ✅ | Prefetch module in idle time | -| **preload** | `boolean` | ✅ | Preload module in parallel with parent | -| **webpackChunkName** | `string` | optional | Custom chunk name for webpack | -| **timeout** | `integer` | ✅ | Dynamic import timeout (ms) | -| **retry** | `{ enabled: boolean; maxAttempts: integer; backoffMs: integer }` | optional | | - - ---- - -## PluginHotReload - -Plugin hot reload configuration - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **enabled** | `boolean` | ✅ | | -| **environment** | `Enum<'development' \| 'staging' \| 'production'>` | ✅ | Target environment controlling safety level | -| **strategy** | `Enum<'full' \| 'partial' \| 'state-preserve'>` | ✅ | | -| **watchPatterns** | `string[]` | optional | Glob patterns for files to watch | -| **ignorePatterns** | `string[]` | optional | Glob patterns for files to ignore | -| **debounceMs** | `integer` | ✅ | | -| **preserveState** | `boolean` | ✅ | | -| **stateSerialization** | `{ enabled: boolean; handler?: string }` | optional | | -| **hooks** | `{ beforeReload?: string; afterReload?: string; onError?: string }` | optional | | -| **productionSafety** | `{ healthValidation: boolean; rollbackOnFailure: boolean; healthTimeout: integer; drainConnections: boolean; … }` | optional | | - - ---- - -## PluginInitialization - -Plugin initialization configuration - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **mode** | `Enum<'sync' \| 'async' \| 'parallel' \| 'sequential'>` | ✅ | | -| **timeout** | `integer` | ✅ | | -| **priority** | `integer` | ✅ | | -| **critical** | `boolean` | ✅ | If true, kernel bootstrap fails if plugin fails | -| **retry** | `{ enabled: boolean; maxAttempts: integer; backoffMs: integer }` | optional | | -| **healthCheckInterval** | `integer` | optional | Health check interval in ms (0 = disabled) | - - ---- - -## PluginLoadingConfig - -Complete plugin loading configuration - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **strategy** | `Enum<'eager' \| 'lazy' \| 'parallel' \| 'deferred' \| 'on-demand'>` | ✅ | Plugin loading strategy | -| **preload** | `{ enabled: boolean; priority: integer; resources?: Enum<'metadata' \| 'dependencies' \| 'assets' \| 'code' \| 'services'>[]; conditions?: object }` | optional | Plugin preloading configuration | -| **codeSplitting** | `{ enabled: boolean; strategy: Enum<'route' \| 'feature' \| 'size' \| 'custom'>; chunkNaming: Enum<'hashed' \| 'named' \| 'sequential'>; maxChunkSize?: integer; … }` | optional | Plugin code splitting configuration | -| **dynamicImport** | `{ enabled: boolean; mode: Enum<'async' \| 'sync' \| 'eager' \| 'lazy'>; prefetch: boolean; preload: boolean; … }` | optional | Plugin dynamic import configuration | -| **initialization** | `{ mode: Enum<'sync' \| 'async' \| 'parallel' \| 'sequential'>; timeout: integer; priority: integer; critical: boolean; … }` | optional | Plugin initialization configuration | -| **dependencyResolution** | `{ strategy: Enum<'strict' \| 'compatible' \| 'latest' \| 'pinned'>; peerDependencies?: object; optionalDependencies?: object; conflictResolution: Enum<'fail' \| 'latest' \| 'oldest' \| 'manual'>; … }` | optional | Plugin dependency resolution configuration | -| **hotReload** | `{ enabled: boolean; environment: Enum<'development' \| 'staging' \| 'production'>; strategy: Enum<'full' \| 'partial' \| 'state-preserve'>; watchPatterns?: string[]; … }` | optional | Plugin hot reload configuration | -| **caching** | `{ enabled: boolean; storage: Enum<'memory' \| 'disk' \| 'indexeddb' \| 'hybrid'>; keyStrategy: Enum<'version' \| 'hash' \| 'timestamp'>; ttl?: integer; … }` | optional | Plugin caching configuration | -| **sandboxing** | `{ enabled: boolean; scope: Enum<'automation-only' \| 'untrusted-only' \| 'all-plugins'>; isolationLevel: Enum<'none' \| 'process' \| 'vm' \| 'iframe' \| 'web-worker'>; allowedCapabilities?: string[]; … }` | optional | Plugin sandboxing configuration | -| **monitoring** | `{ enabled: boolean; metrics?: Enum<'load-time' \| 'init-time' \| 'memory-usage' \| 'cpu-usage' \| 'api-calls' \| … +2 more>[]; samplingRate: number; reportingInterval: integer; … }` | optional | Plugin performance monitoring configuration | - - --- ## PluginLoadingEvent @@ -225,71 +86,3 @@ Plugin loading state --- -## PluginLoadingStrategy - -Plugin loading strategy - -### Allowed Values - -* `eager` -* `lazy` -* `parallel` -* `deferred` -* `on-demand` - - ---- - -## PluginPerformanceMonitoring - -Plugin performance monitoring configuration - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **enabled** | `boolean` | ✅ | | -| **metrics** | `Enum<'load-time' \| 'init-time' \| 'memory-usage' \| 'cpu-usage' \| 'api-calls' \| 'error-rate' \| 'cache-hit-rate'>[]` | optional | | -| **samplingRate** | `number` | ✅ | | -| **reportingInterval** | `integer` | ✅ | | -| **budgets** | `{ maxLoadTimeMs?: integer; maxInitTimeMs?: integer; maxMemoryMB?: integer }` | optional | | -| **onBudgetViolation** | `Enum<'warn' \| 'error' \| 'ignore'>` | ✅ | | - - ---- - -## PluginPreloadConfig - -Plugin preloading configuration - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **enabled** | `boolean` | ✅ | | -| **priority** | `integer` | ✅ | | -| **resources** | `Enum<'metadata' \| 'dependencies' \| 'assets' \| 'code' \| 'services'>[]` | optional | | -| **conditions** | `{ routes?: string[]; roles?: string[]; deviceType?: Enum<'desktop' \| 'mobile' \| 'tablet'>[]; minNetworkSpeed?: Enum<'slow-2g' \| '2g' \| '3g' \| '4g'> }` | optional | | - - ---- - -## PluginSandboxing - -Plugin sandboxing configuration - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **enabled** | `boolean` | ✅ | | -| **scope** | `Enum<'automation-only' \| 'untrusted-only' \| 'all-plugins'>` | ✅ | Which plugins are subject to isolation | -| **isolationLevel** | `Enum<'none' \| 'process' \| 'vm' \| 'iframe' \| 'web-worker'>` | ✅ | | -| **allowedCapabilities** | `string[]` | optional | List of allowed capability IDs | -| **resourceQuotas** | `{ maxMemoryMB?: integer; maxCpuTimeMs?: integer; maxFileDescriptors?: integer; maxNetworkKBps?: integer }` | optional | | -| **permissions** | `{ allowedAPIs?: string[]; allowedPaths?: string[]; allowedEndpoints?: string[]; allowedEnvVars?: string[] }` | optional | | -| **ipc** | `{ enabled: boolean; transport: Enum<'message-port' \| 'unix-socket' \| 'tcp' \| 'memory'>; maxMessageSize: integer; timeout: integer; … }` | optional | | - - ---- - diff --git a/docs/audits/2026-07-unknown-key-strictness-ledger.counts.md b/docs/audits/2026-07-unknown-key-strictness-ledger.counts.md index c6ed9f6704..d72c6d9ea5 100644 --- a/docs/audits/2026-07-unknown-key-strictness-ledger.counts.md +++ b/docs/audits/2026-07-unknown-key-strictness-ledger.counts.md @@ -268,7 +268,7 @@ directory rather than per file. | `cloud/` | 83 | | `identity/` | 33 | | `integration/` | 10 | -| `kernel/` | 319 | +| `kernel/` | 295 | | `qa/` | 6 | | `shared/` | 20 | | `system/` | 362 | diff --git a/docs/protocol-upgrade-guide.md b/docs/protocol-upgrade-guide.md index 74e69c8409..748ce34bdc 100644 --- a/docs/protocol-upgrade-guide.md +++ b/docs/protocol-upgrade-guide.md @@ -230,6 +230,8 @@ One entry in this step is not a removal at all but a SECURE-DEFAULT FLIP, the sh The same descriptor loses a key in this step, and the pairing is the point (#6748, ADR-0049). `ActionDescriptor.isAsync` and `ActionDescriptor.supportsPause` were two spellings of one capability — "this node type can suspend the run" — and #6667 split them by evidence rather than by preference: `supportsPause` took the ENFORCE leg (the engine now refuses a suspension the descriptor never declared, at the one seam every suspension passes through), and `isAsync` takes the REMOVE leg, because a fresh three-repo measurement found zero readers and no consumer it could grow into. What makes the duplicate worse than an ordinary inert key is that five shipped descriptors WROTE it, so the platform itself modelled a declaration that decided nothing — and a plugin author copying `screen` (which declared BOTH) had no way to tell which of the two the runtime honoured. It is tombstoned rather than deleted, so the answer arrives as a rejection carrying the fix; and because a descriptor lives in executor TypeScript rather than in stored metadata, its prescription is a semantic entry below rather than a conversion `os migrate meta` could replay. +The plugin manifest loses its whole `loading` block in this step (#4914, ADR-0049, maintainer ruling 2026-08-04) — the same enforce-or-remove question asked of a block rather than a key, and answered REMOVE on measurement: every reference to `manifest.loading.*` in objectstack, cloud and objectui lived inside `packages/spec` itself, so a full loading policy parsed, entered the manifest, and configured nothing. The reason it outranked ordinary inert-key cleanup is that one of its members was `sandboxing`, declaring process / vm / iframe / web-worker isolation and a service ACL: an inert SECURITY control is worse than an absent one, because an author (very often an AI, ADR-0033) reads the vocabulary as proof the isolation exists and stops looking. Hot reload was a two-source defect on top of that — the retired `PluginHotReloadSchema` was the dead one of two vocabularies, and the ruling converges on the live one, `HotReloadConfigSchema`, which `HotReloadManager` actually reads and which is KEPT unenforced as the starting point for a separate future decision. Like `isAsync`, its prescription is a semantic entry rather than a conversion: a manifest is not a stack collection, so `os migrate meta` has no seam at which to rewrite one. + ### Mechanical (applied for you) | Conversion | Surface | Change | Load window | @@ -416,6 +418,9 @@ The same descriptor loses a key in this step, and the pairing is the point (#674 - **`notification-list-cursor-retired`** — `api.listNotifications cursor — the key on BOTH halves of GET /api/v1/notifications (ListNotificationsRequestSchema and ListNotificationsResponseSchema) and the cursor argument of the client SDK call client.notifications.list(). The same entry covers the limit default: the request schema no longer declares default(20)` → a larger `limit` — the route answers the newest N notifications and has no page 2. There is no replacement for `cursor`, deliberately: nothing ever minted one, so no caller holds a value to carry over. Callers that looped on it were re-reading the first window and should read one window sized to what they display (the Console bell polls exactly this way). For the removed `limit` default, send the number you want explicitly if you were relying on 20 — omitting it takes the server window, which is 50 on the platform inbox and clamped into 1..200, and has been since before the declaration existed - Why not automatic: One capability, both halves, never half-deleted (maintainer ruling 2026-08-07, Option A, ruled jointly with #6363). `cursor` was declared on the request and on the response and honoured on neither: the dispatcher domain reads `read` / `type` / `limit` and nothing else, and no emit site has ever written the response key. It was worse than inert because it had a shipped PRODUCER — the SDK appended it to the query string — so a caller paginating by the published contract looped on page 1 forever, with no error and no 400. Measured over a real boot with 60 unread before the removal: page2 === page1, both parsing green against the response schema, which is why no conformance gate could see it. This is `data.query.cursor` (#4286, `query-cursor-retired`) one layer up, with the same verdict for the same reason, down to deleting the SDK producer alongside the key. A first-class inbox cursor, if one is ever designed, will be a response-minted opaque token — a different API — so keeping this one preserved a wrong design rather than a roadmap. The `limit` default goes with it because the FICTION WAS THE MECHANISM, not the number: no request path parses a query string through this schema (#3899 wired the catalog's requestSchema to the real entry for BODIES only), so `.default(20)` never stamped anything onto anything, and the server has always applied its own 50. Re-spelling 20 as 50 — the other arm the ruling allowed — would have kept a declaration that does not execute and merely made it coincide with the implementation until someone moved the clamp; `.optional()` plus prose is true about both the schema and the server. No constraint (`.int()` / `.max(200)`) is declared either, because the service CLAMPS an out-of-range limit rather than refusing it, and declaring a rejection the wire does not perform is the same defect mirrored. Route 2, and the split is worth stating exactly because the two halves of the bookkeeping go different ways. There IS a tombstone: both schemas are non-strict, so a bare deletion would have made Zod SILENTLY STRIP whatever a caller kept sending — a clean parse and a parameter that never takes effect, which is this issue's own defect re-created one layer down (#3733, ADR-0104). So `cursor` is `retiredKey()` on both halves, typed `never` for tsc and raising the prescription at any parse, and both keys are registered in RETIRED_KEYS_BY_MAJOR[17]. There is NO D2 conversion: a conversion rewrites an authored source or a stored `sys_metadata` row, and these two shapes are HTTP-only — nobody authors a `ListNotificationsRequest` and nothing persists one. Request AND response shapes: two semantic TODOs for API callers, no stack conversion — the same disposition `BatchOptions.validateOnly` (#4052) and the `AnalyticsQueryRequest` envelope keys already take in this major. The `limit` default is declared separately and mechanically, in DEFAULT_CHANGES_BY_MAJOR[17] (#4666), whose `from`/`to` fingerprints are re-derived on every build. ADR-0049 / ADR-0078, #6361. - Done when: No caller sends `cursor` to `GET /api/v1/notifications` and no SDK call site passes it: `client.notifications.list({ cursor })` is a `tsc` error (TS2353, excess property), which is the enforced channel — the removal is loud at compile time for every TypeScript consumer. Reading `response.cursor` no longer type-checks either, and always answered `undefined` before. ⚠️ Behaviour on the wire is deliberately UNCHANGED and must be verified as such: a request still carrying `?cursor=…` is IGNORED, not refused — the domain reads three named query keys and no route validates this query against a schema, so an unknown key has never produced a 400 and does not start doing so here. The declaration stopped promising what the wire never did; the wire did not change. `unreadCount` is untouched (#6363) and still reports the total across the whole matching inbox rather than the window. A caller that omitted `limit` receives the same 50 rows it always received. +- **`plugin-manifest-loading-retired`** — `manifest.loading (the whole block: strategy / preload / codeSplitting / dynamicImport / initialization / dependencyResolution / hotReload / caching / sandboxing / monitoring)` → nothing to re-declare — delete the key. Plugins are composed at boot: `defineStack` registers them and the kernel runs `init` then `start` in an order topologically resolved from each composed plugin's own `dependencies` / `optionalDependencies` (`resolvePluginOrder` in `packages/core/src/plugin-order.ts`). For the isolation `loading.sandboxing` appeared to configure, use the plugin trust tier (`manifest.runtime`, ADR-0025 §3.6) and the manifest permission declarations, which are the surfaces the platform actually enforces + - Why not automatic: ADR-0049 enforce-or-remove; maintainer ruling 2026-08-04 on #4914. The block declared a complete plugin loading policy and NOTHING read it. A bare-name scan of all three repos — objectstack, cloud (measured 2026-08-09) and objectui (measured at pickup), each with a control probe proving the scan saw the tree — put every hit inside `packages/spec` itself: this module's own declaration, its own unit tests, the `Manifest.loading` embed and the generated artifacts. `manifest.loading.*` had zero readers in `packages/core`, `packages/runtime` and `packages/metadata`. So the key parsed, entered the manifest, and changed nothing — #3950, at the scale of a whole block. What made it outrank ordinary inert-key cleanup is `sandboxing`: it declared process / vm / iframe / web-worker isolation, IPC transports and an `allowedServices` ACL, so an AI author (ADR-0033) reading that vocabulary concluded the platform isolates plugins, wrote the config, and received a clean parse and zero isolation. An inert security control is worse than an absent one because it is believed. Hot reload was additionally a TWO-SOURCE defect: the docs pointed at this dead `PluginHotReloadSchema` while the only implementation body, `HotReloadManager` (`packages/core/src/hot-reload.ts`), reads a different vocabulary — `HotReloadConfigSchema` in `plugin-lifecycle-advanced.zod.ts`. Ruling §2 converges on the surviving side: that schema is KEPT as the starting point for a future enforce decision (it has an implementation body but no runtime composes it yet), and enforcing it is deliberately a separate decision, not this retirement. Why D3 semantic and not a D2 conversion: the chain walks a normalized STACK and `applyConversionsToStoredItem` maps a metadata type onto one of its collections. A package manifest is neither — `PLURAL_TO_SINGULAR` has no `packages` / `plugins` entry, so a manifest is not a stack collection member and a stored manifest row passes that seam through unchanged. A conversion would be a transform with no seam that ever runs. + - Done when: No `objectstack.plugin.json` and no stored package manifest carries a `loading` key. The enforced channel is the one place a manifest is parsed with an author present: `os plugin build` runs `ManifestSchema.safeParse` and exits non-zero, printing the tombstone prescription, so a manifest still declaring `loading` fails its build rather than shipping. TypeScript authors get it earlier still — `loading` is typed `never`, so assigning it is a `tsc` error. ⚠️ Runtime behaviour is deliberately UNCHANGED and must be verified as such: nothing ever read the block, so removing it removes no behaviour. A package ALREADY INSTALLED whose stored manifest carries `loading` keeps working — the registry's `validate()` is an explicit diagnostic and not a gate (it catches, logs `[metadata_spec_invalid]`, and registers the item anyway, deliberately, so bad metadata is never a data outage), so such a row degrades to one log line at registration rather than a boot failure. Clear it by deleting the key from the source manifest and reinstalling. - **`api-runtime-create-withdrawn`** — `PUT /api/v1/meta/api/{name} (runtime-authored `api` endpoints, draft and active alike)` → Declare the endpoint as a stack artifact (`**/*.api.ts`, or `defineStack({ apis })`) and ship it through `publishPackage` - Why not automatic: The `api` registry entry declared `allowRuntimeCreate: true` and the runtime never honoured it. Measured on a real showcase boot (#5488): `PUT /api/v1/meta/api/e8_backdoor` answered 200 with `{"success":true,…,"message":"Saved …"}`, and the declared route then answered 404 forever — with NO `[EndpointMatcher] … EXCLUDED` line, because the endpoint was never in the index to be excluded from. The serving criterion belongs to `IMetadataService.matchEndpoint` -> `EndpointMatcher` -> `MetadataManager.listForIndex('api')`, which reads the manager's registry plus its registered loaders (`["filesystem","memory"]` on dev/serve); a runtime write lands in `sys_metadata`, which is in neither. A declared capability the runtime does not honour is ADR-0049 false compliance, and a write that answers "Saved" and then 404s forever is its most dangerous shape for the AI authors ADR-0033 targets. The maintainer ruled REMOVE on 2026-08-07 rather than converge the read path, because making the matcher read `sys_metadata` re-opens cache, invalidation, tenancy and the ADR-0110 D3 miss-vs-outage distinction on a new read path, and there is no business pull for Studio-authored endpoints today (zero `.api.*` artifacts author them at runtime; showcase uses the artifact route, #5040 E8 LIVE). There is NO D2 conversion, for the reason this list exists: nothing in an authored source spells this key. `allowRuntimeCreate` is a PLATFORM registry value, not an authorable one, and the artifact route it points authors toward is untouched — a `**/*.api.ts` file valid before this change is valid after it, byte for byte. What changed is a runtime HTTP verdict, so it is one semantic TODO for operators and Studio callers rather than a stack conversion — the same disposition `BatchOptions.validateOnly` (#4052) takes. Consequently `gateApiDraftsForPublish` (PR #5279) is retired with it: it gated a promotion into a state the matcher can never read, and with the inlet closed no `api` draft can exist for it to judge. Re-entry is recorded in the ruling: if #2657 Part B promotes `apis` to a registered type WITH A REAL CONSUMPTION PATH, the flag flips back then — implementation first, declaration second. ADR-0049 / ADR-0121, #5488 (subsumes #5311). - Done when: No caller creates or updates an `api` item through the runtime metadata API. `PUT /api/v1/meta/api/{name}` answers 403 with `code: "NOT_CREATABLE"` and a body naming both flags (`allowRuntimeCreate=false, allowOrgOverride=false`) and the prescription `Declare it in source (**/*.api.ts) and redeploy` — in `?mode=draft` as well as direct-active, because the gate runs before the draft/publish branch and does not read `mode`. ⚠️ Verify the artifact route is UNAFFECTED, which is the whole point of the change: a stack declaring `apis:` still compiles, still passes `validateApiEndpointDeclarations` at publish (`publishPackage`, #5189) and at load (`buildEndpointIndex`, PR #5203), and its endpoints still SERVE — that route was always the only one that served. An operator who genuinely needs the runtime door back on one deployment sets `OS_METADATA_WRITABLE=api`, the same single escape hatch `job` / `agent` / `capability` use; note that this unlocks the WRITE only, and the endpoint still will not be served, which is why it is a diagnostic and not a workaround. Any `api` rows already sitting in `sys_metadata` from before this change were never served either; they can be deleted (`deleteMetaItem` is deliberately not gated by this refusal, so repair stays possible). diff --git a/packages/spec/PLUGIN_STANDARDS.md b/packages/spec/PLUGIN_STANDARDS.md index 07777b9fd6..92a4044752 100644 --- a/packages/spec/PLUGIN_STANDARDS.md +++ b/packages/spec/PLUGIN_STANDARDS.md @@ -123,38 +123,57 @@ export const LeadObject = ... The microkernel architecture provides the following runtime capabilities for plugins. The Zod schemas governing each capability live in `src/kernel/`. -### 5.1 Hot Reload (`plugin-loading.zod.ts` → `PluginHotReloadSchema`) - -Hot reload supports **development, staging, and production** environments. The `environment` field controls the safety level: - -| Environment | Behavior | -| :--- | :--- | -| `development` | Fast reload with file watchers, no health validation required | -| `staging` | Production-like reload with validation but relaxed rollback | -| `production` | Full safety: health validation, auto-rollback, connection draining | - -Production safety features (`productionSafety`): -- **Health validation** — run health checks after reload before accepting traffic -- **Rollback on failure** — auto-rollback if reloaded plugin fails health check -- **Connection draining** — gracefully drain active requests before reloading -- **Concurrency control** — limit concurrent reloads (`maxConcurrentReloads`) -- **Reload cooldown** — minimum interval between reloads of the same plugin (≥1s) - -### 5.2 Plugin Isolation (`plugin-loading.zod.ts` → `PluginSandboxingSchema`) - -Sandboxing supports configurable **scope** and **isolation level**: - -| Scope | Description | -| :--- | :--- | -| `automation-only` | Sandbox automation/scripting plugins only (default) | -| `untrusted-only` | Sandbox plugins below a trust threshold | -| `all-plugins` | Sandbox all plugins for maximum isolation | - -Isolation levels: `none`, `process`, `vm`, `iframe`, `web-worker`. - -**Inter-Plugin Communication (IPC):** Isolated plugins communicate with the kernel and other plugins via configurable IPC: -- Transports: `message-port`, `unix-socket`, `tcp`, `memory` -- Configurable message size limit, timeout, and service ACL (`allowedServices`) +### 5.1 Hot Reload — ~~`plugin-loading.zod.ts` → `PluginHotReloadSchema`~~ RETIRED in v17; one vocabulary survives + +`PluginHotReloadSchema` was removed in v17 (#4914, ADR-0049 enforce-or-remove), +with the rest of the `manifest.loading` block (§5.2). It was the **dead one of +two** hot-reload vocabularies, and this page used to point at exactly that one: +an author following it configured `environment`, `productionSafety`, health +validation, auto-rollback, connection draining and `maxConcurrentReloads` — and +none of it was read by anything. + +**The surviving vocabulary** is `plugin-lifecycle-advanced.zod.ts` → +`HotReloadConfigSchema`, carried on `AdvancedPluginLifecycleConfig.hotReload`. +That is the one with an implementation body behind it: `HotReloadManager` in +`packages/core/src/hot-reload.ts` reads it. + +⚠️ **Status, stated honestly: a foundation, not a shipped capability.** +`HotReloadManager` exists and is unit-tested, but **no runtime composes one** — +the only constructions are its own test and +`packages/core/examples/phase2-integration.ts`. So configuring +`AdvancedPluginLifecycleConfig.hotReload` today does not give a running system +hot reload either. It is kept, unenforced, as the starting point if hot reload is +ever built for real; making it enforced is a separate decision (ADR-0049's +enforce leg) and deliberately **not** part of the #4914 retirement. Treat this +section as "one honest pointer", not as a feature you can turn on. + +### 5.2 Plugin Isolation — ~~`plugin-loading.zod.ts` → `PluginSandboxingSchema`~~ REMOVED in v17 + +`PluginSandboxingSchema` — and the whole `manifest.loading` block that carried it +— was removed in v17 (#4914, ADR-0049 enforce-or-remove, maintainer ruling +2026-08-04). `Manifest.loading` is now a `retiredKey()` tombstone: authoring it +is a `tsc` error and a parse error carrying the fix. + +It declared configurable sandbox **scope** (`automation-only` / `untrusted-only` +/ `all-plugins`), **isolation levels** (`none` / `process` / `vm` / `iframe` / +`web-worker`), IPC transports (`message-port` / `unix-socket` / `tcp` / +`memory`) and a service ACL (`allowedServices`) — and **none of it was ever +read**. A bare-name scan of objectstack, cloud and objectui (each with a control +probe) found every reference inside `packages/spec` itself: the declaration, its +own unit tests, the `Manifest.loading` embed and generated artifacts. + +**Why this one was urgent rather than untidy.** An inert *security* control is +worse than an absent one, because it is believed. An author — very often an AI +(ADR-0033) — read this vocabulary as proof the platform isolates plugins, wrote +`loading: { sandboxing: { isolationLevel: 'process' } }`, got a clean parse, and +had **no isolation whatsoever**: no process, vm, iframe or web-worker boundary, +and `allowedServices` gating no call. That is ADR-0049 false compliance at its +sharpest, and it is why the ruling chose REMOVE over an `experimental` marker. + +**What is real:** the plugin trust tier (`manifest.runtime`, ADR-0025 §3.6) and +the manifest permission declarations. If plugin isolation is ever built, it +returns via the enforce route of ADR-0049 through a new ADR — mechanism first, +vocabulary second. ### 5.3 Dynamic Loading — ~~`plugin-runtime.zod.ts`~~ REMOVED in v17 @@ -184,8 +203,8 @@ through a new ADR — loader first, vocabulary second. | Lifecycle Hooks | ✅ | `plugin.zod.ts` — `init()` → `start()` → `healthCheck()` → `destroy()` | | Service Registry | ✅ | `service-registry.zod.ts` — 17 services across 13 plugins via `ctx.registerService()` | | Event Bus | ✅ | `events.zod.ts` — Pub/sub with pattern matching | -| Dependency Resolution | ✅ | `plugin-loading.zod.ts` — Declared dependencies with conflict resolution | +| Dependency Resolution | ✅ | `manifest.zod.ts` (`dependencies`) + `packages/core/src/plugin-order.ts` — `resolvePluginOrder` topologically orders plugins from each composed plugin's `dependencies` / `optionalDependencies`, erroring on a cycle or a missing hard dependency. (The `PluginDependencyResolution` *config* schema this row used to cite was inert and went with the `loading` block in #4914 — the capability is real, the configuration surface was not) | | Health Checks | ✅ | `plugin-lifecycle-advanced.zod.ts` — Per-plugin health + system aggregation | -| Hot Reload | ✅ | `plugin-loading.zod.ts` — Dev + production-safe with rollback and draining | -| Plugin Isolation | ✅ | `plugin-loading.zod.ts` — Configurable scope + IPC for process boundaries | +| Hot Reload | ⚠️ | **Foundation only, not enforced.** `plugin-lifecycle-advanced.zod.ts` → `HotReloadConfigSchema` is the surviving vocabulary and `HotReloadManager` (`packages/core/src/hot-reload.ts`) reads it — but no runtime composes one, so configuring it changes nothing today. The rival `PluginHotReloadSchema` was removed in v17 (#4914, ADR-0049): it had no reader at all. See §5.1 | +| Plugin Isolation | ❌ | **Not built.** The `PluginSandboxingSchema` vocabulary that declared it (scope, `process`/`vm`/`iframe`/`web-worker` isolation, IPC, `allowedServices` ACL) was removed in v17 (#4914, ADR-0049) — it had no runtime reader, so it isolated nothing while appearing to. Trust tiers (`manifest.runtime`) and permission declarations are the real surfaces. See §5.2 | | Dynamic Loading | ❌ | **Not built.** The `plugin-runtime.zod.ts` vocabulary that declared it was removed in v17 (#4834, ADR-0049) — it had no runtime reader in any repo. Plugins are composed at boot; the set is fixed until restart | diff --git a/packages/spec/PROTOCOL_MAP.md b/packages/spec/PROTOCOL_MAP.md index 77033104a9..ec08bb5a2d 100644 --- a/packages/spec/PROTOCOL_MAP.md +++ b/packages/spec/PROTOCOL_MAP.md @@ -184,7 +184,7 @@ This document serves as the **Grand Map** of the ObjectStack specification. It l | [`events.zod.ts`](src/kernel/events.zod.ts) | | **Kernel Events**. System lifecycle events. | | [`service-registry.zod.ts`](src/kernel/service-registry.zod.ts) | | **Service Registry**. Internal dependency injection. | | [`metadata-loader.zod.ts`](src/kernel/metadata-loader.zod.ts) | | **Loader**. Logic for loading definitions from disk/DB. | -| [`plugin-loading.zod.ts`](src/kernel/plugin-loading.zod.ts) | ⭐ | **Plugin Loading**. Loading strategies, production-safe hot reload (`environment`, `productionSafety`), full plugin sandboxing (`scope`, `ipc`), code splitting, caching, and performance monitoring. | +| [`plugin-loading.zod.ts`](src/kernel/plugin-loading.zod.ts) | | **Plugin Loading (observational only)**. The lifecycle event and per-plugin loading state. The configuration half — loading strategies, hot reload, sandboxing, code splitting, caching, performance monitoring, reached as `manifest.loading` — was REMOVED in v17 (#4914, ADR-0049 enforce-or-remove): it had no runtime reader in any repo. | | [`plugin-registry.zod.ts`](src/kernel/plugin-registry.zod.ts) | | **Plugin Registry**. Metadata for available plugins. | | [`plugin-security.zod.ts`](src/kernel/plugin-security.zod.ts) | | **Plugin Security**. Security verification for plugins. | | [`plugin-versioning.zod.ts`](src/kernel/plugin-versioning.zod.ts) | | **Versioning**. Semantic versioning rules for plugins. | diff --git a/packages/spec/api-surface/kernel.json b/packages/spec/api-surface/kernel.json index 463beb9405..996aa9e59d 100644 --- a/packages/spec/api-surface/kernel.json +++ b/packages/spec/api-surface/kernel.json @@ -273,18 +273,12 @@ "PermissionScope (type)", "PermissionScopeSchema (const)", "PlatformCapabilityProvider (interface)", - "PluginCaching (type)", - "PluginCachingParsed (type)", - "PluginCachingSchema (const)", "PluginCapability (type)", "PluginCapabilityManifest (type)", "PluginCapabilityManifestParsed (type)", "PluginCapabilityManifestSchema (const)", "PluginCapabilityParsed (type)", "PluginCapabilitySchema (const)", - "PluginCodeSplitting (type)", - "PluginCodeSplittingParsed (type)", - "PluginCodeSplittingSchema (const)", "PluginCompatibilityMatrix (type)", "PluginCompatibilityMatrixParsed (type)", "PluginCompatibilityMatrixSchema (const)", @@ -294,16 +288,10 @@ "PluginDefinition (type)", "PluginDependency (type)", "PluginDependencyParsed (type)", - "PluginDependencyResolution (type)", - "PluginDependencyResolutionParsed (type)", "PluginDependencyResolutionResult (type)", "PluginDependencyResolutionResultParsed (type)", "PluginDependencyResolutionResultSchema (const)", - "PluginDependencyResolutionSchema (const)", "PluginDependencySchema (const)", - "PluginDynamicImport (type)", - "PluginDynamicImportParsed (type)", - "PluginDynamicImportSchema (const)", "PluginEngines (type)", "PluginEnginesSchema (const)", "PluginHealthCheck (type)", @@ -313,12 +301,6 @@ "PluginHealthReportSchema (const)", "PluginHealthStatus (type)", "PluginHealthStatusSchema (const)", - "PluginHotReload (type)", - "PluginHotReloadParsed (type)", - "PluginHotReloadSchema (const)", - "PluginInitialization (type)", - "PluginInitializationParsed (type)", - "PluginInitializationSchema (const)", "PluginInstallConfig (type)", "PluginInstallConfigSchema (const)", "PluginIntegrity (type)", @@ -326,23 +308,15 @@ "PluginInterface (type)", "PluginInterfaceParsed (type)", "PluginInterfaceSchema (const)", - "PluginLoadingConfig (type)", - "PluginLoadingConfigParsed (type)", - "PluginLoadingConfigSchema (const)", "PluginLoadingEvent (type)", "PluginLoadingEventSchema (const)", "PluginLoadingState (type)", "PluginLoadingStateParsed (type)", "PluginLoadingStateSchema (const)", - "PluginLoadingStrategy (type)", - "PluginLoadingStrategySchema (const)", "PluginMetadata (type)", "PluginMetadataSchema (const)", "PluginPackaging (type)", "PluginPackagingSchema (const)", - "PluginPerformanceMonitoring (type)", - "PluginPerformanceMonitoringParsed (type)", - "PluginPerformanceMonitoringSchema (const)", "PluginPermission (type)", "PluginPermissionParsed (type)", "PluginPermissionSchema (const)", @@ -351,9 +325,6 @@ "PluginPermissionSetSchema (const)", "PluginPermissions (type)", "PluginPermissionsSchema (const)", - "PluginPreloadConfig (type)", - "PluginPreloadConfigParsed (type)", - "PluginPreloadConfigSchema (const)", "PluginProvenance (type)", "PluginProvenanceParsed (type)", "PluginProvenanceSchema (const)", @@ -365,9 +336,6 @@ "PluginRegistryEntrySchema (const)", "PluginRuntime (type)", "PluginRuntimeSchema (const)", - "PluginSandboxing (type)", - "PluginSandboxingParsed (type)", - "PluginSandboxingSchema (const)", "PluginSchema (const)", "PluginSearchFilters (type)", "PluginSearchFiltersSchema (const)", diff --git a/packages/spec/authorable-defaults/kernel.json b/packages/spec/authorable-defaults/kernel.json index 59c214cdeb..b94eb66aef 100644 --- a/packages/spec/authorable-defaults/kernel.json +++ b/packages/spec/authorable-defaults/kernel.json @@ -94,23 +94,9 @@ "kernel/PackageDependencyResolutionResult:errors = []", "kernel/PackageDependencyResolutionResult:installOrder = []", "kernel/Plugin:type = \"standard\"", - "kernel/PluginCaching:enabled = true", - "kernel/PluginCaching:keyStrategy = \"version\"", - "kernel/PluginCaching:storage = \"memory\"", "kernel/PluginCapability:certified = false", "kernel/PluginCapability:conformance = \"full\"", - "kernel/PluginCodeSplitting:chunkNaming = \"hashed\"", - "kernel/PluginCodeSplitting:enabled = true", - "kernel/PluginCodeSplitting:strategy = \"feature\"", "kernel/PluginDependency:optional = false", - "kernel/PluginDependencyResolution:circularDependencies = \"warn\"", - "kernel/PluginDependencyResolution:conflictResolution = \"latest\"", - "kernel/PluginDependencyResolution:strategy = \"compatible\"", - "kernel/PluginDynamicImport:enabled = true", - "kernel/PluginDynamicImport:mode = \"async\"", - "kernel/PluginDynamicImport:prefetch = false", - "kernel/PluginDynamicImport:preload = false", - "kernel/PluginDynamicImport:timeout = 30000", "kernel/PluginHealthCheck:autoRestart = false", "kernel/PluginHealthCheck:failureThreshold = 3", "kernel/PluginHealthCheck:interval = 30000", @@ -118,35 +104,16 @@ "kernel/PluginHealthCheck:restartBackoff = \"exponential\"", "kernel/PluginHealthCheck:successThreshold = 1", "kernel/PluginHealthCheck:timeout = 5000", - "kernel/PluginHotReload:debounceMs = 300", - "kernel/PluginHotReload:enabled = false", - "kernel/PluginHotReload:environment = \"development\"", - "kernel/PluginHotReload:preserveState = false", - "kernel/PluginHotReload:strategy = \"full\"", - "kernel/PluginInitialization:critical = false", - "kernel/PluginInitialization:mode = \"async\"", - "kernel/PluginInitialization:priority = 100", - "kernel/PluginInitialization:timeout = 30000", "kernel/PluginInstallConfig:autoUpdate = false", "kernel/PluginInterface:stability = \"stable\"", - "kernel/PluginLoadingConfig:strategy = \"lazy\"", "kernel/PluginLoadingState:progress = 0", "kernel/PluginLoadingState:retryCount = 0", - "kernel/PluginPerformanceMonitoring:enabled = false", - "kernel/PluginPerformanceMonitoring:onBudgetViolation = \"warn\"", - "kernel/PluginPerformanceMonitoring:reportingInterval = 60", - "kernel/PluginPerformanceMonitoring:samplingRate = 1", "kernel/PluginPermission:required = true", "kernel/PluginPermission:scope = \"plugin\"", "kernel/PluginPermissionSet:defaultGrant = \"prompt\"", - "kernel/PluginPreloadConfig:enabled = false", - "kernel/PluginPreloadConfig:priority = 100", "kernel/PluginProvenance:attestations = []", "kernel/PluginProvenance:signatures = []", "kernel/PluginRegistryEntry:deprecated = false", - "kernel/PluginSandboxing:enabled = false", - "kernel/PluginSandboxing:isolationLevel = \"none\"", - "kernel/PluginSandboxing:scope = \"automation-only\"", "kernel/PluginSearchFilters:limit = 20", "kernel/PluginSearchFilters:page = 1", "kernel/PluginSearchFilters:sortOrder = \"desc\"", diff --git a/packages/spec/authorable-surface/kernel.json b/packages/spec/authorable-surface/kernel.json index 79599c169b..7fed1178b2 100644 --- a/packages/spec/authorable-surface/kernel.json +++ b/packages/spec/authorable-surface/kernel.json @@ -325,7 +325,7 @@ "kernel/Manifest:extensions", "kernel/Manifest:id", "kernel/Manifest:integrity", - "kernel/Manifest:loading", + "kernel/Manifest:loading [RETIRED]", "kernel/Manifest:name", "kernel/Manifest:namespace", "kernel/Manifest:navigationContributions", @@ -491,13 +491,6 @@ "kernel/Plugin:staticPath", "kernel/Plugin:type", "kernel/Plugin:version", - "kernel/PluginCaching:compression", - "kernel/PluginCaching:enabled", - "kernel/PluginCaching:invalidateOn", - "kernel/PluginCaching:keyStrategy", - "kernel/PluginCaching:maxSize", - "kernel/PluginCaching:storage", - "kernel/PluginCaching:ttl", "kernel/PluginCapability:certificationDate", "kernel/PluginCapability:certified", "kernel/PluginCapability:conformance", @@ -510,11 +503,6 @@ "kernel/PluginCapabilityManifest:implements", "kernel/PluginCapabilityManifest:provides", "kernel/PluginCapabilityManifest:requires", - "kernel/PluginCodeSplitting:chunkNaming", - "kernel/PluginCodeSplitting:enabled", - "kernel/PluginCodeSplitting:maxChunkSize", - "kernel/PluginCodeSplitting:sharedDependencies", - "kernel/PluginCodeSplitting:strategy", "kernel/PluginCompatibilityMatrix:compatibilityMatrix", "kernel/PluginCompatibilityMatrix:currentVersion", "kernel/PluginCompatibilityMatrix:minimumCompatibleVersion", @@ -525,24 +513,12 @@ "kernel/PluginDependency:reason", "kernel/PluginDependency:requiredCapabilities", "kernel/PluginDependency:version", - "kernel/PluginDependencyResolution:circularDependencies", - "kernel/PluginDependencyResolution:conflictResolution", - "kernel/PluginDependencyResolution:optionalDependencies", - "kernel/PluginDependencyResolution:peerDependencies", - "kernel/PluginDependencyResolution:strategy", "kernel/PluginDependencyResolutionResult:conflicts", "kernel/PluginDependencyResolutionResult:dependencyGraph", "kernel/PluginDependencyResolutionResult:installationOrder", "kernel/PluginDependencyResolutionResult:resolved", "kernel/PluginDependencyResolutionResult:success", "kernel/PluginDependencyResolutionResult:warnings", - "kernel/PluginDynamicImport:enabled", - "kernel/PluginDynamicImport:mode", - "kernel/PluginDynamicImport:prefetch", - "kernel/PluginDynamicImport:preload", - "kernel/PluginDynamicImport:retry", - "kernel/PluginDynamicImport:timeout", - "kernel/PluginDynamicImport:webpackChunkName", "kernel/PluginEngines:platform", "kernel/PluginEngines:protocol", "kernel/PluginHealthCheck:autoRestart", @@ -559,22 +535,6 @@ "kernel/PluginHealthReport:metrics", "kernel/PluginHealthReport:status", "kernel/PluginHealthReport:timestamp", - "kernel/PluginHotReload:debounceMs", - "kernel/PluginHotReload:enabled", - "kernel/PluginHotReload:environment", - "kernel/PluginHotReload:hooks", - "kernel/PluginHotReload:ignorePatterns", - "kernel/PluginHotReload:preserveState", - "kernel/PluginHotReload:productionSafety", - "kernel/PluginHotReload:stateSerialization", - "kernel/PluginHotReload:strategy", - "kernel/PluginHotReload:watchPatterns", - "kernel/PluginInitialization:critical", - "kernel/PluginInitialization:healthCheckInterval", - "kernel/PluginInitialization:mode", - "kernel/PluginInitialization:priority", - "kernel/PluginInitialization:retry", - "kernel/PluginInitialization:timeout", "kernel/PluginInstallConfig:autoUpdate", "kernel/PluginInstallConfig:config", "kernel/PluginInstallConfig:options", @@ -587,16 +547,6 @@ "kernel/PluginInterface:name", "kernel/PluginInterface:stability", "kernel/PluginInterface:version", - "kernel/PluginLoadingConfig:caching", - "kernel/PluginLoadingConfig:codeSplitting", - "kernel/PluginLoadingConfig:dependencyResolution", - "kernel/PluginLoadingConfig:dynamicImport", - "kernel/PluginLoadingConfig:hotReload", - "kernel/PluginLoadingConfig:initialization", - "kernel/PluginLoadingConfig:monitoring", - "kernel/PluginLoadingConfig:preload", - "kernel/PluginLoadingConfig:sandboxing", - "kernel/PluginLoadingConfig:strategy", "kernel/PluginLoadingEvent:durationMs", "kernel/PluginLoadingEvent:error", "kernel/PluginLoadingEvent:metadata", @@ -617,12 +567,6 @@ "kernel/PluginMetadata:requiresServices", "kernel/PluginMetadata:signature", "kernel/PluginMetadata:version", - "kernel/PluginPerformanceMonitoring:budgets", - "kernel/PluginPerformanceMonitoring:enabled", - "kernel/PluginPerformanceMonitoring:metrics", - "kernel/PluginPerformanceMonitoring:onBudgetViolation", - "kernel/PluginPerformanceMonitoring:reportingInterval", - "kernel/PluginPerformanceMonitoring:samplingRate", "kernel/PluginPermission:actions", "kernel/PluginPermission:description", "kernel/PluginPermission:filter", @@ -638,10 +582,6 @@ "kernel/PluginPermissions:hooks", "kernel/PluginPermissions:network", "kernel/PluginPermissions:services", - "kernel/PluginPreloadConfig:conditions", - "kernel/PluginPreloadConfig:enabled", - "kernel/PluginPreloadConfig:priority", - "kernel/PluginPreloadConfig:resources", "kernel/PluginProvenance:artifacts", "kernel/PluginProvenance:attestations", "kernel/PluginProvenance:build", @@ -675,13 +615,6 @@ "kernel/PluginRegistryEntry:updatedAt", "kernel/PluginRegistryEntry:vendor", "kernel/PluginRegistryEntry:version", - "kernel/PluginSandboxing:allowedCapabilities", - "kernel/PluginSandboxing:enabled", - "kernel/PluginSandboxing:ipc", - "kernel/PluginSandboxing:isolationLevel", - "kernel/PluginSandboxing:permissions", - "kernel/PluginSandboxing:resourceQuotas", - "kernel/PluginSandboxing:scope", "kernel/PluginSearchFilters:category", "kernel/PluginSearchFilters:implementsProtocols", "kernel/PluginSearchFilters:limit", diff --git a/packages/spec/export-origins/kernel.json b/packages/spec/export-origins/kernel.json index 980a8c629e..452862f2fb 100644 --- a/packages/spec/export-origins/kernel.json +++ b/packages/spec/export-origins/kernel.json @@ -273,18 +273,12 @@ "PermissionScope": "src/kernel/plugin-security-advanced.zod.ts#PermissionScope (type)", "PermissionScopeSchema": "src/kernel/plugin-security-advanced.zod.ts#PermissionScopeSchema (const)", "PlatformCapabilityProvider": "src/kernel/platform-capabilities.ts#PlatformCapabilityProvider (interface)", - "PluginCaching": "src/kernel/plugin-loading.zod.ts#PluginCaching (type)", - "PluginCachingParsed": "src/kernel/plugin-loading.zod.ts#PluginCachingParsed (type)", - "PluginCachingSchema": "src/kernel/plugin-loading.zod.ts#PluginCachingSchema (const)", "PluginCapability": "src/kernel/plugin-capability.zod.ts#PluginCapability (type)", "PluginCapabilityManifest": "src/kernel/plugin-capability.zod.ts#PluginCapabilityManifest (type)", "PluginCapabilityManifestParsed": "src/kernel/plugin-capability.zod.ts#PluginCapabilityManifestParsed (type)", "PluginCapabilityManifestSchema": "src/kernel/plugin-capability.zod.ts#PluginCapabilityManifestSchema (const)", "PluginCapabilityParsed": "src/kernel/plugin-capability.zod.ts#PluginCapabilityParsed (type)", "PluginCapabilitySchema": "src/kernel/plugin-capability.zod.ts#PluginCapabilitySchema (const)", - "PluginCodeSplitting": "src/kernel/plugin-loading.zod.ts#PluginCodeSplitting (type)", - "PluginCodeSplittingParsed": "src/kernel/plugin-loading.zod.ts#PluginCodeSplittingParsed (type)", - "PluginCodeSplittingSchema": "src/kernel/plugin-loading.zod.ts#PluginCodeSplittingSchema (const)", "PluginCompatibilityMatrix": "src/kernel/plugin-versioning.zod.ts#PluginCompatibilityMatrix (type)", "PluginCompatibilityMatrixParsed": "src/kernel/plugin-versioning.zod.ts#PluginCompatibilityMatrixParsed (type)", "PluginCompatibilityMatrixSchema": "src/kernel/plugin-versioning.zod.ts#PluginCompatibilityMatrixSchema (const)", @@ -294,16 +288,10 @@ "PluginDefinition": "src/kernel/plugin.zod.ts#PluginDefinition (type)", "PluginDependency": "src/kernel/plugin-capability.zod.ts#PluginDependency (type)", "PluginDependencyParsed": "src/kernel/plugin-capability.zod.ts#PluginDependencyParsed (type)", - "PluginDependencyResolution": "src/kernel/plugin-loading.zod.ts#PluginDependencyResolution (type)", - "PluginDependencyResolutionParsed": "src/kernel/plugin-loading.zod.ts#PluginDependencyResolutionParsed (type)", "PluginDependencyResolutionResult": "src/kernel/plugin-versioning.zod.ts#PluginDependencyResolutionResult (type)", "PluginDependencyResolutionResultParsed": "src/kernel/plugin-versioning.zod.ts#PluginDependencyResolutionResultParsed (type)", "PluginDependencyResolutionResultSchema": "src/kernel/plugin-versioning.zod.ts#PluginDependencyResolutionResultSchema (const)", - "PluginDependencyResolutionSchema": "src/kernel/plugin-loading.zod.ts#PluginDependencyResolutionSchema (const)", "PluginDependencySchema": "src/kernel/plugin-capability.zod.ts#PluginDependencySchema (const)", - "PluginDynamicImport": "src/kernel/plugin-loading.zod.ts#PluginDynamicImport (type)", - "PluginDynamicImportParsed": "src/kernel/plugin-loading.zod.ts#PluginDynamicImportParsed (type)", - "PluginDynamicImportSchema": "src/kernel/plugin-loading.zod.ts#PluginDynamicImportSchema (const)", "PluginEngines": "src/kernel/manifest.zod.ts#PluginEngines (type)", "PluginEnginesSchema": "src/kernel/manifest.zod.ts#PluginEnginesSchema (const)", "PluginHealthCheck": "src/kernel/plugin-lifecycle-advanced.zod.ts#PluginHealthCheck (type)", @@ -313,12 +301,6 @@ "PluginHealthReportSchema": "src/kernel/plugin-lifecycle-advanced.zod.ts#PluginHealthReportSchema (const)", "PluginHealthStatus": "src/kernel/plugin-lifecycle-advanced.zod.ts#PluginHealthStatus (type)", "PluginHealthStatusSchema": "src/kernel/plugin-lifecycle-advanced.zod.ts#PluginHealthStatusSchema (const)", - "PluginHotReload": "src/kernel/plugin-loading.zod.ts#PluginHotReload (type)", - "PluginHotReloadParsed": "src/kernel/plugin-loading.zod.ts#PluginHotReloadParsed (type)", - "PluginHotReloadSchema": "src/kernel/plugin-loading.zod.ts#PluginHotReloadSchema (const)", - "PluginInitialization": "src/kernel/plugin-loading.zod.ts#PluginInitialization (type)", - "PluginInitializationParsed": "src/kernel/plugin-loading.zod.ts#PluginInitializationParsed (type)", - "PluginInitializationSchema": "src/kernel/plugin-loading.zod.ts#PluginInitializationSchema (const)", "PluginInstallConfig": "src/kernel/plugin-registry.zod.ts#PluginInstallConfig (type)", "PluginInstallConfigSchema": "src/kernel/plugin-registry.zod.ts#PluginInstallConfigSchema (const)", "PluginIntegrity": "src/kernel/manifest.zod.ts#PluginIntegrity (type)", @@ -326,23 +308,15 @@ "PluginInterface": "src/kernel/plugin-capability.zod.ts#PluginInterface (type)", "PluginInterfaceParsed": "src/kernel/plugin-capability.zod.ts#PluginInterfaceParsed (type)", "PluginInterfaceSchema": "src/kernel/plugin-capability.zod.ts#PluginInterfaceSchema (const)", - "PluginLoadingConfig": "src/kernel/plugin-loading.zod.ts#PluginLoadingConfig (type)", - "PluginLoadingConfigParsed": "src/kernel/plugin-loading.zod.ts#PluginLoadingConfigParsed (type)", - "PluginLoadingConfigSchema": "src/kernel/plugin-loading.zod.ts#PluginLoadingConfigSchema (const)", "PluginLoadingEvent": "src/kernel/plugin-loading.zod.ts#PluginLoadingEvent (type)", "PluginLoadingEventSchema": "src/kernel/plugin-loading.zod.ts#PluginLoadingEventSchema (const)", "PluginLoadingState": "src/kernel/plugin-loading.zod.ts#PluginLoadingState (type)", "PluginLoadingStateParsed": "src/kernel/plugin-loading.zod.ts#PluginLoadingStateParsed (type)", "PluginLoadingStateSchema": "src/kernel/plugin-loading.zod.ts#PluginLoadingStateSchema (const)", - "PluginLoadingStrategy": "src/kernel/plugin-loading.zod.ts#PluginLoadingStrategy (type)", - "PluginLoadingStrategySchema": "src/kernel/plugin-loading.zod.ts#PluginLoadingStrategySchema (const)", "PluginMetadata": "src/kernel/plugin-validator.zod.ts#PluginMetadata (type)", "PluginMetadataSchema": "src/kernel/plugin-validator.zod.ts#PluginMetadataSchema (const)", "PluginPackaging": "src/kernel/manifest.zod.ts#PluginPackaging (type)", "PluginPackagingSchema": "src/kernel/manifest.zod.ts#PluginPackagingSchema (const)", - "PluginPerformanceMonitoring": "src/kernel/plugin-loading.zod.ts#PluginPerformanceMonitoring (type)", - "PluginPerformanceMonitoringParsed": "src/kernel/plugin-loading.zod.ts#PluginPerformanceMonitoringParsed (type)", - "PluginPerformanceMonitoringSchema": "src/kernel/plugin-loading.zod.ts#PluginPerformanceMonitoringSchema (const)", "PluginPermission": "src/kernel/plugin-security-advanced.zod.ts#PluginPermission (type)", "PluginPermissionParsed": "src/kernel/plugin-security-advanced.zod.ts#PluginPermissionParsed (type)", "PluginPermissionSchema": "src/kernel/plugin-security-advanced.zod.ts#PluginPermissionSchema (const)", @@ -351,9 +325,6 @@ "PluginPermissionSetSchema": "src/kernel/plugin-security-advanced.zod.ts#PluginPermissionSetSchema (const)", "PluginPermissions": "src/kernel/manifest.zod.ts#PluginPermissions (type)", "PluginPermissionsSchema": "src/kernel/manifest.zod.ts#PluginPermissionsSchema (const)", - "PluginPreloadConfig": "src/kernel/plugin-loading.zod.ts#PluginPreloadConfig (type)", - "PluginPreloadConfigParsed": "src/kernel/plugin-loading.zod.ts#PluginPreloadConfigParsed (type)", - "PluginPreloadConfigSchema": "src/kernel/plugin-loading.zod.ts#PluginPreloadConfigSchema (const)", "PluginProvenance": "src/kernel/plugin-security.zod.ts#PluginProvenance (type)", "PluginProvenanceParsed": "src/kernel/plugin-security.zod.ts#PluginProvenanceParsed (type)", "PluginProvenanceSchema": "src/kernel/plugin-security.zod.ts#PluginProvenanceSchema (const)", @@ -365,9 +336,6 @@ "PluginRegistryEntrySchema": "src/kernel/plugin-registry.zod.ts#PluginRegistryEntrySchema (const)", "PluginRuntime": "src/kernel/manifest.zod.ts#PluginRuntime (type)", "PluginRuntimeSchema": "src/kernel/manifest.zod.ts#PluginRuntimeSchema (const)", - "PluginSandboxing": "src/kernel/plugin-loading.zod.ts#PluginSandboxing (type)", - "PluginSandboxingParsed": "src/kernel/plugin-loading.zod.ts#PluginSandboxingParsed (type)", - "PluginSandboxingSchema": "src/kernel/plugin-loading.zod.ts#PluginSandboxingSchema (const)", "PluginSchema": "src/kernel/plugin.zod.ts#PluginSchema (const)", "PluginSearchFilters": "src/kernel/plugin-registry.zod.ts#PluginSearchFilters (type)", "PluginSearchFiltersSchema": "src/kernel/plugin-registry.zod.ts#PluginSearchFiltersSchema (const)", diff --git a/packages/spec/json-schema.manifest/kernel.json b/packages/spec/json-schema.manifest/kernel.json index 6067cb1476..f41f858424 100644 --- a/packages/spec/json-schema.manifest/kernel.json +++ b/packages/spec/json-schema.manifest/kernel.json @@ -100,40 +100,29 @@ "kernel/PermissionAction", "kernel/PermissionScope", "kernel/Plugin", - "kernel/PluginCaching", "kernel/PluginCapability", "kernel/PluginCapabilityManifest", - "kernel/PluginCodeSplitting", "kernel/PluginCompatibilityMatrix", "kernel/PluginDependency", - "kernel/PluginDependencyResolution", "kernel/PluginDependencyResolutionResult", - "kernel/PluginDynamicImport", "kernel/PluginEngines", "kernel/PluginHealthCheck", "kernel/PluginHealthReport", "kernel/PluginHealthStatus", - "kernel/PluginHotReload", - "kernel/PluginInitialization", "kernel/PluginInstallConfig", "kernel/PluginIntegrity", "kernel/PluginInterface", - "kernel/PluginLoadingConfig", "kernel/PluginLoadingEvent", "kernel/PluginLoadingState", - "kernel/PluginLoadingStrategy", "kernel/PluginMetadata", "kernel/PluginPackaging", - "kernel/PluginPerformanceMonitoring", "kernel/PluginPermission", "kernel/PluginPermissionSet", "kernel/PluginPermissions", - "kernel/PluginPreloadConfig", "kernel/PluginProvenance", "kernel/PluginQualityMetrics", "kernel/PluginRegistryEntry", "kernel/PluginRuntime", - "kernel/PluginSandboxing", "kernel/PluginSearchFilters", "kernel/PluginSecurityManifest", "kernel/PluginStartupResult", diff --git a/packages/spec/spec-changes.json b/packages/spec/spec-changes.json index 4c415ca978..3c0639446e 100644 --- a/packages/spec/spec-changes.json +++ b/packages/spec/spec-changes.json @@ -752,6 +752,13 @@ "toMajor": 17, "rationale": "One capability, both halves, never half-deleted (maintainer ruling 2026-08-07, Option A, ruled jointly with #6363). `cursor` was declared on the request and on the response and honoured on neither: the dispatcher domain reads `read` / `type` / `limit` and nothing else, and no emit site has ever written the response key. It was worse than inert because it had a shipped PRODUCER — the SDK appended it to the query string — so a caller paginating by the published contract looped on page 1 forever, with no error and no 400. Measured over a real boot with 60 unread before the removal: page2 === page1, both parsing green against the response schema, which is why no conformance gate could see it. This is `data.query.cursor` (#4286, `query-cursor-retired`) one layer up, with the same verdict for the same reason, down to deleting the SDK producer alongside the key. A first-class inbox cursor, if one is ever designed, will be a response-minted opaque token — a different API — so keeping this one preserved a wrong design rather than a roadmap. The `limit` default goes with it because the FICTION WAS THE MECHANISM, not the number: no request path parses a query string through this schema (#3899 wired the catalog's requestSchema to the real entry for BODIES only), so `.default(20)` never stamped anything onto anything, and the server has always applied its own 50. Re-spelling 20 as 50 — the other arm the ruling allowed — would have kept a declaration that does not execute and merely made it coincide with the implementation until someone moved the clamp; `.optional()` plus prose is true about both the schema and the server. No constraint (`.int()` / `.max(200)`) is declared either, because the service CLAMPS an out-of-range limit rather than refusing it, and declaring a rejection the wire does not perform is the same defect mirrored. Route 2, and the split is worth stating exactly because the two halves of the bookkeeping go different ways. There IS a tombstone: both schemas are non-strict, so a bare deletion would have made Zod SILENTLY STRIP whatever a caller kept sending — a clean parse and a parameter that never takes effect, which is this issue's own defect re-created one layer down (#3733, ADR-0104). So `cursor` is `retiredKey()` on both halves, typed `never` for tsc and raising the prescription at any parse, and both keys are registered in RETIRED_KEYS_BY_MAJOR[17]. There is NO D2 conversion: a conversion rewrites an authored source or a stored `sys_metadata` row, and these two shapes are HTTP-only — nobody authors a `ListNotificationsRequest` and nothing persists one. Request AND response shapes: two semantic TODOs for API callers, no stack conversion — the same disposition `BatchOptions.validateOnly` (#4052) and the `AnalyticsQueryRequest` envelope keys already take in this major. The `limit` default is declared separately and mechanically, in DEFAULT_CHANGES_BY_MAJOR[17] (#4666), whose `from`/`to` fingerprints are re-derived on every build. ADR-0049 / ADR-0078, #6361." }, + { + "surface": "manifest.loading (the whole block: strategy / preload / codeSplitting / dynamicImport / initialization / dependencyResolution / hotReload / caching / sandboxing / monitoring)", + "replacement": "nothing to re-declare — delete the key. Plugins are composed at boot: `defineStack` registers them and the kernel runs `init` then `start` in an order topologically resolved from each composed plugin's own `dependencies` / `optionalDependencies` (`resolvePluginOrder` in `packages/core/src/plugin-order.ts`). For the isolation `loading.sandboxing` appeared to configure, use the plugin trust tier (`manifest.runtime`, ADR-0025 §3.6) and the manifest permission declarations, which are the surfaces the platform actually enforces", + "migrationId": "plugin-manifest-loading-retired", + "toMajor": 17, + "rationale": "ADR-0049 enforce-or-remove; maintainer ruling 2026-08-04 on #4914. The block declared a complete plugin loading policy and NOTHING read it. A bare-name scan of all three repos — objectstack, cloud (measured 2026-08-09) and objectui (measured at pickup), each with a control probe proving the scan saw the tree — put every hit inside `packages/spec` itself: this module's own declaration, its own unit tests, the `Manifest.loading` embed and the generated artifacts. `manifest.loading.*` had zero readers in `packages/core`, `packages/runtime` and `packages/metadata`. So the key parsed, entered the manifest, and changed nothing — #3950, at the scale of a whole block. What made it outrank ordinary inert-key cleanup is `sandboxing`: it declared process / vm / iframe / web-worker isolation, IPC transports and an `allowedServices` ACL, so an AI author (ADR-0033) reading that vocabulary concluded the platform isolates plugins, wrote the config, and received a clean parse and zero isolation. An inert security control is worse than an absent one because it is believed. Hot reload was additionally a TWO-SOURCE defect: the docs pointed at this dead `PluginHotReloadSchema` while the only implementation body, `HotReloadManager` (`packages/core/src/hot-reload.ts`), reads a different vocabulary — `HotReloadConfigSchema` in `plugin-lifecycle-advanced.zod.ts`. Ruling §2 converges on the surviving side: that schema is KEPT as the starting point for a future enforce decision (it has an implementation body but no runtime composes it yet), and enforcing it is deliberately a separate decision, not this retirement. Why D3 semantic and not a D2 conversion: the chain walks a normalized STACK and `applyConversionsToStoredItem` maps a metadata type onto one of its collections. A package manifest is neither — `PLURAL_TO_SINGULAR` has no `packages` / `plugins` entry, so a manifest is not a stack collection member and a stored manifest row passes that seam through unchanged. A conversion would be a transform with no seam that ever runs." + }, { "surface": "PUT /api/v1/meta/api/{name} (runtime-authored `api` endpoints, draft and active alike)", "replacement": "Declare the endpoint as a stack artifact (`**/*.api.ts`, or `defineStack({ apis })`) and ship it through `publishPackage`", @@ -1576,6 +1583,13 @@ "toMajor": 17, "rationale": "One capability, both halves, never half-deleted (maintainer ruling 2026-08-07, Option A, ruled jointly with #6363). `cursor` was declared on the request and on the response and honoured on neither: the dispatcher domain reads `read` / `type` / `limit` and nothing else, and no emit site has ever written the response key. It was worse than inert because it had a shipped PRODUCER — the SDK appended it to the query string — so a caller paginating by the published contract looped on page 1 forever, with no error and no 400. Measured over a real boot with 60 unread before the removal: page2 === page1, both parsing green against the response schema, which is why no conformance gate could see it. This is `data.query.cursor` (#4286, `query-cursor-retired`) one layer up, with the same verdict for the same reason, down to deleting the SDK producer alongside the key. A first-class inbox cursor, if one is ever designed, will be a response-minted opaque token — a different API — so keeping this one preserved a wrong design rather than a roadmap. The `limit` default goes with it because the FICTION WAS THE MECHANISM, not the number: no request path parses a query string through this schema (#3899 wired the catalog's requestSchema to the real entry for BODIES only), so `.default(20)` never stamped anything onto anything, and the server has always applied its own 50. Re-spelling 20 as 50 — the other arm the ruling allowed — would have kept a declaration that does not execute and merely made it coincide with the implementation until someone moved the clamp; `.optional()` plus prose is true about both the schema and the server. No constraint (`.int()` / `.max(200)`) is declared either, because the service CLAMPS an out-of-range limit rather than refusing it, and declaring a rejection the wire does not perform is the same defect mirrored. Route 2, and the split is worth stating exactly because the two halves of the bookkeeping go different ways. There IS a tombstone: both schemas are non-strict, so a bare deletion would have made Zod SILENTLY STRIP whatever a caller kept sending — a clean parse and a parameter that never takes effect, which is this issue's own defect re-created one layer down (#3733, ADR-0104). So `cursor` is `retiredKey()` on both halves, typed `never` for tsc and raising the prescription at any parse, and both keys are registered in RETIRED_KEYS_BY_MAJOR[17]. There is NO D2 conversion: a conversion rewrites an authored source or a stored `sys_metadata` row, and these two shapes are HTTP-only — nobody authors a `ListNotificationsRequest` and nothing persists one. Request AND response shapes: two semantic TODOs for API callers, no stack conversion — the same disposition `BatchOptions.validateOnly` (#4052) and the `AnalyticsQueryRequest` envelope keys already take in this major. The `limit` default is declared separately and mechanically, in DEFAULT_CHANGES_BY_MAJOR[17] (#4666), whose `from`/`to` fingerprints are re-derived on every build. ADR-0049 / ADR-0078, #6361." }, + { + "surface": "manifest.loading (the whole block: strategy / preload / codeSplitting / dynamicImport / initialization / dependencyResolution / hotReload / caching / sandboxing / monitoring)", + "replacement": "nothing to re-declare — delete the key. Plugins are composed at boot: `defineStack` registers them and the kernel runs `init` then `start` in an order topologically resolved from each composed plugin's own `dependencies` / `optionalDependencies` (`resolvePluginOrder` in `packages/core/src/plugin-order.ts`). For the isolation `loading.sandboxing` appeared to configure, use the plugin trust tier (`manifest.runtime`, ADR-0025 §3.6) and the manifest permission declarations, which are the surfaces the platform actually enforces", + "migrationId": "plugin-manifest-loading-retired", + "toMajor": 17, + "rationale": "ADR-0049 enforce-or-remove; maintainer ruling 2026-08-04 on #4914. The block declared a complete plugin loading policy and NOTHING read it. A bare-name scan of all three repos — objectstack, cloud (measured 2026-08-09) and objectui (measured at pickup), each with a control probe proving the scan saw the tree — put every hit inside `packages/spec` itself: this module's own declaration, its own unit tests, the `Manifest.loading` embed and the generated artifacts. `manifest.loading.*` had zero readers in `packages/core`, `packages/runtime` and `packages/metadata`. So the key parsed, entered the manifest, and changed nothing — #3950, at the scale of a whole block. What made it outrank ordinary inert-key cleanup is `sandboxing`: it declared process / vm / iframe / web-worker isolation, IPC transports and an `allowedServices` ACL, so an AI author (ADR-0033) reading that vocabulary concluded the platform isolates plugins, wrote the config, and received a clean parse and zero isolation. An inert security control is worse than an absent one because it is believed. Hot reload was additionally a TWO-SOURCE defect: the docs pointed at this dead `PluginHotReloadSchema` while the only implementation body, `HotReloadManager` (`packages/core/src/hot-reload.ts`), reads a different vocabulary — `HotReloadConfigSchema` in `plugin-lifecycle-advanced.zod.ts`. Ruling §2 converges on the surviving side: that schema is KEPT as the starting point for a future enforce decision (it has an implementation body but no runtime composes it yet), and enforcing it is deliberately a separate decision, not this retirement. Why D3 semantic and not a D2 conversion: the chain walks a normalized STACK and `applyConversionsToStoredItem` maps a metadata type onto one of its collections. A package manifest is neither — `PLURAL_TO_SINGULAR` has no `packages` / `plugins` entry, so a manifest is not a stack collection member and a stored manifest row passes that seam through unchanged. A conversion would be a transform with no seam that ever runs." + }, { "surface": "PUT /api/v1/meta/api/{name} (runtime-authored `api` endpoints, draft and active alike)", "replacement": "Declare the endpoint as a stack artifact (`**/*.api.ts`, or `defineStack({ apis })`) and ship it through `publishPackage`", diff --git a/packages/spec/src/kernel/index.ts b/packages/spec/src/kernel/index.ts index a3dd53187e..b65cc29165 100644 --- a/packages/spec/src/kernel/index.ts +++ b/packages/spec/src/kernel/index.ts @@ -41,6 +41,38 @@ export * from './package-upgrade.zod'; export * from './plugin-capability.zod'; export * from './plugin-lifecycle-advanced.zod'; export * from './plugin-loading.zod'; +// plugin-loading.zod's CONFIGURATION half (PluginLoadingConfigSchema + +// PluginLoadingStrategySchema / PluginPreloadConfigSchema / +// PluginCodeSplittingSchema / PluginDynamicImportSchema / +// PluginInitializationSchema / PluginDependencyResolutionSchema / +// PluginHotReloadSchema / PluginCachingSchema / PluginSandboxingSchema / +// PluginPerformanceMonitoringSchema + every type alias) was REMOVED per ADR-0049 +// enforce-or-remove (#4914), together with the `Manifest.loading` key that was +// its only carrier — that key is now a `retiredKey()` tombstone in +// `manifest.zod.ts`, because `ManifestSchema` is not `.strict()` and a plain +// deletion would strip it in silence. +// +// The block declared a full loading policy — lazy/eager strategy, preloading, +// code splitting, dynamic import, initialization, dependency resolution, hot +// reload, caching, sandboxing and performance budgets — and NOTHING read it. A +// bare-name scan of objectstack, cloud and objectui (each with a control probe) +// put every hit inside `packages/spec` itself. `manifest.loading.*` had zero +// readers in `packages/core`, `packages/runtime` and `packages/metadata`. +// +// `sandboxing` is why this outranked ordinary inert-key cleanup: it declared +// process/vm/iframe/web-worker isolation, IPC transports and an `allowedServices` +// ACL, so an AI author (ADR-0033) reading it concluded the platform sandboxes +// plugins, wrote the config, and got a clean parse and zero isolation. An inert +// security control is worse than an absent one because it is believed. +// +// Hot reload converges on ONE vocabulary (ruling §2): the surviving side is +// `HotReloadConfigSchema` in `plugin-lifecycle-advanced.zod.ts`, which is the one +// `HotReloadManager` (`packages/core/src/hot-reload.ts`) reads. It has an +// implementation body but no runtime composes it yet — kept as the starting +// point for a future enforce decision, which is deliberately NOT this change. +// +// What survives here is only the observational half (`PluginLoadingEventSchema`, +// `PluginLoadingStateSchema`). // plugin-runtime.zod (DynamicLoadRequestSchema / DynamicUnloadRequestSchema / // DynamicPluginResultSchema / PluginSourceSchema / DynamicPluginOperationSchema // + every type alias) was REMOVED per ADR-0049 enforce-or-remove (#4834). The diff --git a/packages/spec/src/kernel/manifest.zod.ts b/packages/spec/src/kernel/manifest.zod.ts index 30358b46d5..bbfe9d9be4 100644 --- a/packages/spec/src/kernel/manifest.zod.ts +++ b/packages/spec/src/kernel/manifest.zod.ts @@ -2,8 +2,8 @@ import { z } from 'zod'; import { PluginCapabilityManifestSchema } from './plugin-capability.zod'; -import { PluginLoadingConfigSchema } from './plugin-loading.zod'; import { CORE_PLUGIN_TYPES } from './plugin.zod'; +import { retiredKey } from '../shared/retired-key'; import { SeedSchema } from '../data/seed.zod'; import { NavigationContributionSchema } from '../ui/app.zod'; @@ -502,12 +502,33 @@ export const ManifestSchema = z.object({ .describe('Navigation items this package contributes into apps owned by other packages'), /** - * Plugin Loading Configuration. - * Configures how the plugin is loaded, initialized, and managed at runtime. - * Includes strategies for lazy loading, code splitting, caching, and hot reload. + * REMOVED in v17 (#4914, ADR-0049 enforce-or-remove). + * + * `loading` carried the whole `PluginLoadingConfig` block — `strategy`, + * `preload`, `codeSplitting`, `dynamicImport`, `initialization`, + * `dependencyResolution`, `hotReload`, `caching`, `sandboxing`, `monitoring` + * — and NOTHING read any of it. It parsed, it entered the manifest, and it + * changed nothing. Tombstoned rather than deleted because `ManifestSchema` is + * not `.strict()`: a plain deletion would silently strip the key, replacing an + * inert declaration with an invisible one. + * + * See `plugin-loading.zod.ts` for the full record, including why `sandboxing` + * made this a security concern and not merely tidying. */ - loading: PluginLoadingConfigSchema.optional() - .describe('Plugin loading and runtime behavior configuration'), + loading: retiredKey( + '`manifest.loading` was removed in @objectstack/spec 17.0.0 (#4914, ADR-0049 ' + + 'enforce-or-remove) — the entire block (`strategy`, `preload`, `codeSplitting`, ' + + '`dynamicImport`, `initialization`, `dependencyResolution`, `hotReload`, `caching`, ' + + '`sandboxing`, `monitoring`) had no runtime reader in any repo, so authoring it ' + + 'configured nothing. Delete the key. Plugins are composed at boot — `defineStack` ' + + 'registers them and the kernel runs `init` then `start` in an order topologically ' + + "resolved from each composed plugin's own `dependencies` / `optionalDependencies` " + + '(`resolvePluginOrder`); the set is fixed until the process restarts. ' + + '⚠️ `loading.sandboxing` in particular never isolated anything: it did not run ' + + 'plugins in a process, vm, iframe or web-worker, and `allowedServices` gated no ' + + 'call. If you were relying on it for isolation, you had none — use the plugin trust ' + + 'tier (`manifest.runtime`) and the permission declarations, which are enforced.', + ), /** * Platform Compatibility Requirements. diff --git a/packages/spec/src/kernel/plugin-loading-retirement.test.ts b/packages/spec/src/kernel/plugin-loading-retirement.test.ts new file mode 100644 index 0000000000..849cafc5a3 --- /dev/null +++ b/packages/spec/src/kernel/plugin-loading-retirement.test.ts @@ -0,0 +1,139 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +import { describe, it, expect } from 'vitest'; +import { ManifestSchema } from './manifest.zod'; + +// ─── [#4914] `manifest.loading` and the PluginLoadingConfig block are REMOVED ── +// +// ADR-0049 enforce-or-remove, maintainer ruling 2026-08-04, ruled REMOVE. The +// block declared a complete plugin loading policy — strategy, preload, code +// splitting, dynamic import, initialization, dependency resolution, hot reload, +// caching, sandboxing and performance budgets — and nothing read any of it: a +// bare-name scan of objectstack, cloud and objectui (each with a control probe) +// put every reference inside `packages/spec` itself. +// +// `sandboxing` is what made it a security concern rather than tidying: it +// declared process / vm / iframe / web-worker isolation, IPC transports and an +// `allowedServices` ACL, so an author (ADR-0033: very often an AI) read the +// vocabulary as proof the platform isolates plugins, wrote the config, and got a +// clean parse and zero isolation. +// +// Route: `retiredKey()` tombstone, NOT plain deletion. `ManifestSchema` is not +// `.strict()`, so deleting the key would make zod strip it in silence — +// replacing an inert declaration with an invisible one (the #3726 / #3733 shape, +// ADR-0104). The tombstone is audible in two channels: `tsc` (the key's input +// type is `never`) and the parse below. +// +// ⚠️ On the assertion set. The dispatch asked for the ADR-0112 rejection +// envelope (`code` + `status`) as the floor for a rejection-class case. That +// envelope belongs to the API error surface — `ApiErrorSchema`, the +// `ERROR_CODE_LEDGER` vocabulary and an HTTP status — and a schema tombstone +// does not raise one: it raises a `ZodError`, whose issues carry `code` and +// `path` but have no `status` field at all (measured, not assumed). Asserting a +// `status` here would be a fabrication that reads as verification, so these +// pins assert the strongest set this surface really has — refusal, the issue +// `code`, the `path` naming WHICH key was refused, and the prescription text +// (#5240: where the wording is the contract, pin the wording). That set still +// satisfies what #6142 is actually after: it goes red if the refusal moves to +// the wrong key, loses its code, or stops carrying the fix — none of which a +// bare `toThrow()` can see. +describe('[#4914] manifest.loading retirement', () => { + /** A manifest that is valid except for whatever the individual test adds. */ + const baseManifest = { + id: 'com.example.plugin', + namespace: 'example', + version: '1.0.0', + type: 'app', + name: 'Example Plugin', + } as const; + + it('REJECTS an authored `loading` block, naming the key and carrying the fix', () => { + const result = ManifestSchema.safeParse({ + ...baseManifest, + loading: { strategy: 'lazy' }, + }); + + expect(result.success).toBe(false); + if (result.success) return; // narrowing; the assertion above already failed + + const issue = result.error.issues.find((i) => i.path[0] === 'loading'); + expect(issue, 'the refusal must name `loading`').toBeDefined(); + // The machine-readable half of the envelope this surface actually has. + expect(issue!.code).toBe('invalid_type'); + expect(issue!.path).toEqual(['loading']); + // The prescription itself — this string IS the migration doc for whoever + // hits it, so it is contract, not commentary. + expect(issue!.message).toMatch(/`manifest\.loading`.*removed.*17\.0\.0.*#4914/s); + expect(issue!.message).toMatch(/Delete the key/s); + }); + + it('REJECTS the sandboxing block specifically, since that is the dangerous one', () => { + const result = ManifestSchema.safeParse({ + ...baseManifest, + loading: { sandboxing: { enabled: true, isolationLevel: 'process' } }, + }); + + expect(result.success).toBe(false); + if (result.success) return; + + const issue = result.error.issues.find((i) => i.path[0] === 'loading'); + expect(issue!.code).toBe('invalid_type'); + // The prescription must say plainly that no isolation was ever applied — + // an author who believed otherwise has a security decision to revisit, and + // silence here is what made the inert key dangerous in the first place. + expect(issue!.message).toMatch(/never isolated anything/s); + expect(issue!.message).toMatch(/manifest\.runtime/s); + }); + + it('REJECTS hotReload too, and points at the surviving vocabulary', () => { + const result = ManifestSchema.safeParse({ + ...baseManifest, + loading: { hotReload: { enabled: true, strategy: 'state-preserve' } }, + }); + + expect(result.success).toBe(false); + if (result.success) return; + expect(result.error.issues.find((i) => i.path[0] === 'loading')!.code).toBe('invalid_type'); + }); + + it('parses cleanly once the key is deleted, and grows no `loading` property', () => { + const parsed = ManifestSchema.parse({ ...baseManifest }); + expect(parsed.id).toBe('com.example.plugin'); + // The non-strict strip path: absence must stay absence. If the tombstone + // were ever replaced by a plain deletion, an authored `loading` would be + // stripped here in silence — this pin plus the rejection above are what + // make that regression loud. + expect(parsed).not.toHaveProperty('loading'); + }); + + it('does not export the retired loading-config schemas from ./kernel', async () => { + const kernel = await import('./index'); + const retired = [ + 'PluginLoadingConfigSchema', + 'PluginLoadingStrategySchema', + 'PluginPreloadConfigSchema', + 'PluginCodeSplittingSchema', + 'PluginDynamicImportSchema', + 'PluginInitializationSchema', + 'PluginDependencyResolutionSchema', + 'PluginHotReloadSchema', + 'PluginCachingSchema', + 'PluginSandboxingSchema', + 'PluginPerformanceMonitoringSchema', + ]; + for (const name of retired) { + expect(kernel, `${name} must not be exported after #4914`).not.toHaveProperty(name); + } + + // Anti-vacuity: this pin is only meaningful if the barrel really resolved + // and still exports the neighbours that SURVIVE the retirement — the two + // observational shapes from the same module, and the hot-reload vocabulary + // the ruling deliberately kept (§2: `HotReloadConfigSchema` has an + // implementation body, `HotReloadManager`, and is the starting point for a + // FUTURE enforce decision — this change must not remove it). + expect(kernel).toHaveProperty('PluginLoadingEventSchema'); + expect(kernel).toHaveProperty('PluginLoadingStateSchema'); + expect(kernel).toHaveProperty('HotReloadConfigSchema'); + expect(kernel).toHaveProperty('ManifestSchema'); + }); +}); diff --git a/packages/spec/src/kernel/plugin-loading.test.ts b/packages/spec/src/kernel/plugin-loading.test.ts index 31e937c958..48817593b1 100644 --- a/packages/spec/src/kernel/plugin-loading.test.ts +++ b/packages/spec/src/kernel/plugin-loading.test.ts @@ -1,448 +1,20 @@ import { describe, it, expect } from 'vitest'; import { - PluginLoadingStrategySchema, - PluginLoadingConfigSchema, - PluginPreloadConfigSchema, - PluginCodeSplittingSchema, - PluginDynamicImportSchema, - PluginInitializationSchema, - PluginDependencyResolutionSchema, - PluginHotReloadSchema, - PluginCachingSchema, - PluginSandboxingSchema, - PluginPerformanceMonitoringSchema, PluginLoadingEventSchema, PluginLoadingStateSchema, } from './plugin-loading.zod'; +// The PluginLoadingConfig block's tests (PluginLoadingStrategySchema, +// PluginPreloadConfigSchema, PluginCodeSplittingSchema, +// PluginDynamicImportSchema, PluginInitializationSchema, +// PluginDependencyResolutionSchema, PluginHotReloadSchema, +// PluginCachingSchema, PluginSandboxingSchema, +// PluginPerformanceMonitoringSchema, PluginLoadingConfigSchema) and the +// "Integration scenarios" that composed them were removed with those schemas in +// #4914 (ADR-0049 enforce-or-remove). The retirement itself is pinned in +// `plugin-loading-retirement.test.ts`. What remains here is the module's +// surviving observational half. describe('Plugin Loading Protocol', () => { - describe('PluginLoadingStrategySchema', () => { - it('should accept valid loading strategies', () => { - expect(PluginLoadingStrategySchema.parse('eager')).toBe('eager'); - expect(PluginLoadingStrategySchema.parse('lazy')).toBe('lazy'); - expect(PluginLoadingStrategySchema.parse('parallel')).toBe('parallel'); - expect(PluginLoadingStrategySchema.parse('deferred')).toBe('deferred'); - expect(PluginLoadingStrategySchema.parse('on-demand')).toBe('on-demand'); - }); - - it('should reject invalid strategies', () => { - expect(() => PluginLoadingStrategySchema.parse('invalid')).toThrow(); - }); - }); - - describe('PluginPreloadConfigSchema', () => { - it('should accept minimal preload config', () => { - const config = { - enabled: true, - }; - const result = PluginPreloadConfigSchema.parse(config); - expect(result.enabled).toBe(true); - expect(result.priority).toBe(100); // default - }); - - it('should accept full preload config', () => { - const config = { - enabled: true, - priority: 50, - resources: ['metadata', 'code', 'services'], - conditions: { - routes: ['/dashboard', '/settings'], - roles: ['admin', 'manager'], - deviceType: ['desktop'], - minNetworkSpeed: '4g', - }, - }; - const result = PluginPreloadConfigSchema.parse(config); - expect(result.enabled).toBe(true); - expect(result.priority).toBe(50); - expect(result.resources).toHaveLength(3); - expect(result.conditions?.routes).toHaveLength(2); - }); - - it('should apply defaults', () => { - const result = PluginPreloadConfigSchema.parse({}); - expect(result.enabled).toBe(false); - expect(result.priority).toBe(100); - }); - }); - - describe('PluginCodeSplittingSchema', () => { - it('should accept code splitting config', () => { - const config = { - enabled: true, - strategy: 'feature', - chunkNaming: 'hashed', - maxChunkSize: 500, - sharedDependencies: { - enabled: true, - minChunks: 3, - }, - }; - const result = PluginCodeSplittingSchema.parse(config); - expect(result.enabled).toBe(true); - expect(result.strategy).toBe('feature'); - expect(result.maxChunkSize).toBe(500); - }); - - it('should apply defaults', () => { - const result = PluginCodeSplittingSchema.parse({}); - expect(result.enabled).toBe(true); - expect(result.strategy).toBe('feature'); - expect(result.chunkNaming).toBe('hashed'); - }); - }); - - describe('PluginDynamicImportSchema', () => { - it('should accept dynamic import config', () => { - const config = { - enabled: true, - mode: 'async', - prefetch: true, - preload: false, - timeout: 15000, - retry: { - enabled: true, - maxAttempts: 5, - backoffMs: 2000, - }, - }; - const result = PluginDynamicImportSchema.parse(config); - expect(result.enabled).toBe(true); - expect(result.mode).toBe('async'); - expect(result.retry?.maxAttempts).toBe(5); - }); - - it('should apply defaults', () => { - const result = PluginDynamicImportSchema.parse({}); - expect(result.enabled).toBe(true); - expect(result.mode).toBe('async'); - expect(result.timeout).toBe(30000); - }); - }); - - describe('PluginInitializationSchema', () => { - it('should accept initialization config', () => { - const config = { - mode: 'parallel', - timeout: 60000, - priority: 10, - critical: true, - retry: { - enabled: true, - maxAttempts: 3, - backoffMs: 1000, - }, - healthCheckInterval: 30000, - }; - const result = PluginInitializationSchema.parse(config); - expect(result.mode).toBe('parallel'); - expect(result.critical).toBe(true); - expect(result.healthCheckInterval).toBe(30000); - }); - - it('should apply defaults', () => { - const result = PluginInitializationSchema.parse({}); - expect(result.mode).toBe('async'); - expect(result.timeout).toBe(30000); - expect(result.priority).toBe(100); - expect(result.critical).toBe(false); - }); - }); - - describe('PluginDependencyResolutionSchema', () => { - it('should accept dependency resolution config', () => { - const config = { - strategy: 'compatible', - peerDependencies: { - resolve: true, - onMissing: 'warn', - onMismatch: 'error', - }, - optionalDependencies: { - load: true, - onFailure: 'ignore', - }, - conflictResolution: 'latest', - circularDependencies: 'warn', - }; - const result = PluginDependencyResolutionSchema.parse(config); - expect(result.strategy).toBe('compatible'); - expect(result.peerDependencies?.onMismatch).toBe('error'); - expect(result.conflictResolution).toBe('latest'); - }); - - it('should apply defaults', () => { - const result = PluginDependencyResolutionSchema.parse({}); - expect(result.strategy).toBe('compatible'); - expect(result.conflictResolution).toBe('latest'); - expect(result.circularDependencies).toBe('warn'); - }); - }); - - describe('PluginHotReloadSchema', () => { - it('should accept hot reload config', () => { - const config = { - enabled: true, - strategy: 'partial', - watchPatterns: ['src/**/*.ts', 'src/**/*.tsx'], - ignorePatterns: ['**/*.test.ts', '**/*.spec.ts'], - debounceMs: 500, - preserveState: true, - stateSerialization: { - enabled: true, - handler: './state-handler.js', - }, - hooks: { - beforeReload: 'onBeforeReload', - afterReload: 'onAfterReload', - onError: 'onReloadError', - }, - }; - const result = PluginHotReloadSchema.parse(config); - expect(result.enabled).toBe(true); - expect(result.strategy).toBe('partial'); - expect(result.preserveState).toBe(true); - }); - - it('should apply defaults', () => { - const result = PluginHotReloadSchema.parse({}); - expect(result.enabled).toBe(false); - expect(result.strategy).toBe('full'); - expect(result.debounceMs).toBe(300); - expect(result.environment).toBe('development'); - }); - - it('should accept production environment with safety config', () => { - const config = { - enabled: true, - environment: 'production' as const, - strategy: 'state-preserve' as const, - productionSafety: { - healthValidation: true, - rollbackOnFailure: true, - healthTimeout: 60000, - drainConnections: true, - drainTimeout: 30000, - maxConcurrentReloads: 2, - minReloadInterval: 10000, - }, - }; - const result = PluginHotReloadSchema.parse(config); - expect(result.environment).toBe('production'); - expect(result.productionSafety?.healthValidation).toBe(true); - expect(result.productionSafety?.rollbackOnFailure).toBe(true); - expect(result.productionSafety?.maxConcurrentReloads).toBe(2); - }); - - it('should apply production safety defaults', () => { - const config = { - enabled: true, - environment: 'production' as const, - productionSafety: {}, - }; - const result = PluginHotReloadSchema.parse(config); - expect(result.productionSafety?.healthValidation).toBe(true); - expect(result.productionSafety?.rollbackOnFailure).toBe(true); - expect(result.productionSafety?.drainConnections).toBe(true); - expect(result.productionSafety?.maxConcurrentReloads).toBe(1); - expect(result.productionSafety?.minReloadInterval).toBe(5000); - }); - - it('should accept all environment values', () => { - const envs = ['development', 'staging', 'production']; - envs.forEach((env) => { - const result = PluginHotReloadSchema.parse({ environment: env }); - expect(result.environment).toBe(env); - }); - }); - }); - - describe('PluginCachingSchema', () => { - it('should accept caching config', () => { - const config = { - enabled: true, - storage: 'hybrid', - keyStrategy: 'hash', - ttl: 3600, - maxSize: 100, - invalidateOn: ['version-change', 'dependency-change'], - compression: { - enabled: true, - algorithm: 'brotli', - }, - }; - const result = PluginCachingSchema.parse(config); - expect(result.enabled).toBe(true); - expect(result.storage).toBe('hybrid'); - expect(result.compression?.algorithm).toBe('brotli'); - }); - - it('should apply defaults', () => { - const result = PluginCachingSchema.parse({}); - expect(result.enabled).toBe(true); - expect(result.storage).toBe('memory'); - expect(result.keyStrategy).toBe('version'); - }); - }); - - describe('PluginSandboxingSchema', () => { - it('should accept sandboxing config', () => { - const config = { - enabled: true, - isolationLevel: 'process', - allowedCapabilities: ['com.objectstack.protocol.storage.v1'], - resourceQuotas: { - maxMemoryMB: 512, - maxCpuTimeMs: 5000, - maxFileDescriptors: 100, - maxNetworkKBps: 1024, - }, - permissions: { - allowedAPIs: ['objectql', 'storage'], - allowedPaths: ['/data', '/tmp'], - allowedEndpoints: ['https://api.example.com'], - allowedEnvVars: ['NODE_ENV', 'API_KEY'], - }, - }; - const result = PluginSandboxingSchema.parse(config); - expect(result.enabled).toBe(true); - expect(result.isolationLevel).toBe('process'); - expect(result.resourceQuotas?.maxMemoryMB).toBe(512); - }); - - it('should apply defaults', () => { - const result = PluginSandboxingSchema.parse({}); - expect(result.enabled).toBe(false); - expect(result.isolationLevel).toBe('none'); - expect(result.scope).toBe('automation-only'); - }); - - it('should accept all isolation scope values', () => { - const scopes = ['automation-only', 'untrusted-only', 'all-plugins']; - scopes.forEach((scope) => { - const result = PluginSandboxingSchema.parse({ scope }); - expect(result.scope).toBe(scope); - }); - }); - - it('should accept full plugin isolation with IPC', () => { - const config = { - enabled: true, - scope: 'all-plugins' as const, - isolationLevel: 'process' as const, - ipc: { - enabled: true, - transport: 'unix-socket' as const, - maxMessageSize: 2097152, - timeout: 15000, - allowedServices: ['metadata', 'data', 'auth'], - }, - }; - const result = PluginSandboxingSchema.parse(config); - expect(result.scope).toBe('all-plugins'); - expect(result.ipc?.enabled).toBe(true); - expect(result.ipc?.transport).toBe('unix-socket'); - expect(result.ipc?.allowedServices).toHaveLength(3); - }); - - it('should apply IPC defaults', () => { - const config = { - enabled: true, - scope: 'all-plugins' as const, - isolationLevel: 'process' as const, - ipc: {}, - }; - const result = PluginSandboxingSchema.parse(config); - expect(result.ipc?.enabled).toBe(true); - expect(result.ipc?.transport).toBe('message-port'); - expect(result.ipc?.maxMessageSize).toBe(1048576); - expect(result.ipc?.timeout).toBe(30000); - }); - }); - - describe('PluginPerformanceMonitoringSchema', () => { - it('should accept performance monitoring config', () => { - const config = { - enabled: true, - metrics: ['load-time', 'init-time', 'memory-usage'], - samplingRate: 0.5, - reportingInterval: 120, - budgets: { - maxLoadTimeMs: 1000, - maxInitTimeMs: 2000, - maxMemoryMB: 256, - }, - onBudgetViolation: 'error', - }; - const result = PluginPerformanceMonitoringSchema.parse(config); - expect(result.enabled).toBe(true); - expect(result.samplingRate).toBe(0.5); - expect(result.budgets?.maxLoadTimeMs).toBe(1000); - }); - - it('should apply defaults', () => { - const result = PluginPerformanceMonitoringSchema.parse({}); - expect(result.enabled).toBe(false); - expect(result.samplingRate).toBe(1); - expect(result.reportingInterval).toBe(60); - }); - }); - - describe('PluginLoadingConfigSchema', () => { - it('should accept complete loading config', () => { - const config = { - strategy: 'lazy', - preload: { - enabled: true, - priority: 50, - }, - codeSplitting: { - enabled: true, - strategy: 'feature', - }, - dynamicImport: { - enabled: true, - mode: 'async', - }, - initialization: { - mode: 'parallel', - timeout: 30000, - }, - caching: { - enabled: true, - storage: 'memory', - }, - }; - const result = PluginLoadingConfigSchema.parse(config); - expect(result.strategy).toBe('lazy'); - expect(result.preload?.enabled).toBe(true); - expect(result.codeSplitting?.strategy).toBe('feature'); - }); - - it('should apply defaults', () => { - const result = PluginLoadingConfigSchema.parse({}); - expect(result.strategy).toBe('lazy'); - }); - - it('should accept all optional configurations', () => { - const config = { - strategy: 'eager', - preload: { enabled: true }, - codeSplitting: { enabled: false }, - dynamicImport: { enabled: true }, - initialization: { mode: 'sync' }, - dependencyResolution: { strategy: 'strict' }, - hotReload: { enabled: true }, - caching: { enabled: true }, - sandboxing: { enabled: true }, - monitoring: { enabled: true }, - }; - const result = PluginLoadingConfigSchema.parse(config); - expect(result.hotReload?.enabled).toBe(true); - expect(result.sandboxing?.enabled).toBe(true); - }); - }); - describe('PluginLoadingEventSchema', () => { it('should accept loading events', () => { const event = { @@ -571,97 +143,4 @@ describe('Plugin Loading Protocol', () => { ).toThrow(); }); }); - - describe('Integration scenarios', () => { - it('should support eager loading with preloading', () => { - const config = { - strategy: 'eager' as const, - preload: { - enabled: true, - priority: 1, - resources: ['metadata', 'dependencies', 'code'], - }, - initialization: { - mode: 'sequential' as const, - critical: true, - }, - }; - const result = PluginLoadingConfigSchema.parse(config); - expect(result.strategy).toBe('eager'); - expect(result.initialization?.critical).toBe(true); - }); - - it('should support lazy loading with caching', () => { - const config = { - strategy: 'lazy' as const, - caching: { - enabled: true, - storage: 'hybrid' as const, - ttl: 3600, - }, - dynamicImport: { - enabled: true, - mode: 'async' as const, - prefetch: true, - }, - }; - const result = PluginLoadingConfigSchema.parse(config); - expect(result.strategy).toBe('lazy'); - expect(result.caching?.storage).toBe('hybrid'); - }); - - it('should support development mode with hot reload', () => { - const config = { - strategy: 'eager' as const, - hotReload: { - enabled: true, - strategy: 'state-preserve' as const, - preserveState: true, - debounceMs: 500, - }, - monitoring: { - enabled: true, - metrics: ['load-time', 'init-time', 'memory-usage'], - }, - }; - const result = PluginLoadingConfigSchema.parse(config); - expect(result.hotReload?.enabled).toBe(true); - expect(result.hotReload?.strategy).toBe('state-preserve'); - }); - - it('should support production optimizations', () => { - const config = { - strategy: 'parallel' as const, - codeSplitting: { - enabled: true, - strategy: 'feature' as const, - maxChunkSize: 500, - sharedDependencies: { - enabled: true, - minChunks: 2, - }, - }, - caching: { - enabled: true, - storage: 'hybrid' as const, - keyStrategy: 'hash' as const, - compression: { - enabled: true, - algorithm: 'brotli' as const, - }, - }, - monitoring: { - enabled: true, - budgets: { - maxLoadTimeMs: 1000, - maxInitTimeMs: 2000, - }, - onBudgetViolation: 'error' as const, - }, - }; - const result = PluginLoadingConfigSchema.parse(config); - expect(result.codeSplitting?.enabled).toBe(true); - expect(result.caching?.compression?.enabled).toBe(true); - }); - }); }); diff --git a/packages/spec/src/kernel/plugin-loading.zod.ts b/packages/spec/src/kernel/plugin-loading.zod.ts index f50ed8367a..418e7ba408 100644 --- a/packages/spec/src/kernel/plugin-loading.zod.ts +++ b/packages/spec/src/kernel/plugin-loading.zod.ts @@ -1,719 +1,67 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. import { z } from 'zod'; - -/** - * # Plugin Loading Protocol - * - * Defines the enhanced plugin loading mechanism for the microkernel architecture. - * Inspired by industry best practices from: - * - Kubernetes CRDs and Operators - * - OSGi Dynamic Module System - * - Eclipse Plugin Framework - * - Webpack Module Federation - * - * This protocol enables: - * - Lazy loading and code splitting - * - Dynamic imports and parallel initialization - * - Capability-based discovery - * - Hot reload in development - * - Advanced caching strategies - */ - -/** - * Plugin Loading Strategy - * Determines how and when a plugin is loaded into memory - */ import { lazySchema } from '../shared/lazy-schema'; -export const PluginLoadingStrategySchema = lazySchema(() => z.enum([ - 'eager', // Load immediately during bootstrap (critical plugins) - 'lazy', // Load on first use (feature plugins) - 'parallel', // Load in parallel with other plugins - 'deferred', // Load after initial bootstrap complete - 'on-demand', // Load only when explicitly requested -]).describe('Plugin loading strategy')); /** - * Plugin Preloading Configuration - * Configures preloading behavior for faster activation - */ -export const PluginPreloadConfigSchema = lazySchema(() => z.object({ - /** - * Enable preloading for this plugin - */ - enabled: z.boolean().default(false), - - /** - * Preload priority (lower = higher priority) - */ - priority: z.number().int().min(0).default(100), - - /** - * Resources to preload - */ - resources: z.array(z.enum([ - 'metadata', // Plugin manifest and metadata - 'dependencies', // Plugin dependencies - 'assets', // Static assets (icons, translations) - 'code', // JavaScript code chunks - 'services', // Service definitions - ])).optional(), - - /** - * Conditions for preloading - */ - conditions: z.object({ - /** - * Preload only on specific routes - */ - routes: z.array(z.string()).optional(), - - /** - * Preload only for specific user roles - */ - roles: z.array(z.string()).optional(), - - /** - * Preload based on device type - */ - deviceType: z.array(z.enum(['desktop', 'mobile', 'tablet'])).optional(), - - /** - * Network connection quality threshold - */ - minNetworkSpeed: z.enum(['slow-2g', '2g', '3g', '4g']).optional(), - }).optional(), -}).describe('Plugin preloading configuration')); - -/** - * Plugin Code Splitting Configuration - * Configures how plugin code is split for optimal loading - */ -export const PluginCodeSplittingSchema = lazySchema(() => z.object({ - /** - * Enable code splitting for this plugin - */ - enabled: z.boolean().default(true), - - /** - * Split strategy - */ - strategy: z.enum([ - 'route', // Split by UI routes - 'feature', // Split by feature modules - 'size', // Split by bundle size threshold - 'custom', // Custom split points defined by plugin - ]).default('feature'), - - /** - * Chunk naming strategy - */ - chunkNaming: z.enum(['hashed', 'named', 'sequential']).default('hashed'), - - /** - * Maximum chunk size in KB - */ - maxChunkSize: z.number().int().min(10).optional().describe('Max chunk size in KB'), - - /** - * Shared dependencies optimization - */ - sharedDependencies: z.object({ - enabled: z.boolean().default(true), - /** - * Minimum times a module must be shared before extraction - */ - minChunks: z.number().int().min(1).default(2), - }).optional(), -}).describe('Plugin code splitting configuration')); - -/** - * Plugin Dynamic Import Configuration - * Configures dynamic import behavior for runtime module loading - */ -export const PluginDynamicImportSchema = lazySchema(() => z.object({ - /** - * Enable dynamic imports - */ - enabled: z.boolean().default(true), - - /** - * Import mode - */ - mode: z.enum([ - 'async', // Asynchronous import (recommended) - 'sync', // Synchronous import (blocking) - 'eager', // Eager evaluation - 'lazy', // Lazy evaluation - ]).default('async'), - - /** - * Prefetch strategy - */ - prefetch: z.boolean().default(false).describe('Prefetch module in idle time'), - - /** - * Preload strategy - */ - preload: z.boolean().default(false).describe('Preload module in parallel with parent'), - - /** - * Webpack magic comments support - */ - webpackChunkName: z.string().optional().describe('Custom chunk name for webpack'), - - /** - * Import timeout in milliseconds - */ - timeout: z.number().int().min(100).default(30000).describe('Dynamic import timeout (ms)'), - - /** - * Retry configuration on import failure - */ - retry: z.object({ - enabled: z.boolean().default(true), - maxAttempts: z.number().int().min(1).max(10).default(3), - backoffMs: z.number().int().min(0).default(1000).describe('Exponential backoff base delay'), - }).optional(), -}).describe('Plugin dynamic import configuration')); - -/** - * Plugin Initialization Configuration - * Configures how plugin initialization is executed - */ -export const PluginInitializationSchema = lazySchema(() => z.object({ - /** - * Initialization mode - */ - mode: z.enum([ - 'sync', // Synchronous initialization - 'async', // Asynchronous initialization - 'parallel', // Parallel with other plugins - 'sequential', // Must complete before next plugin - ]).default('async'), - - /** - * Initialization timeout in milliseconds - */ - timeout: z.number().int().min(100).default(30000), - - /** - * Startup priority (lower = higher priority, earlier initialization) - */ - priority: z.number().int().min(0).default(100), - - /** - * Whether to continue bootstrap if this plugin fails - */ - critical: z.boolean().default(false).describe('If true, kernel bootstrap fails if plugin fails'), - - /** - * Retry configuration on initialization failure - */ - retry: z.object({ - enabled: z.boolean().default(false), - maxAttempts: z.number().int().min(1).max(5).default(3), - backoffMs: z.number().int().min(0).default(1000), - }).optional(), - - /** - * Health check interval for monitoring - */ - healthCheckInterval: z.number().int().min(0).optional().describe('Health check interval in ms (0 = disabled)'), -}).describe('Plugin initialization configuration')); - -/** - * Plugin Dependency Resolution Configuration - * Advanced dependency resolution using semantic versioning - */ -export const PluginDependencyResolutionSchema = lazySchema(() => z.object({ - /** - * Dependency resolution strategy - */ - strategy: z.enum([ - 'strict', // Exact version match required - 'compatible', // Semver compatible versions (^) - 'latest', // Always use latest compatible - 'pinned', // Lock to specific version - ]).default('compatible'), - - /** - * Peer dependency handling - */ - peerDependencies: z.object({ - /** - * Whether to resolve peer dependencies - */ - resolve: z.boolean().default(true), - - /** - * Action on missing peer dependency - */ - onMissing: z.enum(['error', 'warn', 'ignore']).default('warn'), - - /** - * Action on peer version mismatch - */ - onMismatch: z.enum(['error', 'warn', 'ignore']).default('warn'), - }).optional(), - - /** - * Optional dependency handling - */ - optionalDependencies: z.object({ - /** - * Whether to attempt loading optional dependencies - */ - load: z.boolean().default(true), - - /** - * Action on optional dependency load failure - */ - onFailure: z.enum(['warn', 'ignore']).default('warn'), - }).optional(), - - /** - * Conflict resolution - */ - conflictResolution: z.enum([ - 'fail', // Fail on any version conflict - 'latest', // Use latest version - 'oldest', // Use oldest version - 'manual', // Require manual resolution - ]).default('latest'), - - /** - * Circular dependency handling - */ - circularDependencies: z.enum([ - 'error', // Throw error on circular dependency - 'warn', // Warn but continue - 'allow', // Allow circular dependencies - ]).default('warn'), -}).describe('Plugin dependency resolution configuration')); - -/** - * Plugin Hot Reload Configuration - * Enables hot module replacement for development and production environments. - * - * Production mode adds safety features: health validation, rollback on failure, - * connection draining, and concurrency control for zero-downtime reloads. - */ -export const PluginHotReloadSchema = lazySchema(() => z.object({ - /** - * Enable hot reload - */ - enabled: z.boolean().default(false), - - /** - * Target environment for hot reload behavior - */ - environment: z.enum([ - 'development', // Fast reload with relaxed safety (file watchers, no health validation) - 'staging', // Production-like reload with validation but relaxed rollback - 'production', // Full safety: health validation, rollback, connection draining - ]).default('development').describe('Target environment controlling safety level'), - - /** - * Hot reload strategy - */ - strategy: z.enum([ - 'full', // Full plugin reload (destroy and reinitialize) - 'partial', // Partial reload (update changed modules only) - 'state-preserve', // Preserve plugin state during reload - ]).default('full'), - - /** - * Files to watch for changes - */ - watchPatterns: z.array(z.string()).optional().describe('Glob patterns for files to watch'), - - /** - * Files to ignore - */ - ignorePatterns: z.array(z.string()).optional().describe('Glob patterns for files to ignore'), - - /** - * Debounce delay in milliseconds - */ - debounceMs: z.number().int().min(0).default(300), - - /** - * Whether to preserve state during reload - */ - preserveState: z.boolean().default(false), - - /** - * State serialization - */ - stateSerialization: z.object({ - enabled: z.boolean().default(false), - /** - * Path to state serialization handler - */ - handler: z.string().optional(), - }).optional(), - - /** - * Hooks for hot reload lifecycle - */ - hooks: z.object({ - beforeReload: z.string().optional().describe('Function to call before reload'), - afterReload: z.string().optional().describe('Function to call after reload'), - onError: z.string().optional().describe('Function to call on reload error'), - }).optional(), - - /** - * Production safety configuration - * Applied when environment is 'staging' or 'production' - */ - productionSafety: z.object({ - /** - * Validate plugin health before completing reload - */ - healthValidation: z.boolean().default(true) - .describe('Run health checks after reload before accepting traffic'), - - /** - * Automatically rollback to previous version on reload failure - */ - rollbackOnFailure: z.boolean().default(true) - .describe('Auto-rollback if reloaded plugin fails health check'), - - /** - * Maximum time to wait for health validation after reload (ms) - */ - healthTimeout: z.number().int().min(1000).default(30000) - .describe('Health check timeout after reload in ms'), - - /** - * Drain active connections before reload - */ - drainConnections: z.boolean().default(true) - .describe('Gracefully drain active requests before reloading'), - - /** - * Maximum time to wait for connection draining (ms) - */ - drainTimeout: z.number().int().min(0).default(15000) - .describe('Max wait time for connection draining in ms'), - - /** - * Maximum number of concurrent plugin reloads - */ - maxConcurrentReloads: z.number().int().min(1).default(1) - .describe('Limit concurrent reloads to prevent system instability'), - - /** - * Minimum interval between reloads of the same plugin (ms) - */ - minReloadInterval: z.number().int().min(1000).default(5000) - .describe('Cooldown period between reloads of the same plugin'), - }).optional(), -}).describe('Plugin hot reload configuration')); - -/** - * Plugin Caching Configuration - * Configures caching strategy for faster subsequent loads - */ -export const PluginCachingSchema = lazySchema(() => z.object({ - /** - * Enable caching - */ - enabled: z.boolean().default(true), - - /** - * Cache storage type - */ - storage: z.enum([ - 'memory', // In-memory cache (fastest, not persistent) - 'disk', // Disk cache (persistent) - 'indexeddb', // Browser IndexedDB (persistent, browser only) - 'hybrid', // Memory + Disk hybrid - ]).default('memory'), - - /** - * Cache key strategy - */ - keyStrategy: z.enum([ - 'version', // Cache by plugin version - 'hash', // Cache by content hash - 'timestamp', // Cache by last modified timestamp - ]).default('version'), - - /** - * Cache TTL in seconds - */ - ttl: z.number().int().min(0).optional().describe('Time to live in seconds (0 = infinite)'), - - /** - * Maximum cache size in MB - */ - maxSize: z.number().int().min(1).optional().describe('Max cache size in MB'), - - /** - * Cache invalidation triggers - */ - invalidateOn: z.array(z.enum([ - 'version-change', - 'dependency-change', - 'manual', - 'error', - ])).optional(), - - /** - * Compression - */ - compression: z.object({ - enabled: z.boolean().default(false), - algorithm: z.enum(['gzip', 'brotli', 'deflate']).default('gzip'), - }).optional(), -}).describe('Plugin caching configuration')); - -/** - * Plugin Sandboxing Configuration - * Security isolation for plugins with configurable scope. - * - * Supports isolation beyond automation scripts: any plugin can be sandboxed - * with process-level isolation and inter-plugin communication (IPC). - */ -export const PluginSandboxingSchema = lazySchema(() => z.object({ - /** - * Enable sandboxing - */ - enabled: z.boolean().default(false), - - /** - * Isolation scope - which plugins are subject to sandboxing - */ - scope: z.enum([ - 'automation-only', // Sandbox automation/scripting plugins only (current behavior) - 'untrusted-only', // Sandbox plugins below a trust threshold - 'all-plugins', // Sandbox all plugins (maximum isolation) - ]).default('automation-only').describe('Which plugins are subject to isolation'), - - /** - * Sandbox isolation level - */ - isolationLevel: z.enum([ - 'none', // No isolation - 'process', // Separate process (Node.js worker threads) - 'vm', // VM context isolation - 'iframe', // iframe isolation (browser) - 'web-worker', // Web Worker (browser) - ]).default('none'), - - /** - * Allowed capabilities - */ - allowedCapabilities: z.array(z.string()).optional().describe('List of allowed capability IDs'), - - /** - * Resource quotas - */ - resourceQuotas: z.object({ - /** - * Maximum memory usage in MB - */ - maxMemoryMB: z.number().int().min(1).optional(), - - /** - * Maximum CPU time in milliseconds - */ - maxCpuTimeMs: z.number().int().min(100).optional(), - - /** - * Maximum number of file descriptors - */ - maxFileDescriptors: z.number().int().min(1).optional(), - - /** - * Maximum network bandwidth in KB/s - */ - maxNetworkKBps: z.number().int().min(1).optional(), - }).optional(), - - /** - * Permissions - */ - permissions: z.object({ - /** - * Allowed API access - */ - allowedAPIs: z.array(z.string()).optional(), - - /** - * Allowed file system paths - */ - allowedPaths: z.array(z.string()).optional(), - - /** - * Allowed network endpoints - */ - allowedEndpoints: z.array(z.string()).optional(), - - /** - * Allowed environment variables - */ - allowedEnvVars: z.array(z.string()).optional(), - }).optional(), - - /** - * Inter-Plugin Communication (IPC) configuration - * Enables isolated plugins to communicate with the kernel and other plugins - */ - ipc: z.object({ - /** - * Enable IPC for sandboxed plugins - */ - enabled: z.boolean().default(true) - .describe('Allow sandboxed plugins to communicate via IPC'), - - /** - * IPC transport mechanism - */ - transport: z.enum([ - 'message-port', // MessagePort (worker threads / Web Workers) - 'unix-socket', // Unix domain sockets (process isolation) - 'tcp', // TCP sockets (container isolation) - 'memory', // Shared memory channel (in-process VM) - ]).default('message-port') - .describe('IPC transport for cross-boundary communication'), - - /** - * Maximum message size in bytes - */ - maxMessageSize: z.number().int().min(1024).default(1048576) - .describe('Maximum IPC message size in bytes (default 1MB)'), - - /** - * Message timeout in milliseconds - */ - timeout: z.number().int().min(100).default(30000) - .describe('IPC message response timeout in ms'), - - /** - * Allowed service calls through IPC - */ - allowedServices: z.array(z.string()).optional() - .describe('Service names the sandboxed plugin may invoke via IPC'), - }).optional(), -}).describe('Plugin sandboxing configuration')); - -/** - * Plugin Performance Monitoring Configuration - * Telemetry and performance tracking + * # Plugin Loading Protocol + * + * What remains of this module is the **observational** half: the lifecycle + * event a loader would emit (`PluginLoadingEventSchema`) and the per-plugin + * state it would track (`PluginLoadingStateSchema`). + * + * The **configuration** half — `PluginLoadingConfigSchema` and the ten member + * schemas it combined, reached from authored metadata as `manifest.loading` — + * was REMOVED in v17 per ADR-0049 enforce-or-remove (#4914). See the block + * below. */ -export const PluginPerformanceMonitoringSchema = lazySchema(() => z.object({ - /** - * Enable performance monitoring - */ - enabled: z.boolean().default(false), - - /** - * Metrics to collect - */ - metrics: z.array(z.enum([ - 'load-time', - 'init-time', - 'memory-usage', - 'cpu-usage', - 'api-calls', - 'error-rate', - 'cache-hit-rate', - ])).optional(), - - /** - * Sampling rate (0-1, where 1 = 100%) - */ - samplingRate: z.number().min(0).max(1).default(1), - - /** - * Reporting interval in seconds - */ - reportingInterval: z.number().int().min(1).default(60), - - /** - * Performance budget thresholds - */ - budgets: z.object({ - /** - * Maximum load time in milliseconds - */ - maxLoadTimeMs: z.number().int().min(0).optional(), - - /** - * Maximum init time in milliseconds - */ - maxInitTimeMs: z.number().int().min(0).optional(), - - /** - * Maximum memory usage in MB - */ - maxMemoryMB: z.number().int().min(0).optional(), - }).optional(), - - /** - * Action on budget violation - */ - onBudgetViolation: z.enum(['warn', 'error', 'ignore']).default('warn'), -}).describe('Plugin performance monitoring configuration')); -/** - * Complete Plugin Loading Configuration - * Combines all loading-related configurations - */ -export const PluginLoadingConfigSchema = lazySchema(() => z.object({ - /** - * Loading strategy - */ - strategy: PluginLoadingStrategySchema.default('lazy'), - - /** - * Preloading configuration - */ - preload: PluginPreloadConfigSchema.optional(), - - /** - * Code splitting configuration - */ - codeSplitting: PluginCodeSplittingSchema.optional(), - - /** - * Dynamic import configuration - */ - dynamicImport: PluginDynamicImportSchema.optional(), - - /** - * Initialization configuration - */ - initialization: PluginInitializationSchema.optional(), - - /** - * Dependency resolution configuration - */ - dependencyResolution: PluginDependencyResolutionSchema.optional(), - - /** - * Hot reload configuration (development and production) - */ - hotReload: PluginHotReloadSchema.optional(), - - /** - * Caching configuration - */ - caching: PluginCachingSchema.optional(), - - /** - * Sandboxing configuration - */ - sandboxing: PluginSandboxingSchema.optional(), - - /** - * Performance monitoring - */ - monitoring: PluginPerformanceMonitoringSchema.optional(), -}).describe('Complete plugin loading configuration')); +// --------------------------------------------------------------------------- +// REMOVED in v17 (#4914, ADR-0049 enforce-or-remove; maintainer ruling +// 2026-08-04): the entire `manifest.loading` configuration block. +// +// Gone with it: PluginLoadingConfigSchema and every schema only it embedded — +// PluginLoadingStrategySchema, PluginPreloadConfigSchema, +// PluginCodeSplittingSchema, PluginDynamicImportSchema, +// PluginInitializationSchema, PluginDependencyResolutionSchema, +// PluginHotReloadSchema, PluginCachingSchema, PluginSandboxingSchema, +// PluginPerformanceMonitoringSchema, and every type alias of those. +// `Manifest.loading` itself is a `retiredKey()` tombstone in `manifest.zod.ts`. +// +// Why: none of it had a runtime reader. A bare-name scan of all three repos +// (objectstack / cloud / objectui, each with a control probe proving the scan +// saw the tree) found every hit inside `packages/spec` itself — this module's +// own declaration, its own unit tests, the `manifest.zod.ts` embed and the +// generated artifacts. Nothing in `packages/core`, `packages/runtime` or +// `packages/metadata` ever read `manifest.loading.*`. So an author could write +// a full loading policy, have it parse clean, land it in the manifest, and have +// NOTHING happen — the #3950 shape (an exported schema with no consumer is read +// as a capability) at the scale of a whole block. +// +// Why `sandboxing` made this urgent rather than merely untidy: `PluginSandboxing` +// declared `isolationLevel: 'process' | 'vm' | 'iframe' | 'web-worker'`, IPC +// transports and an `allowedServices` ACL. An AI author (ADR-0033) reading that +// vocabulary concludes the platform isolates plugins and writes +// `loading: { sandboxing: { isolationLevel: 'process' } }`. It parsed, and it +// isolated nothing. A security control that is inert is worse than an absent +// one, because it is *believed* — that is ADR-0049's false-compliance case at +// its sharpest, and it is why the ruling chose REMOVE over `experimental`. +// +// Hot reload — the two-source convergence (ruling §2). This module's +// `PluginHotReloadSchema` was the DEAD one of two hot-reload vocabularies. The +// surviving one is `HotReloadConfigSchema` in `plugin-lifecycle-advanced.zod.ts` +// (carried on `AdvancedPluginLifecycleConfig.hotReload`), which is what +// `HotReloadManager` (`packages/core/src/hot-reload.ts`) actually reads. That +// side is KEPT as the starting point if hot reload is ever enforced — note it +// has an implementation body but no runtime composes it today (only its own +// unit test and `packages/core/examples/phase2-integration.ts` construct one), +// so it is a foundation, not a shipped capability. Enforcing it is deliberately +// a separate decision, not this retirement. +// +// If runtime loading policy is ever built, it returns via the ENFORCE route of +// ADR-0049 through a new ADR: write the loader first, then declare exactly the +// configuration it honours. The vocabulary it needs is unlikely to be this one. +// --------------------------------------------------------------------------- /** * Plugin Loading Event @@ -739,27 +87,27 @@ export const PluginLoadingEventSchema = lazySchema(() => z.object({ 'dynamic-unload', // Plugin unloaded at runtime 'dynamic-discover', // Plugin discovered via registry ]), - + /** * Plugin identifier */ pluginId: z.string(), - + /** * Timestamp */ timestamp: z.number().int().min(0), - + /** * Duration in milliseconds */ durationMs: z.number().int().min(0).optional(), - + /** * Additional metadata */ metadata: z.record(z.string(), z.unknown()).optional(), - + /** * Error if event represents a failure */ @@ -779,7 +127,7 @@ export const PluginLoadingStateSchema = lazySchema(() => z.object({ * Plugin identifier */ pluginId: z.string(), - + /** * Current state */ @@ -794,27 +142,27 @@ export const PluginLoadingStateSchema = lazySchema(() => z.object({ 'unloading', // Being unloaded at runtime 'unloaded', // Successfully unloaded (dynamic loading) ]), - + /** * Load progress (0-100) */ progress: z.number().min(0).max(100).default(0), - + /** * Loading start time */ startedAt: z.number().int().min(0).optional(), - + /** * Loading completion time */ completedAt: z.number().int().min(0).optional(), - + /** * Last error */ lastError: z.string().optional(), - + /** * Retry count */ @@ -822,37 +170,6 @@ export const PluginLoadingStateSchema = lazySchema(() => z.object({ }).describe('Plugin loading state')); // Export types -export type PluginLoadingStrategy = z.input; -export type PluginPreloadConfig = z.input; -/** Post-parse shape of {@link PluginPreloadConfig} — defaults applied, transforms run (ADR-0122). */ -export type PluginPreloadConfigParsed = z.infer; -export type PluginCodeSplitting = z.input; -/** Post-parse shape of {@link PluginCodeSplitting} — defaults applied, transforms run (ADR-0122). */ -export type PluginCodeSplittingParsed = z.infer; -export type PluginDynamicImport = z.input; -/** Post-parse shape of {@link PluginDynamicImport} — defaults applied, transforms run (ADR-0122). */ -export type PluginDynamicImportParsed = z.infer; -export type PluginInitialization = z.input; -/** Post-parse shape of {@link PluginInitialization} — defaults applied, transforms run (ADR-0122). */ -export type PluginInitializationParsed = z.infer; -export type PluginDependencyResolution = z.input; -/** Post-parse shape of {@link PluginDependencyResolution} — defaults applied, transforms run (ADR-0122). */ -export type PluginDependencyResolutionParsed = z.infer; -export type PluginHotReload = z.input; -/** Post-parse shape of {@link PluginHotReload} — defaults applied, transforms run (ADR-0122). */ -export type PluginHotReloadParsed = z.infer; -export type PluginCaching = z.input; -/** Post-parse shape of {@link PluginCaching} — defaults applied, transforms run (ADR-0122). */ -export type PluginCachingParsed = z.infer; -export type PluginSandboxing = z.input; -/** Post-parse shape of {@link PluginSandboxing} — defaults applied, transforms run (ADR-0122). */ -export type PluginSandboxingParsed = z.infer; -export type PluginPerformanceMonitoring = z.input; -/** Post-parse shape of {@link PluginPerformanceMonitoring} — defaults applied, transforms run (ADR-0122). */ -export type PluginPerformanceMonitoringParsed = z.infer; -export type PluginLoadingConfig = z.input; -/** Post-parse shape of {@link PluginLoadingConfig} — defaults applied, transforms run (ADR-0122). */ -export type PluginLoadingConfigParsed = z.infer; export type PluginLoadingEvent = z.input; export type PluginLoadingState = z.input; /** Post-parse shape of {@link PluginLoadingState} — defaults applied, transforms run (ADR-0122). */ diff --git a/packages/spec/src/migrations/registry.ts b/packages/spec/src/migrations/registry.ts index 2f99cff0a0..6c1b397911 100644 --- a/packages/spec/src/migrations/registry.ts +++ b/packages/spec/src/migrations/registry.ts @@ -1217,7 +1217,22 @@ const step17: MigrationStep = { + 'two the runtime honoured. It is tombstoned rather than deleted, so the answer arrives ' + 'as a rejection carrying the fix; and because a descriptor lives in executor TypeScript ' + 'rather than in stored metadata, its prescription is a semantic entry below rather than ' - + 'a conversion `os migrate meta` could replay.', + + 'a conversion `os migrate meta` could replay.\n\n' + + 'The plugin manifest loses its whole `loading` block in this step (#4914, ADR-0049, ' + + 'maintainer ruling 2026-08-04) — the same enforce-or-remove question asked of a block ' + + 'rather than a key, and answered REMOVE on measurement: every reference to ' + + '`manifest.loading.*` in objectstack, cloud and objectui lived inside `packages/spec` ' + + 'itself, so a full loading policy parsed, entered the manifest, and configured nothing. ' + + 'The reason it outranked ordinary inert-key cleanup is that one of its members was ' + + '`sandboxing`, declaring process / vm / iframe / web-worker isolation and a service ACL: ' + + 'an inert SECURITY control is worse than an absent one, because an author (very often an ' + + 'AI, ADR-0033) reads the vocabulary as proof the isolation exists and stops looking. Hot ' + + 'reload was a two-source defect on top of that — the retired `PluginHotReloadSchema` was ' + + 'the dead one of two vocabularies, and the ruling converges on the live one, ' + + '`HotReloadConfigSchema`, which `HotReloadManager` actually reads and which is KEPT ' + + 'unenforced as the starting point for a separate future decision. Like `isAsync`, its ' + + 'prescription is a semantic entry rather than a conversion: a manifest is not a stack ' + + 'collection, so `os migrate meta` has no seam at which to rewrite one.', conversionIds: [ 'action-execute-to-target', 'field-conditionalRequired-to-requiredWhen', @@ -3076,6 +3091,61 @@ const step17: MigrationStep = { + 'reports the total across the whole matching inbox rather than the window. A caller ' + 'that omitted `limit` receives the same 50 rows it always received.', }, + { + id: 'plugin-manifest-loading-retired', + surface: + 'manifest.loading (the whole block: strategy / preload / codeSplitting / dynamicImport / ' + + 'initialization / dependencyResolution / hotReload / caching / sandboxing / monitoring)', + replacement: + 'nothing to re-declare — delete the key. Plugins are composed at boot: `defineStack` ' + + 'registers them and the kernel runs `init` then `start` in an order topologically ' + + "resolved from each composed plugin's own `dependencies` / `optionalDependencies` " + + '(`resolvePluginOrder` in `packages/core/src/plugin-order.ts`). For the isolation ' + + '`loading.sandboxing` appeared to configure, use the plugin trust tier ' + + '(`manifest.runtime`, ADR-0025 §3.6) and the manifest permission declarations, which ' + + 'are the surfaces the platform actually enforces', + reason: + 'ADR-0049 enforce-or-remove; maintainer ruling 2026-08-04 on #4914. The block declared a ' + + 'complete plugin loading policy and NOTHING read it. A bare-name scan of all three ' + + 'repos — objectstack, cloud (measured 2026-08-09) and objectui (measured at pickup), ' + + 'each with a control probe proving the scan saw the tree — put every hit inside ' + + '`packages/spec` itself: this module\'s own declaration, its own unit tests, the ' + + '`Manifest.loading` embed and the generated artifacts. `manifest.loading.*` had zero ' + + 'readers in `packages/core`, `packages/runtime` and `packages/metadata`. So the key ' + + 'parsed, entered the manifest, and changed nothing — #3950, at the scale of a whole ' + + 'block. What made it outrank ordinary inert-key cleanup is `sandboxing`: it declared ' + + 'process / vm / iframe / web-worker isolation, IPC transports and an `allowedServices` ' + + 'ACL, so an AI author (ADR-0033) reading that vocabulary concluded the platform ' + + 'isolates plugins, wrote the config, and received a clean parse and zero isolation. An ' + + 'inert security control is worse than an absent one because it is believed. Hot reload ' + + 'was additionally a TWO-SOURCE defect: the docs pointed at this dead ' + + '`PluginHotReloadSchema` while the only implementation body, `HotReloadManager` ' + + '(`packages/core/src/hot-reload.ts`), reads a different vocabulary — ' + + '`HotReloadConfigSchema` in `plugin-lifecycle-advanced.zod.ts`. Ruling §2 converges on ' + + 'the surviving side: that schema is KEPT as the starting point for a future enforce ' + + 'decision (it has an implementation body but no runtime composes it yet), and ' + + 'enforcing it is deliberately a separate decision, not this retirement. ' + + 'Why D3 semantic and not a D2 conversion: the chain walks a normalized STACK and ' + + '`applyConversionsToStoredItem` maps a metadata type onto one of its collections. A ' + + 'package manifest is neither — `PLURAL_TO_SINGULAR` has no `packages` / `plugins` ' + + 'entry, so a manifest is not a stack collection member and a stored manifest row ' + + 'passes that seam through unchanged. A conversion would be a transform with no seam ' + + 'that ever runs.', + acceptanceCriteria: + 'No `objectstack.plugin.json` and no stored package manifest carries a `loading` key. ' + + 'The enforced channel is the one place a manifest is parsed with an author present: ' + + '`os plugin build` runs `ManifestSchema.safeParse` and exits non-zero, printing the ' + + 'tombstone prescription, so a manifest still declaring `loading` fails its build ' + + 'rather than shipping. TypeScript authors get it earlier still — `loading` is typed ' + + '`never`, so assigning it is a `tsc` error. ⚠️ Runtime behaviour is deliberately ' + + 'UNCHANGED and must be verified as such: nothing ever read the block, so removing it ' + + 'removes no behaviour. A package ALREADY INSTALLED whose stored manifest carries ' + + '`loading` keeps working — the registry\'s `validate()` is an explicit diagnostic and ' + + 'not a gate (it catches, logs `[metadata_spec_invalid]`, and registers the item ' + + 'anyway, deliberately, so bad metadata is never a data outage), so such a row ' + + 'degrades to one log line at registration rather than a boot failure. Clear it by ' + + 'deleting the key from the source manifest and reinstalling.', + }, { id: 'api-runtime-create-withdrawn', surface: 'PUT /api/v1/meta/api/{name} (runtime-authored `api` endpoints, draft and active alike)', @@ -3353,6 +3423,26 @@ export const RETIRED_KEYS_BY_MAJOR: Readonly> // callers rather than a stack conversion"). 'api/ListNotificationsRequest:cursor', 'api/ListNotificationsResponse:cursor', + // #4914 — ADR-0049 enforce-or-remove on the plugin manifest's whole + // `loading` block (maintainer ruling 2026-08-04). ONE tombstoned key here, + // because `loading` was the single carrier: every schema underneath it + // (`PluginLoadingConfig` and the ten members it combined) leaves the + // published set as a whole-def removal and is registered in + // `RETIRED_DEFS_BY_MAJOR` below, not as ~27 individual key entries. + // + // Registered here but NOT in `src/conversions/registry.ts`, for the reason + // `automation/ActionDescriptor:isAsync` above gives: the conversion chain + // walks a normalized STACK (`mapCollection(stack, 'objects' | 'views' | …)`) + // and `applyConversionsToStoredItem` maps a metadata type onto one of those + // collections. A package manifest is neither — there is no `packages` / + // `plugins` entry in `PLURAL_TO_SINGULAR`, so a manifest is not a stack + // collection member and a stored manifest row passes that seam through + // unchanged. A MetadataConversion here would be a transform with no seam + // that ever runs. The prescription reaches authors instead through the + // tombstone at the one place a manifest is parsed with an author present + // (`os plugin build` → `ManifestSchema.safeParse`, which exits non-zero), + // and through the D3 semantic entry `plugin-manifest-loading-retired`. + 'kernel/Manifest:loading', // #6815 — the per-aggregation DISTINCT flag, retired under ADR-0049 by // maintainer ruling 2026-08-09. ONE key, and one entry, because // `AggregationNodeSchema` is reused BY REFERENCE rather than `.extend()`ed: @@ -3523,5 +3613,29 @@ export const RETIRED_DEFS_BY_MAJOR: Readonly> 'automation/ETLTransformationType', 'automation/ETLSyncMode', 'automation/ETLRunStatus', + // #4914 — the plugin manifest's `loading` block (ADR-0049 enforce-or-remove, + // maintainer ruling 2026-08-04). `PluginLoadingConfig` was reachable from + // authored metadata ONLY through `Manifest.loading`, and the ten members + // below were embedded only by it, so retiring the carrier key unpublishes + // the whole closure. The carrier itself is a `retiredKey()` tombstone + // registered one level up in `RETIRED_KEYS_BY_MAJOR`. + // + // ⚠️ `kernel/PluginLoadingEvent` and `kernel/PluginLoadingState` are + // deliberately NOT here. They live in the same module and share its prefix, + // but neither was ever embedded in `PluginLoadingConfig` — they are the + // observational half (a lifecycle event and a per-plugin state), they are + // not authorable, and they still emit. Module adjacency is not evidence, + // the `system/ServerRateLimitConfig` note above applies verbatim. + 'kernel/PluginLoadingConfig', + 'kernel/PluginLoadingStrategy', + 'kernel/PluginPreloadConfig', + 'kernel/PluginCodeSplitting', + 'kernel/PluginDynamicImport', + 'kernel/PluginInitialization', + 'kernel/PluginDependencyResolution', + 'kernel/PluginHotReload', + 'kernel/PluginCaching', + 'kernel/PluginSandboxing', + 'kernel/PluginPerformanceMonitoring', ], }; diff --git a/packages/spec/src/type-alias-convention.pin.test.ts b/packages/spec/src/type-alias-convention.pin.test.ts index 1f947aeae6..2e3a1bec45 100644 --- a/packages/spec/src/type-alias-convention.pin.test.ts +++ b/packages/spec/src/type-alias-convention.pin.test.ts @@ -265,7 +265,7 @@ import type * as M167 from './ui/view.zod.js'; import type * as M170 from './ui/component.zod.js'; // --------------------------------------------------------------------------- -// 824 isomorphic aliases: `z.input` === `z.infer`, so no `XParsed` is declared. +// 823 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 @@ -920,7 +920,8 @@ export type Iso439 = Assert, export type Iso440 = Assert, z.infer< typeof M94.PluginHealthReportSchema > >>; // kernel/plugin-loading.zod.ts -export type Iso441 = Assert, z.infer< typeof M95.PluginLoadingStrategySchema > >>; +// (Iso441 pinned `PluginLoadingStrategySchema`, removed with the rest of the +// `manifest.loading` block in #4914 — ADR-0049 enforce-or-remove.) export type Iso442 = Assert, z.infer< typeof M95.PluginLoadingEventSchema > >>; // kernel/plugin-registry.zod.ts @@ -1621,7 +1622,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 824 isomorphic pins', () => { + it('still declares all 823 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 @@ -1757,9 +1758,16 @@ describe('ADR-0122 type-alias convention', () => { // the pin also took its THIRD number (759 -> 760 -> 833), each time // because the next-free id had been claimed by a branch that merged // first. The file, not the history, is the operand. + // + // 824 -> 823 is #4914's ADR-0049 retirement of the `manifest.loading` + // block: `Iso441` pinned `PluginLoadingStrategySchema`, one of the eleven + // defs unpublished with the carrier key, so its pin goes with the schema. + // A DECREASE, and the first here — the id is retired in place rather than + // renumbered, because the ids are claims about pins and not positions + // (`Iso824` remains the highest, and the next author still takes 825). const self = readFileSync(fileURLToPath(import.meta.url), 'utf8'); const pins = self.match(/^export type Iso\d+ = Assert