diff --git a/.changeset/kernel-service-status-dual-source.md b/.changeset/kernel-service-status-dual-source.md new file mode 100644 index 0000000000..71622ca3e7 --- /dev/null +++ b/.changeset/kernel-service-status-dual-source.md @@ -0,0 +1,26 @@ +--- +"@objectstack/spec": minor +--- + +BREAKING(spec): `@objectstack/spec/system` renames `ServiceStatusSchema` → `KernelServiceStatusSchema` and gains the `KernelServiceStatus` type alias (#6604, ADR-0112 D9a) + +`ServiceStatus` was published by two entry points for two disjoint concepts: `./api`'s +discovery health **enum** (`api/discovery.zod.ts`) and `./system`'s kernel service **state +object** (`system/core-services.zod.ts`, a `features`-bearing record). Per the maintainer +ruling of 2026-08-08 (Option B) the kernel side takes the domain-specific name, matching +its `KernelServiceMapSchema` sibling in the same file: + +- `ServiceStatusSchema` → `KernelServiceStatusSchema` (`@objectstack/spec/system`) +- new: `export type KernelServiceStatus` — the alias `#4593`'s backfill had to skip, + because declaring `ServiceStatus` on both entry points would have minted the #4411 + dual-source trap +- JSON Schema def `system/ServiceStatus` → `system/KernelServiceStatus`, carried through + `RENAMED_DEFS` with all 6 authorable keys (`enabled` / `features` / `name` / `provider` / + `status` / `version`) intact + +**`@objectstack/spec/api`'s `ServiceStatus` is untouched** — it keeps its published name, +so no consumer of the discovery health enum changes. Nothing left the author-facing +contract: the renamed def re-emits every key byte-for-byte, which is why this rides the +rename table rather than the retirement kit. Consumers importing `ServiceStatusSchema` +from `@objectstack/spec/system` update the name; `tsc` reports TS2724/TS2305 on any that +does not. diff --git a/content/docs/references/index.mdx b/content/docs/references/index.mdx index 4a60bf9d6c..4ec32151e2 100644 --- a/content/docs/references/index.mdx +++ b/content/docs/references/index.mdx @@ -330,7 +330,7 @@ The runtime environment — logging, jobs, cache, metrics, notifications, i18n a | [`cache.zod.ts`](/docs/references/system/cache) | `CacheAvalanchePrevention`, `CacheConfig`, `CacheConsistency`, `CacheInvalidation`, `CacheStrategy`, `CacheTier`, `CacheWarmup`, `DistributedCacheConfig` | | [`change-management.zod.ts`](/docs/references/system/change-management) | `ChangeImpact`, `ChangePriority`, `ChangeRequest`, `ChangeStatus`, `ChangeType`, `RollbackPlan` | | [`collaboration.zod.ts`](/docs/references/system/collaboration) | `AwarenessEvent`, `AwarenessSession`, `AwarenessUpdate`, `AwarenessUserState`, `CRDTMergeResult`, `CRDTState`, `CRDTType`, `CollaborationMode`, `CollaborationSession`, `CollaborationSessionConfig`, `CollaborativeCursor`, `CounterOperation`, `CursorColorPreset`, `CursorSelection`, `CursorStyle`, `CursorUpdate`, `GCounter`, `LWWRegister`, `ORSet`, `ORSetElement`, `OTComponent`, `OTOperation`, `OTOperationType`, `OTTransformResult`, `PNCounter`, `TextCRDTOperation`, `TextCRDTState`, `UserActivityStatus`, `VectorClock` | -| [`core-services.zod.ts`](/docs/references/system/core-services) | `CoreServiceName`, `KernelServiceMap`, `ServiceConfig`, `ServiceCriticality`, `ServiceStatus` | +| [`core-services.zod.ts`](/docs/references/system/core-services) | `CoreServiceName`, `KernelServiceMap`, `KernelServiceStatus`, `ServiceConfig`, `ServiceCriticality` | | [`deploy-bundle.zod.ts`](/docs/references/system/deploy-bundle) | `DeployBundle`, `DeployDiff`, `DeployManifest`, `DeployStatusEnum`, `DeployValidationIssue`, `DeployValidationResult`, `MigrationPlan`, `MigrationStatement`, `SchemaChange` | | [`disaster-recovery.zod.ts`](/docs/references/system/disaster-recovery) | `BackupConfig`, `BackupRetention`, `BackupStrategy`, `DisasterRecoveryPlan`, `FailoverConfig`, `FailoverMode`, `RPO`, `RTO` | | [`doc.zod.ts`](/docs/references/system/doc) | `Doc` | diff --git a/content/docs/references/system/core-services.mdx b/content/docs/references/system/core-services.mdx index e0012e5b7b..1c3de7d6fe 100644 --- a/content/docs/references/system/core-services.mdx +++ b/content/docs/references/system/core-services.mdx @@ -20,8 +20,8 @@ This registry is used by the `ObjectKernel` and `HttpDispatcher` to: ## TypeScript Usage ```typescript -import { CoreServiceName, KernelServiceMapSchema, ServiceConfigSchema, ServiceCriticalitySchema, ServiceStatusSchema } from '@objectstack/spec/system'; -import type { CoreServiceName, KernelServiceMap, ServiceConfig, ServiceCriticality } from '@objectstack/spec/system'; +import { CoreServiceName, KernelServiceMapSchema, KernelServiceStatusSchema, ServiceConfigSchema, ServiceCriticalitySchema } from '@objectstack/spec/system'; +import type { CoreServiceName, KernelServiceMap, KernelServiceStatus, ServiceConfig, ServiceCriticality } from '@objectstack/spec/system'; // Validate data const result = CoreServiceName.parse(data); @@ -53,6 +53,22 @@ const result = CoreServiceName.parse(data); --- +--- + +## KernelServiceStatus + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `Enum<'metadata' \| 'data' \| 'auth' \| 'file-storage' \| 'search' \| 'cache' \| 'queue' \| 'automation' \| 'analytics' \| 'realtime' \| 'job' \| 'notification' \| 'ai' \| 'i18n' \| 'ui'>` | ✅ | | +| **enabled** | `boolean` | ✅ | | +| **status** | `Enum<'running' \| 'stopped' \| 'degraded' \| 'initializing'>` | ✅ | | +| **version** | `string` | optional | | +| **provider** | `string` | optional | Implementation provider (e.g. "s3" for storage) | +| **features** | `string[]` | optional | List of supported sub-features | + + --- ## ServiceConfig @@ -79,19 +95,3 @@ const result = CoreServiceName.parse(data); --- -## ServiceStatus - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **name** | `Enum<'metadata' \| 'data' \| 'auth' \| 'file-storage' \| 'search' \| 'cache' \| 'queue' \| 'automation' \| 'analytics' \| 'realtime' \| 'job' \| 'notification' \| 'ai' \| 'i18n' \| 'ui'>` | ✅ | | -| **enabled** | `boolean` | ✅ | | -| **status** | `Enum<'running' \| 'stopped' \| 'degraded' \| 'initializing'>` | ✅ | | -| **version** | `string` | optional | | -| **provider** | `string` | optional | Implementation provider (e.g. "s3" for storage) | -| **features** | `string[]` | optional | List of supported sub-features | - - ---- - diff --git a/packages/spec/api-surface/system.json b/packages/spec/api-surface/system.json index e95c68cf69..cb989fdaea 100644 --- a/packages/spec/api-surface/system.json +++ b/packages/spec/api-surface/system.json @@ -291,6 +291,8 @@ "JobSchema (const)", "KernelServiceMap (type)", "KernelServiceMapSchema (const)", + "KernelServiceStatus (type)", + "KernelServiceStatusSchema (const)", "KeyManagementProvider (type)", "KeyManagementProviderSchema (const)", "KeyRotationPolicy (type)", @@ -573,7 +575,6 @@ "ServiceLevelObjectiveParsed (type)", "ServiceLevelObjectiveSchema (const)", "ServiceRequirementDef (const)", - "ServiceStatusSchema (const)", "SettingsActionResult (type)", "SettingsActionResultSchema (const)", "SettingsChangeEvent (type)", diff --git a/packages/spec/authorable-surface/system.json b/packages/spec/authorable-surface/system.json index 240bbcd68d..bd41665e5e 100644 --- a/packages/spec/authorable-surface/system.json +++ b/packages/spec/authorable-surface/system.json @@ -540,6 +540,12 @@ "system/JobExecution:jobId", "system/JobExecution:startedAt", "system/JobExecution:status", + "system/KernelServiceStatus:enabled", + "system/KernelServiceStatus:features", + "system/KernelServiceStatus:name", + "system/KernelServiceStatus:provider", + "system/KernelServiceStatus:status", + "system/KernelServiceStatus:version", "system/KeyRotationPolicy:autoRotate", "system/KeyRotationPolicy:enabled", "system/KeyRotationPolicy:frequencyDays", @@ -1051,12 +1057,6 @@ "system/ServiceLevelObjective:period", "system/ServiceLevelObjective:sli", "system/ServiceLevelObjective:target", - "system/ServiceStatus:enabled", - "system/ServiceStatus:features", - "system/ServiceStatus:name", - "system/ServiceStatus:provider", - "system/ServiceStatus:status", - "system/ServiceStatus:version", "system/SettingsActionResult:details", "system/SettingsActionResult:message", "system/SettingsActionResult:ok", diff --git a/packages/spec/docs-import-surface.baseline.json b/packages/spec/docs-import-surface.baseline.json index 0ed804c285..9f9e1e4963 100644 --- a/packages/spec/docs-import-surface.baseline.json +++ b/packages/spec/docs-import-surface.baseline.json @@ -42,7 +42,6 @@ "system/AddFieldOperation — no type export", "system/CreateObjectOperation — no type export", "system/DeployStatusEnum — no type export", - "system/ServiceStatus — no type export", "ui/AIChatWindowProps — no type export", "ui/DerivedMeasureOp — no type export", "ui/ElementButtonProps — no type export", diff --git a/packages/spec/export-origins/system.json b/packages/spec/export-origins/system.json index 6f3c51ab93..fd153db9f0 100644 --- a/packages/spec/export-origins/system.json +++ b/packages/spec/export-origins/system.json @@ -291,6 +291,8 @@ "JobSchema": "src/system/job.zod.ts#JobSchema (const)", "KernelServiceMap": "src/system/core-services.zod.ts#KernelServiceMap (type)", "KernelServiceMapSchema": "src/system/core-services.zod.ts#KernelServiceMapSchema (const)", + "KernelServiceStatus": "src/system/core-services.zod.ts#KernelServiceStatus (type)", + "KernelServiceStatusSchema": "src/system/core-services.zod.ts#KernelServiceStatusSchema (const)", "KeyManagementProvider": "src/system/encryption.zod.ts#KeyManagementProvider (type)", "KeyManagementProviderSchema": "src/system/encryption.zod.ts#KeyManagementProviderSchema (const)", "KeyRotationPolicy": "src/system/encryption.zod.ts#KeyRotationPolicy (type)", @@ -573,7 +575,6 @@ "ServiceLevelObjectiveParsed": "src/system/metrics.zod.ts#ServiceLevelObjectiveParsed (type)", "ServiceLevelObjectiveSchema": "src/system/metrics.zod.ts#ServiceLevelObjectiveSchema (const)", "ServiceRequirementDef": "src/system/core-services.zod.ts#ServiceRequirementDef (const)", - "ServiceStatusSchema": "src/system/core-services.zod.ts#ServiceStatusSchema (const)", "SettingsActionResult": "src/system/settings-manifest.zod.ts#SettingsActionResult (type)", "SettingsActionResultSchema": "src/system/settings-manifest.zod.ts#SettingsActionResultSchema (const)", "SettingsChangeEvent": "src/system/settings-client.zod.ts#SettingsChangeEvent (type)", diff --git a/packages/spec/json-schema.manifest/system.json b/packages/spec/json-schema.manifest/system.json index 40c12cf943..640169f655 100644 --- a/packages/spec/json-schema.manifest/system.json +++ b/packages/spec/json-schema.manifest/system.json @@ -115,6 +115,7 @@ "system/JobExecution", "system/JobExecutionStatus", "system/KernelServiceMap", + "system/KernelServiceStatus", "system/KeyManagementProvider", "system/KeyRotationPolicy", "system/LWWRegister", @@ -222,7 +223,6 @@ "system/ServiceCriticality", "system/ServiceLevelIndicator", "system/ServiceLevelObjective", - "system/ServiceStatus", "system/SettingsActionResult", "system/SettingsChangeEvent", "system/SettingsManifest", diff --git a/packages/spec/scripts/lib/renamed-defs.ts b/packages/spec/scripts/lib/renamed-defs.ts index acce867e88..e240bb0846 100644 --- a/packages/spec/scripts/lib/renamed-defs.ts +++ b/packages/spec/scripts/lib/renamed-defs.ts @@ -137,6 +137,30 @@ export const RENAMED_DEFS: Readonly> = { // always had), which no ratchet here measures and which the changeset states. // `shared/HttpMethodSubset` is a plain manifest addition. 'ui/HttpMethod': 'ui/HttpMethodSubset', + + // #6604 / ADR-0112 D9a — `ServiceStatus` was published by `./api` AND + // `./system` for two disjoint concepts: the discovery HEALTH ENUM + // (`api/discovery.zod.ts`, `z.enum(['operational', ...])`) and the kernel + // service STATE OBJECT (`system/core-services.zod.ts`, a `features`-bearing + // record). Maintainer ruling 2026-08-08 (Option B): the kernel side is + // renamed and `./api` keeps the published bare name untouched — the smallest + // blast radius, because the system side had no type alias yet, so the name + // had not petrified there. + // + // 6 keys carried (`enabled` / `features` / `name` / `provider` / `status` / + // `version`) — every one of them re-emitted byte-for-byte under the new def, + // which is the whole point of routing this through the table rather than the + // retirement kit: NOTHING left the author-facing contract. `api/ServiceStatus` + // is deliberately absent — it is still emitted, by the enum that always + // declared it, so the table would (correctly) reject it as a copy. + // + // This entry is what finally lets the `./system` side declare + // `KernelServiceStatus`: #4593's alias backfill had to skip this one name + // because declaring `ServiceStatus` on both entry points would have minted + // the #4411 dual-source trap `dual-source-exports.baseline.json` stays empty + // to prevent. The rename removes the collision at its source, so the baseline + // stays empty rather than gaining its first exception. + 'system/ServiceStatus': 'system/KernelServiceStatus', // 6 keys carried }; /** diff --git a/packages/spec/scripts/schema-index.test.ts b/packages/spec/scripts/schema-index.test.ts index 66bfe1f332..d41a75c1dd 100644 --- a/packages/spec/scripts/schema-index.test.ts +++ b/packages/spec/scripts/schema-index.test.ts @@ -97,11 +97,19 @@ describe('exportedValueNames', () => { }); describe('buildSchemaIndex — cross-category same name', () => { - // The live specimen on `main`: `ServiceStatus` is an enum declared in - // `api/discovery.zod.ts` AND an object declared in + // Drawn from the live specimen this behaviour was found on: `ServiceStatus` + // was an enum declared in `api/discovery.zod.ts` AND an object declared in // `system/core-services.zod.ts`. The bare-name index let `system` (walked // later) win, and then placed the API schema on `api/core-services.mdx` — // a page with no `api/core-services.zod.ts` behind it. + // + // That specimen is no longer live: #6604 renamed the system side to + // `KernelServiceStatusSchema`, so the two categories no longer publish the + // name. The fixture stays SYNTHETIC and unchanged on purpose — what it pins + // is that cross-category same-name resolution is correct whenever it occurs, + // and a rename of one specimen retires the specimen, not the property. Wiring + // the fixture to whatever names happen to collide on `main` today would make + // this coverage evaporate the next time someone tidies those names up. const idx = index( file('api', 'discovery.zod.ts', 'export const ServiceStatus = z.enum([]);'), file('system', 'core-services.zod.ts', 'export const ServiceStatusSchema = z.object({});'), diff --git a/packages/spec/src/system/core-services.test.ts b/packages/spec/src/system/core-services.test.ts index a83efa52a2..78b988c565 100644 --- a/packages/spec/src/system/core-services.test.ts +++ b/packages/spec/src/system/core-services.test.ts @@ -3,7 +3,7 @@ import { CoreServiceName, ServiceCriticalitySchema, ServiceRequirementDef, - ServiceStatusSchema, + KernelServiceStatusSchema, KernelServiceMapSchema, ServiceConfigSchema, } from './core-services.zod'; @@ -73,9 +73,9 @@ describe('ServiceRequirementDef', () => { }); }); -describe('ServiceStatusSchema', () => { +describe('KernelServiceStatusSchema', () => { it('should accept valid service status', () => { - const status = ServiceStatusSchema.parse({ + const status = KernelServiceStatusSchema.parse({ name: 'metadata', enabled: true, status: 'running', @@ -90,7 +90,7 @@ describe('ServiceStatusSchema', () => { const statuses = ['running', 'stopped', 'degraded', 'initializing']; statuses.forEach((s) => { - expect(() => ServiceStatusSchema.parse({ + expect(() => KernelServiceStatusSchema.parse({ name: 'data', enabled: true, status: s, @@ -99,7 +99,7 @@ describe('ServiceStatusSchema', () => { }); it('should accept optional fields', () => { - const status = ServiceStatusSchema.parse({ + const status = KernelServiceStatusSchema.parse({ name: 'file-storage', enabled: true, status: 'running', @@ -114,7 +114,7 @@ describe('ServiceStatusSchema', () => { }); it('should reject invalid service name', () => { - expect(() => ServiceStatusSchema.parse({ + expect(() => KernelServiceStatusSchema.parse({ name: 'invalid', enabled: true, status: 'running', @@ -122,8 +122,8 @@ describe('ServiceStatusSchema', () => { }); it('should reject missing required fields', () => { - expect(() => ServiceStatusSchema.parse({})).toThrow(); - expect(() => ServiceStatusSchema.parse({ name: 'metadata' })).toThrow(); + expect(() => KernelServiceStatusSchema.parse({})).toThrow(); + expect(() => KernelServiceStatusSchema.parse({ name: 'metadata' })).toThrow(); }); }); diff --git a/packages/spec/src/system/core-services.zod.ts b/packages/spec/src/system/core-services.zod.ts index d63c18b51c..6469461a92 100644 --- a/packages/spec/src/system/core-services.zod.ts +++ b/packages/spec/src/system/core-services.zod.ts @@ -224,9 +224,17 @@ export const ServiceRequirementDef = { // ========================================== /** - * Describes the availability and health of a service + * Describes the availability and health of a kernel service. + * + * Named `KernelServiceStatus`, not `ServiceStatus`: `./api` publishes its own + * `ServiceStatus` — the discovery health **enum** in `api/discovery.zod.ts` — + * and the two are different concepts (a health value vs this `features`-bearing + * object). One name for two declarations across two entry points is the + * #4411 trap `check:dual-source-exports` guards, so the kernel side carries the + * `Kernel` prefix its sibling `KernelServiceMapSchema` already uses (#6604, + * maintainer ruling 2026-08-08 Option B). */ -export const ServiceStatusSchema = lazySchema(() => z.object({ +export const KernelServiceStatusSchema = lazySchema(() => z.object({ name: CoreServiceName, enabled: z.boolean(), status: z.enum(['running', 'stopped', 'degraded', 'initializing']), @@ -234,6 +242,7 @@ export const ServiceStatusSchema = lazySchema(() => z.object({ provider: z.string().optional().describe('Implementation provider (e.g. "s3" for storage)'), features: z.array(z.string()).optional().describe('List of supported sub-features'), })); +export type KernelServiceStatus = z.input; /** * The Contract definition for what the Kernel MUST expose diff --git a/packages/spec/src/type-alias-convention.pin.test.ts b/packages/spec/src/type-alias-convention.pin.test.ts index 2e3a1bec45..9bd7a302bc 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'; // --------------------------------------------------------------------------- -// 823 isomorphic aliases: `z.input` === `z.infer`, so no `XParsed` is declared. +// 824 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 @@ -1512,6 +1512,7 @@ export type Iso805 = Assert, z.in export type Iso806 = Assert, z.infer< typeof M124.KernelServiceMapSchema > >>; export type Iso807 = Assert, z.infer< typeof M124.ServiceConfigSchema > >>; export type Iso808 = Assert, z.infer< typeof M124.ServiceCriticalitySchema > >>; +export type Iso835 = Assert, z.infer< typeof M124.KernelServiceStatusSchema > >>; // system/metadata-persistence.zod.ts export type Iso809 = Assert, z.infer< typeof M138.MetadataStateSchema > >>; @@ -1622,7 +1623,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 823 isomorphic pins', () => { + it('still declares all 824 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 @@ -1765,9 +1766,28 @@ describe('ADR-0122 type-alias convention', () => { // 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). + // + // 823 -> 824 is #6604 — the `-1` in #4593's arithmetic above, collected. + // That subtraction was not a measurement but an OPEN QUESTION: the alias + // for `system/ServiceStatus` was withheld because declaring it would have + // minted the #4411 dual-source trap against `./api`'s discovery health + // enum, and which of the two names was wrong is a rename decision. The + // maintainer made it on 2026-08-08 (Option B): the kernel side is now + // `KernelServiceStatusSchema`, matching its `KernelServiceMapSchema` + // sibling two lines up in the pin list, and the name it vacated stays + // `./api`'s alone. So the alias the -1 deferred exists at last, under a + // different name than the one it was deferred under — and `Iso835`, not + // `Iso809`, is its id, because the ids are claims about pins and not + // positions (same rule the #4914 decrease above records). Isomorphism + // MEASURED the same way as its 73 siblings: the tree is one enum, one + // boolean, one inline `z.enum` and three optional strings/arrays — no + // `.default()`, `.transform()`, `.catch()` or `.pipe()` anywhere — so the + // two shapes coincide and ADR-0122 gives it a pin rather than an + // `XParsed`. The rename itself contributes 0: renaming a schema const + // moves no shape, and this file counts pins, not names. const self = readFileSync(fileURLToPath(import.meta.url), 'utf8'); const pins = self.match(/^export type Iso\d+ = Assert